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.

Why redact.String is not enough on its own

redact.String recognises exactly one header shape — authorization: followed by Bearer, Basic, Digest or ApiKey. Every other header line goes through untouched:

Authorization: Bearer abc123def456   →  Authorization: Bearer ***
X-API-Key: abc123def456ghijklmno     →  unchanged
Cookie: session=deadbeef             →  unchanged

So piping a header dump through String and calling it done leaks every credential that is not in an Authorization header. Decide which headers to mask by name, mask those values yourself, and use String afterwards as a second pass over what survives.

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.

Do not append to SensitiveHeaderKeys

It is an exported slice, so the compiler will let you extend it. It will not do what you want:

redact.SensitiveHeaderKeys = append(redact.SensitiveHeaderKeys, "X-Widget")
redact.IsSensitiveHeaderKey("X-Widget") // still false

IsSensitiveHeaderKey reads a lookup map built once at package init from the slice's original contents, so a later append changes nothing — and mutating a package-level slice races with anything else reading it. To widen or narrow the set, copy it into your own structure, as in the previous section, and test against that.

Add the header names your own service uses

The fuzzy predicate matches whole words, so a glued spelling such as X-Apikey is not caught, while X-API-Key is. If your service accepts a name the pattern misses, compose the two checks:

// extraSensitive covers the spellings IsSensitiveHeaderKey does not.
var extraSensitive = map[string]struct{}{
    "x-apikey":     {},
    "x-acme-token": {},
}

func isSensitive(name string) bool {
    if redact.IsSensitiveHeaderKey(name) {
        return true
    }

    _, ok := extraSensitive[strings.ToLower(strings.TrimSpace(name))]

    return ok
}

The sensitive header reference lists which names match and which do not, so you can check yours before adding it.