Redaction rules¶
string and string_into apply the same seven rules, in a fixed order, with no
configuration. This page lists each one: the pattern as it appears in
src/lib.rs, what it replaces, and — the part that usually matters more — what
it leaves alone.
Every example below is the literal output of rtb_redact::string at version
0.6.3.
The fast path, and what it skips¶
Before any rule runs, string scans the input for an anchor. If none is
present it returns the input borrowed, without allocating and without running a
single pattern. An input is anchored if it contains any of:
- one of the characters
@,=,?,-,_,. - the literal
-----BEGIN - a case-insensitive
bearer,basicortoken— with a trailing space, not a tab or newline - a run of 40 or more characters from
A-Z a-z 0-9 + / = _ -
Almost every real log line clears that bar, which is the point: the check exists to make clean strings cheap, not to be exact.
It does have teeth, though. A string whose only secret is made purely of letters and digits, with no anchor character anywhere else in the line, is returned untouched even though a rule below would have matched it:
in : AKIAABCDEFGHIJKLMNOP out: AKIAABCDEFGHIJKLMNOP (unchanged)
in : key AKIAABCDEFGHIJKLMNOP here
out: key AKIAABCDEFGHIJKLMNOP here (unchanged)
in : aws_key=AKIAABCDEFGHIJKLMNOP
out: aws_key=[redacted] (the `_` and `=` anchor it)
The two credential formats this reaches are AWS access key IDs (AKIA…,
ASIA…) and Google API keys (AIza…), both of which are pure alphanumerics
shorter than 40 characters. Provider tokens that carry -, _ or . in their
own shape — sk-…, ghp_…, glpat-…, xox…-…, SG.… — anchor themselves and
are unaffected.
Tab- and newline-separated auth schemes hit the same edge:
The order the rules run in¶
Each rule runs over the output of the one before it:
- URL userinfo
- Authorization-style schemes
- Sensitive query parameters
- PEM private-key blocks
- Well-known provider prefixes
- JSON Web Tokens
- Long opaque runs
The order is deliberate: high-confidence, narrowly-shaped rules run first so that the broad length-based rule at the end only ever sees what is left. The PEM rule runs ahead of the token rules for the same reason — a key body is a long opaque run, and collapsing the whole block first keeps the output readable instead of leaving a half-masked key.
The numbered comments in src/lib.rs (// 1., // 7., // 4. …) are the
original design document's numbering, not the execution order. The list above is
the execution order.
The output is idempotent: redacting an already-redacted string returns it
unchanged, because [redacted] contains no character any rule matches on.
URL userinfo¶
Replaces user:password@ with [redacted]@, keeping the scheme, host and path.
in : connect to postgres://app:hunter2@db.internal/mydb
out: connect to postgres://[redacted]@db.internal/mydb
Any scheme matches — postgres://, redis://, amqp://, not just HTTP.
The scheme's case is preserved as written.
Not matched:
- Userinfo with no password.
https://token@github.com/x/y.gitis left as it is; the pattern requires theuser:passwordpair. - An empty password.
https://user:@host/does not match either. - A password containing
@. The value runs only up to the first@, sohttps://u:p@ss@host/becomeshttps://[redacted]@ss@host/and the tail of the password survives.
Authorization-style schemes¶
Keeps the scheme word, replaces the credential:
in : Authorization: Bearer ghp_abcdefghijklmnopqrstuvwxyz123456
out: Authorization: Bearer [redacted]
The scheme is re-emitted exactly as it was written (bearer stays lowercase),
and whatever whitespace separated it from the credential is normalised to a
single space.
Only these three scheme words are recognised. Digest, ApiKey, Negotiate
and Signature are not.
This rule over-redacts English prose. It cannot tell a scheme from the same word used as a noun, so the word after one is destroyed:
in : we offer a Basic tier out: we offer a Basic [redacted]
in : the Token bucket algorithm out: the Token [redacted] algorithm
If your log lines routinely contain those words as prose, expect this. Losing a word is the deliberate trade against leaking a token — see Why it over-redacts.
Sensitive query parameters¶
(?i)([?&]?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|password
|passwd|secret|signature|sig|auth|x[_-]?api[_-]?key))=[^&\s#]+
Keeps the parameter name, replaces the value:
The recognised names, case-insensitively, with _, - or nothing between the
words: apikey, api_key, api-key, access_token, refresh_token, token,
password, passwd, secret, signature, sig, auth, x-api-key.
Two consequences of how loosely the name is anchored:
- The
?or&is optional, so bare assignments are caught too —password=hunter2andPASSWORD=hunter2in an environment dump both redact. - There is no left-hand word boundary, so any name ending in a recognised
one matches:
csrf_token=,client_secret=andx-api-key=all redact.
Names that are not in the list, and are therefore left in the clear:
| Not matched | Note |
|---|---|
key= |
?key=AIza… only redacts because the Google prefix rule catches the value, not because key is recognised |
authorization= |
auth must be followed immediately by = |
pwd=, credential=, session=, sessionid= |
no rule covers these |
The value runs further than you may expect. It ends at &, whitespace or
# — and at nothing else. A ; or , separator does not stop it, so
neighbouring non-secret data is swallowed:
in : ?password=hunter2;other=1 out: ?password=[redacted]
in : ?password=hunter2#frag out: ?password=[redacted]#frag
PEM private-key blocks¶
The whole block, header to footer, is replaced with a canonical three-line form:
in : -----BEGIN RSA PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
-----END RSA PRIVATE KEY-----
out: -----BEGIN PRIVATE KEY-----
[redacted]
-----END PRIVATE KEY-----
The key type is normalised away. RSA, EC and OPENSSH all come out as
plain PRIVATE KEY, so the output does not tell you which kind of key was
present. Multiple blocks in one string are each replaced.
Not matched:
- Anything that is not a private key.
-----BEGIN CERTIFICATE-----and-----BEGIN PUBLIC KEY-----are not private-key blocks and this rule ignores them. Their bodies may still be caught by the long-opaque rule if the base64 lines are 40 characters or more, but theBEGIN/ENDlines are re-emitted verbatim — a certificate comes back as aCERTIFICATEheader wrapped around a[redacted]body, not as a normalisedPRIVATE KEYblock. - Lowercase markers. The pattern is case-sensitive;
-----begin rsa private key-----passes straight through. - A block with no
ENDline. A truncated key — the common case when a log line is cut short — does not match this rule. Its body is usually still caught by the long-opaque rule, but theBEGINheader stays.
Well-known provider prefixes¶
sk-ant-[A-Za-z0-9_\-]+ (?:ghp|gho|ghs|ghu)_[A-Za-z0-9]+
sk-[A-Za-z0-9_\-]+ glpat-[A-Za-z0-9_\-]+
AIza[A-Za-z0-9_\-]+ (?:AKIA|ASIA)[A-Z0-9]+
xox[baprs]-[A-Za-z0-9\-]+ SG\.[A-Za-z0-9_\-]{22,}\.[A-Za-z0-9_\-]{43,}
A match is replaced whole, prefix included, with [redacted] — the output
does not tell you which provider the token belonged to.
A match shorter than 20 characters in total is left alone. That is what
keeps sk-abc in a sentence from being mangled:
in : short sk-abc tail out: short sk-abc tail (6 chars)
in : sk-abcdefghijklmnopq out: [redacted] (20 chars)
There is no word boundary at the end and no whitespace requirement, so a token inside a URL path or a quoted JSON value is still caught.
Formats not carried here: GitHub fine-grained PATs (github_pat_…), GitLab
runner and deploy tokens (glrt-, gldt-), Stripe (sk_live_…), Twilio
(SK…), and every internal or bespoke credential format. Some are long enough
to be caught by the long-opaque rule instead — which depends on the surrounding
punctuation, not on the token — and some are not caught at all.
JSON Web Tokens¶
Three base64url segments beginning eyJ, replaced whole with [redacted] —
but only when the match is 100 characters or longer. A shorter JWT-shaped
string is left exactly as it is:
in : auth: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwi…
out: auth: [redacted] (155 chars)
in : eyJhbGciOi.eyJzdWIi.Sflabc (26 chars)
out: eyJhbGciOi.eyJzdWIi.Sflabc (unchanged)
A real token with a signed payload clears 100 characters comfortably; an unsigned or minimal one may not, and the dots in it stop the long-opaque rule from catching it either.
Only the eyJ opening is recognised — that is base64 for {", so it covers
tokens whose header is a JSON object, which is every conventional JWT. A JWE or
a token with a non-JSON header does not match.
Long opaque runs¶
The catch-all: a run of 40 or more characters from that alphabet, bounded by
whitespace or by the ends of the string, becomes [redacted]. The boundary
characters themselves are re-emitted, so spacing survives.
Two things follow from the boundary requirement:
- Punctuation defeats it. A 40-character token in parentheses, quotes, or
followed by a
;or,is not whitespace-bounded and is not matched by this rule.Set-Cookie: sid=<40 chars>; Path=/keeps its value in full. - Two long runs separated by a single space, only the first is redacted. The match consumes its trailing space, leaving the second run with no leading boundary:
A two-character separator such as \r\n does not have this problem.
Why 40, and what it costs¶
40 is low enough to catch most opaque session and API tokens and high enough to clear a UUID. What sits either side of the line:
| Value | Length | Redacted by this rule? |
|---|---|---|
| MD5 digest | 32 | No |
| UUID with hyphens | 36 | No |
| git SHA-1 / commit hash | 40 | Yes |
| SHA-256 digest | 64 | Yes |
A bare commit hash in a log line is replaced with [redacted]. That is the
known cost of the threshold, and the most common false positive in practice —
git log output and CI job logs are full of them. The reasoning behind
accepting it is in
Why the threshold is 40.
Long file paths pay the same price when they contain no .:
GET /api/v1/resources/subresources/items/list HTTP becomes
GET [redacted] HTTP, because / is in the alphabet.
Quick lookup: is this redacted?¶
| Input | Result |
|---|---|
postgres://app:hunter2@db/mydb |
postgres://[redacted]@db/mydb |
https://token@github.com/x/y |
unchanged |
Authorization: Bearer <token> |
Authorization: Bearer [redacted] |
?api_key=<value> |
?api_key=[redacted] |
?key=<value> |
unchanged unless the value itself matches a rule |
sk-ant-api03-… (20+ chars) |
[redacted] |
AKIA… alone on a line |
unchanged — no anchor |
| JWT of 100+ chars | [redacted] |
| JWT under 100 chars | unchanged |
| git SHA-1, whitespace-bounded | [redacted] |
| UUID | unchanged |
"password": "hunter2" (JSON) |
unchanged — JSON uses :, not = |
Cookie: session=abc123 |
unchanged — session is not a recognised name |
Related¶
- API reference — the functions that apply these rules.
- What rtb-redact does not do — the limits stated as limits, with the reasoning.
- Cover a secret the rules miss — what to do when your credential is in the "not matched" column.