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.
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.
Related¶
- Redact at the boundary — the general discipline.
- The rule catalogue & its limits — what
redact.Stringcatches inside a header value.