Skip to content

Redact HTTP headers

Goal. Strip credential-bearing HTTP header values out of anything you log or ship — using SensitiveHeaderKeys and IsSensitiveHeaderKey to decide which headers to mask.

HTTP headers are a prime leak: Authorization, Cookie, and a swarm of X-*-Token variants all carry secrets, and they end up in log lines and error traces whenever a request or response is dumped. redact gives you two tools to find them.

The two tools

// The canonical list of header names whose values should be redacted.
var redact.SensitiveHeaderKeys []string

// Reports whether a header name looks credential-bearing (case-insensitive).
func redact.IsSensitiveHeaderKey(name string) bool

SensitiveHeaderKeys is the explicit, curated set:

Authorization        Proxy-Authorization   Cookie              Set-Cookie
X-API-Key            X-API-Token           X-Auth-Token        X-Access-Token
X-CSRF-Token         X-Session-Token

IsSensitiveHeaderKey is deliberately wider than that list. It returns true for any name in SensitiveHeaderKeys (case-insensitive exact match) and for any name whose words include auth, token, key, secret, bearer, password, or credential. The question it answers is "is the operator likely to have put a secret in this header?" — not "is this on my allowlist?" That makes it the right predicate for logging, where erring toward redaction is the safe default.

Redact header values in logging middleware

Use IsSensitiveHeaderKey as the predicate: mask the value of any header that looks credential-bearing, and pass everything else through untouched.

package httplog

import (
    "net/http"

    "gitlab.com/phpboyscout/go/redact"
)

// redactHeaders returns a copy of h safe to log: sensitive values are masked,
// everything else is preserved.
func redactHeaders(h http.Header) http.Header {
    safe := make(http.Header, len(h))
    for name, values := range h {
        if redact.IsSensitiveHeaderKey(name) {
            safe[name] = []string{"***"}
            continue
        }
        safe[name] = values
    }
    return safe
}

http.Header keys are canonicalised (Authorization, X-Api-Key), and IsSensitiveHeaderKey compares case-insensitively, so you do not need to normalise the name yourself.

Belt and braces: redact the surviving values too

Even a header that is not on the sensitive list can carry an accidental secret — a Location redirect with an embedded token, a custom header holding a URL. Run the values you keep through redact.String as a second pass:

func redactHeaders(h http.Header) http.Header {
    safe := make(http.Header, len(h))
    for name, values := range h {
        if redact.IsSensitiveHeaderKey(name) {
            safe[name] = []string{"***"}
            continue
        }

        masked := make([]string, len(values))
        for i, v := range values {
            masked[i] = redact.String(v) // catches tokens hiding in "safe" headers
        }
        safe[name] = masked
    }
    return safe
}

Choosing a stricter predicate

If you want to redact only the curated set and leave fuzzy-matched headers intact — for example, to keep a diagnostic X-Request-Token visible — build your own lookup from SensitiveHeaderKeys instead of using IsSensitiveHeaderKey:

import (
    "strings"

    "gitlab.com/phpboyscout/go/redact"
)

// exactSensitive is the strict allowlist: only the curated header names.
var exactSensitive = func() map[string]struct{} {
    m := make(map[string]struct{}, len(redact.SensitiveHeaderKeys))
    for _, k := range redact.SensitiveHeaderKeys {
        m[strings.ToLower(k)] = struct{}{}
    }
    return m
}()

func isExactlySensitive(name string) bool {
    _, ok := exactSensitive[strings.ToLower(name)]
    return ok
}

Build the map once (as above) rather than iterating the slice on every header. SensitiveHeaderKeys is a package-level slice; treat it as read-only — if you need more entries, compose a wider set locally instead of appending to it.