Redaction pattern reference¶
The complete, normative list of what redact.String matches and what it puts in
its place. Every pattern below is quoted verbatim from redact.go; if this page
and the code ever disagree, the code is right and this page is a bug.
There is no way to add, remove, or reorder these rules — see What redact does not do. For why the catalogue is shaped this way, see Why the rule catalogue looks like this.
The order rules are applied in¶
String runs eight kinds of rule in a fixed order, each operating on the output
of the last:
| # | Rule | Replacement |
|---|---|---|
| 1 | URL userinfo | <redacted> |
| 2 | Credential assignments (name=value) |
*** |
| 3 | JSON credential fields | *** |
| 4 | Authorization headers in free text | *** |
| 5 | AWS secret-key assignments | *** |
| 6 | JSON Web Tokens | <redacted-token> |
| 7 | Well-known provider prefixes (11 patterns, in table order) | <prefix>*** |
| 8 | The long-opaque-token fallback | <redacted-token> |
Order is load-bearing in one direction only: the specific, high-confidence rules
run before the broad fallback, so a secret a named rule already masked is never
re-examined. That is why token= followed by a 41-character value comes back as
token=*** and not token=<redacted-token> — rule 2 got there first.
URL userinfo¶
regexp.MustCompile(`([A-Za-z][A-Za-z0-9+.\-]*://)[^/\s:@]*:[^/\s@]+@`)
// replacement: "${1}<redacted>@"
Matches the user:password@ component of a URL for any RFC 3986 scheme, not
just HTTP. The scheme is captured and written back verbatim, so the shape of the
URL survives redaction.
https://admin:hunter2@api.example.com/v1 → https://<redacted>@api.example.com/v1
postgres://svc:s3cret@db.internal:5432/x → postgres://<redacted>@db.internal:5432/x
redis://:hunter2@cache.internal:6379 → redis://<redacted>@cache.internal:6379
mongodb+srv://u:p@cluster.example/db → mongodb+srv://<redacted>@cluster.example/db
ldap://cn=admin:pw@host → ldap://<redacted>@host
Connection strings in dial errors are the single most common shape of leaked
credential, which is why the scheme is left open rather than restricted to
http/https.
The username half may be empty (redis://:pass@host matches) but the colon and
the password may not. These are not matched:
| Input | Why not |
|---|---|
user:pass@host |
No ://, so bare a:b@c prose stays intact |
https://user@host/path |
No :password — a token used as the whole userinfo is not caught here |
https://:@host/path |
Password is empty |
file:///etc/passwd |
No userinfo at all |
Credential assignments (name=value)¶
regexp.MustCompile(`(?i)\b(apikey|api_key|key|access_token|refresh_token|token|secret|password|auth|authorization|signature)=([^&\s]+)`)
// replacement: "$1=***"
Case-insensitively matches one of these names followed by =, and masks the
value while keeping the name:
apikey, api_key, key, access_token, refresh_token, token, secret,
password, auth, authorization, signature
Despite the "query parameter" framing this rule is not limited to URLs. It fires
on any name=value in free text, which is deliberate — the same names turn up in
command lines and environment dumps:
POST /v1?apikey=sk-abc123&user=bob → POST /v1?apikey=***&user=bob
--api-key=sk-abc123 --verbose → --api-key=*** --verbose
login?user=bob&password=hunter2 → login?user=bob&password=***
The value runs to the next whitespace or &, and nothing else. Any other
delimiter is swallowed with the value, so redaction can mangle surrounding
structure:
| Input | Output |
|---|---|
Cookie: token=abc; theme=dark |
Cookie: token=*** theme=dark — the ; goes too |
msg="key=abc" other=1 |
msg="key=*** other=1 — the closing quote goes too |
token=abc) |
token=*** — the ) goes too |
The name must start on a word boundary, so a credential name glued to a longer word is not matched, and one separated by a hyphen or a space is:
| Input | Output |
|---|---|
sort_key=name |
unchanged — _ is a word character, so there is no boundary |
sort-key=name |
sort-key=*** — a false positive |
monkey=banana |
unchanged |
primary key=abc |
primary key=*** |
= is required. api_key: abc123 — the YAML or ini spelling — is not
matched by this rule, and only aws_secret_access_key and secret_access_key
have a rule that accepts a colon.
JSON credential fields¶
regexp.MustCompile(`(?i)("(?:apikey|api_?key|access_token|refresh_token|token|secret|client_secret|password|auth|authorization|signature)"\s*:\s*")([^"]+)"`)
// replacement: `${1}***"`
Matches a quoted "key": "value" pair whose key is one of the names above, and
replaces the value with ***. The key, colon and opening quote are captured and
written back, and the closing quote is restored, so a body whose values contain
no escaped quotes stays well-formed JSON. OAuth token-endpoint responses quoted
inside HTTP client errors are the shape this exists for.
The key list is the assignment list plus client_secret, and it matches
apikey and api_key but not api-key.
{"access_token":"ya29.a0abc","token_type":"Bearer"} → {"access_token":"***","token_type":"Bearer"}
{"password": "x"} → {"password": "***"}
{"API_Key":"abc12345"} → {"API_Key":"***"}
{"client_secret":"cs_abc123def"} → {"client_secret":"***"}
{"api-key":"abc"} → unchanged
{"name":"bob","token_type":"bearer"} → unchanged
Whitespace either side of the colon is allowed. Only the value is masked — key names are never touched, which is what keeps a redacted body readable.
This is a text pattern, not a JSON parser. A value containing an escaped quote
("secret":"a\"b") ends the match at that quote and leaves the remainder of the
value in the output.
Authorization headers in free text¶
regexp.MustCompile(`(?i)(authorization:\s*)(bearer|basic|digest|apikey)\s+([A-Za-z0-9._~\-+/=]+)`)
// replacement: "${1}$2 ***"
Matches an authorization: header written into a log line or error message,
followed by one of four schemes — Bearer, Basic, Digest, ApiKey — and
masks the credential while keeping the header name and the scheme, so you can
still see which auth failed.
Authorization: Bearer abc123def456 → Authorization: Bearer ***
Authorization: Basic dXNlcjpwYXNz → Authorization: Basic ***
Proxy-Authorization: Basic dXNlcjpwYXNz → Proxy-Authorization: Basic ***
Authorization:Bearer abc123def456 → Authorization:Bearer ***
Proxy-Authorization matches because the pattern is unanchored and
proxy-authorization: ends in authorization:.
These are not matched:
| Input | Why not |
|---|---|
Authorization: token abc123 |
token is not one of the four schemes |
authorization : Bearer abc |
Whitespace before the colon is not allowed |
BEARER abc123def456 |
The authorization: prefix is required |
X-API-Key: abc123def456 |
This rule only knows authorization: |
That last row is the important one. String does not mask arbitrary header
values — see
Does String mask HTTP header values?
and the sensitive header reference.
AWS secret-key assignments¶
regexp.MustCompile(`(?i)(aws_secret_access_key|secret_access_key)(\s*[=:]\s*)\S+`)
// replacement: "${1}${2}***"
AWS secret access keys carry no distinguishing prefix, so they are matched by the
name they are assigned to rather than by their value — matching a bare 40-
character string would also scrub every git SHA-1 in your logs. The name and the
separator (which may be = or :, with optional surrounding whitespace) are
preserved.
aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/… → aws_secret_access_key=***
AWS_SECRET_ACCESS_KEY = wJalrXUtnFEMI/K7MDENG/… → AWS_SECRET_ACCESS_KEY = ***
secret_access_key : abc def → secret_access_key : *** def
The value is \S+ — everything to the next whitespace. A secret containing a
space is only partly masked; a bare aws_secret_access_key with nothing assigned
is left alone.
JSON Web Tokens¶
regexp.MustCompile(`eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+`)
// replacement: "<redacted-token>"
Matches the three-segment base64url shape beginning with the distinctive eyJ
header prefix (which is {" base64-encoded) and replaces the whole token —
unlike the provider prefixes, there is no fragment worth keeping. It catches bare
Bearer eyJ… tokens that the Authorization-header rule misses because they carry
no authorization: prefix.
All three segments are required, each at least one character: a two-segment
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0 is not matched.
Only the URL-safe base64 alphabet is recognised. Standard base64's + and /,
and = padding, are outside the character class, and a token that uses them
fares badly in either of two ways — a + inside a segment stops the token
matching at all, and a / in the final segment ends the match early, leaving
the tail in the clear:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIi+OiIxIn0.abc → unchanged
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.ab/c= → <redacted-token>/c=
JWTs are specified to use the URL-safe alphabet, so this bites only on a malformed or re-encoded token.
Well-known provider prefixes¶
Eleven patterns whose prefix is unambiguous enough that a match is almost
certainly a real credential. Each keeps its literal prefix for debug
readability and replaces the rest with ***.
The number of characters kept is anchored to the known prefix — it is a constant
per pattern, never measured from the matched text. That is what stops a token
whose body happens to contain _ or - from leaking a fragment of itself.
| Provider | Pattern | Minimum body | Redacts to |
|---|---|---|---|
| OpenAI / Anthropic-style | sk-[A-Za-z0-9_\-]{16,} |
16 | sk-*** |
| GitHub PAT (classic) | ghp_[A-Za-z0-9]{30,} |
30 | ghp_*** |
| GitHub OAuth | gho_[A-Za-z0-9]{30,} |
30 | gho_*** |
| GitHub app server | ghs_[A-Za-z0-9]{30,} |
30 | ghs_*** |
| GitHub fine-grained PAT | github_pat_[A-Za-z0-9_]{30,} |
30 | github_pat_*** |
| GitLab personal access token | glpat-[A-Za-z0-9_\-]{16,} |
16 | glpat-*** |
| GitLab runner token | glrt-[A-Za-z0-9_\-]{16,} |
16 | glrt-*** |
| GitLab deploy token | gldt-[A-Za-z0-9_\-]{16,} |
16 | gldt-*** |
| Slack | xox[baprs]-[A-Za-z0-9-]{10,} |
10 | xoxb-*** (etc.) |
| Google API key | AIza[A-Za-z0-9_\-]{30,} |
30 | AIza*** |
| AWS access key ID | AKIA[A-Z0-9]{16} |
exactly 16 | AKIA*** |
The minimum body length is a hard floor, not a hint. sk- followed by 15
characters is left in the clear; 16 is redacted. AKIA requires exactly 16
uppercase characters, matching the real AWS access-key-ID format —
AKIAIOSFODNN7EXAMPL (15) passes straight through.
The Slack pattern covers xoxb, xoxa, xoxp, xoxr and xoxs only. Other
Slack token families, xoxc among them, are not matched.
The long-opaque-token fallback¶
Anything still left that is a single run of 41 or more characters from
[A-Za-z0-9_-] is replaced whole. This is the catch-all for high-entropy secrets
with no recognisable prefix, and it runs last so it only ever sees what the named
rules did not claim.
The threshold clears the opaque strings that legitimately appear in error text:
| Value | Length | Redacted? |
|---|---|---|
| UUID (no hyphens) | 32 | No |
| MD5 hash | 32 | No |
| UUID (with hyphens) | 36 | No |
| SHA-1 / git commit hash | 40 | No |
| SHA-256 hash | 64 | Yes |
Because - is inside the character class but is not a word character, two
adjacent 41-character runs joined by a hyphen are treated as one token and
replaced with a single <redacted-token>.
Replacement tokens¶
| Token | Length | Used for |
|---|---|---|
<redacted> |
10 | URL userinfo |
*** |
3 | Assignment values, JSON values, Authorization credentials, AWS secret values, provider-prefix bodies |
<redacted-token> |
16 | JWTs and the long-opaque-token fallback |
None of these re-matches any rule, which is what makes String idempotent.
They are not configurable, and there is no marker distinguishing "a rule fired
here" from a literal *** that was in the input already.