Redact HTTP headers¶
Goal. Strip credential-bearing HTTP header values out of anything you log or ship — using
SensitiveHeaderKeysandIsSensitiveHeaderKeyto 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.
Related¶
- Redact at the boundary — the general discipline.
- Sensitive header reference — the curated list, the fuzzy word list, and a matched/not-matched table.
- Redaction pattern reference — what
redact.Stringcatches inside a header value.