Why rtb-redact matches shapes, not values¶
This page explains the thinking the rule set is built on. For what the rules actually do, see Redaction rules.
The leak this exists to stop¶
The secrets that escape are rarely the ones you were holding. You already mask the API key you read from config, because you knew it was a secret when you read it. The line that ends up in a log aggregator is the other kind:
- an upstream service quotes your bearer token back at you inside a
401body, and that error string goes into the log the way error strings do; - a connection string with the password in its userinfo lands in a debug line;
- a client library stringifies a whole request, headers and all, to be helpful.
None of those strings were assembled by code that knew a credential was in them. You cannot mask a value you never had a variable pointing at.
Why there is no registry of known secrets¶
The obvious design is to hand the redactor the secrets you hold, so it can watch for them. It is also the design that misses everything above, because the dangerous secret is exactly the one you do not have a copy of.
So rtb-redact keeps no state and holds no values. It knows what credentials
look like — a sk-ant- prefix, a user:password@ in a URL, three base64url
segments beginning eyJ — and it recognises them wherever they turn up,
including in a string produced by code you did not write.
The trade is that it can only catch shapes it has seen before. That is a real limit, and it is set out in What rtb-redact does not do.
Why redaction belongs at the boundary¶
Redacting everywhere is expensive and makes local debugging worse. Redacting nowhere is how the leak happens. The useful line is the boundary: the moment a string leaves for somewhere you cannot reach back into.
A telemetry vendor, a log aggregator, a crash reporter, a GitLab issue body — once a credential lands there it is replicated, indexed and retained on someone else's schedule. A debug line on your own machine that never leaves is a different case, and there the raw content is often the thing you need.
So the guidance is directional: redact on the way out. In this toolkit that
means rtb-telemetry runs string over an event's args and err_msg before
any out-of-process sink serialises them, and anything else emitting free-form
text to an external surface calls string at the same point.
Why it would rather over-redact¶
Two failure modes are available to a rule, and they do not cost the same.
A false negative puts a live credential in a third party's index. You cannot take it back; the remedy is rotating the secret and hoping nobody read the log first.
A false positive costs you a word out of a log line. It is annoying — losing a git SHA in a build log or the noun after the word "Basic" makes a line harder to read — but it is recoverable, because the line was never the source of truth.
So where a rule has to guess, it guesses toward redacting. That is why the
Authorization rule eats the word after "Token" even in prose, and why the query
parameter rule takes the value all the way to the next & or space rather than
trying to be clever about where a value really ends.
It is a defensible default, not a law of nature. If a particular false positive is costing you real debugging time, redact later in the pipeline or keep the raw line on a surface that never leaves the host.
Why the threshold is 40 characters here and 41 in the Go module¶
The last rule is the fallback: any whitespace-bounded run of 40 or more characters from the base64/hex alphabet is assumed to be a token. It exists because most opaque credentials carry no prefix at all, and length plus entropy is the only signal left.
Wherever the line is drawn, some legitimate identifier sits near it:
| Value | Length |
|---|---|
| MD5 digest | 32 |
| UUID with hyphens | 36 |
| git SHA-1 | 40 |
| SHA-256 digest | 64 |
At 40, this crate redacts a bare git commit hash. The Go sibling
go/redact sets the same fallback at 41
specifically so that a SHA-1 survives.
The two implementations are not bug-for-bug identical, and this is the difference most likely to surprise you: the same log line passed through a Rust service and a Go service can come out differently, and the Rust one will be the more aggressive of the two. If you are comparing redacted output across the toolkit's two language stacks, this is why.
Whether the extra character is worth it depends on what your logs are full of. Build and CI logs are full of commit hashes, and losing them hurts. Application logs mostly are not.
Why the rules run in a fixed order¶
Each rule runs over the output of the previous one, narrowest first, with the length-based fallback last. Ordering matters in two places:
- The PEM rule runs before the token rules so that a key body — which is a long opaque run several times over — collapses into a single marker rather than a block of separately-masked lines.
- The named provider rules run before the fallback so that a token that was already recognised is not re-examined and partially re-masked.
The consequence is that the output is stable and idempotent: running string
over its own output changes nothing, because [redacted] matches no rule.
Why there is nothing to configure¶
There are no feature flags, no options struct and no way to register a pattern. A redactor that can be configured can be configured wrongly, and the failure is silent — you find out when the secret is already in someone else's index. A fixed rule set means every caller in every service gets the same behaviour, and the behaviour can be tested once.
The cost is that covering a credential format the crate does not know about is not a config change; it is either work you do at the call site, or a change to this crate. Cover a secret the rules miss walks through both.
Related¶
- What rtb-redact does not do
- Redaction rules — the catalogue itself
- Redact before an external surface