Skip to content

Where redaction applies

Two of an event's fields are scrubbed for secrets automatically. One is not. The split is deliberate, and getting it wrong is the most likely way to leak something.

The rule

Event::redacted() returns a clone with args and err_msg passed through rtb_redact::string. FileSink, HttpSink and OtlpSink — every sink that writes outside the process — call it before serialising. MemorySink and NoopSink do not, because neither one lets the event leave.

Nothing else in the event is touched: not name, not tool, not machine_id, and not attrs.

Why args and err_msg are scrubbed and attrs are not

args and err_msg exist to hold text nobody controls the shape of. A command line contains paths, hostnames and the occasional pasted token; an error message contains whatever an upstream library decided to print, including the Authorization header it just failed to authenticate with. Fields whose whole purpose is arbitrary text get a scrubber by default, because the alternative is asking every caller to remember.

attrs is the opposite: a map the caller fills in with values the caller chose. The intended contents are a command name, an outcome such as ok/error/cancelled, a duration bucket, a version string — a small enumerated vocabulary that is safe by construction and useful precisely because it is stable. Running a secret-scrubber over a set of known-safe enumerated values buys nothing and costs a regex pass per attribute.

So the boundary is: free-form text goes in args or err_msg and gets scrubbed; enumerated values go in attrs and do not. If you have free-form text and want it redacted, either put it in one of the two scrubbed fields or call rtb_redact::string yourself before inserting it.

What the scrubber catches

rtb-redact applies a fixed set of patterns and replaces each match with [redacted]:

  • URL userinfo — postgres://app:hunter2@db/mydb
  • Bearer / Basic / Token credentials in header-shaped text
  • sensitive query parameters — api_key, access_token, token, password, secret, signature, auth and near-spellings of those
  • well-known provider key prefixes — sk-, sk-ant-, ghp_, glpat-, AIza, AKIA/ASIA, xoxb- and friends, at 20 characters or more
  • JWT-shaped tokens of 100 characters or more
  • PEM private key blocks
  • any whitespace-bounded run of 40+ base64/hex-ish characters

What it does not catch, and why that matters here

The list above is a pattern set, not a proof. A credential that does not match a pattern — a short shared password, an internal token with a bespoke shape, a customer name in a file path — passes through unchanged. Redaction reduces the blast radius of an accidental paste; it is not a licence to send arbitrary text to a collector.

Which is the real reason attrs is the safe-by-construction field. The enumerated-value discipline is what keeps sensitive data out of events in the first place, and the scrubber is the safety net under the two fields where that discipline cannot apply.

Redaction happens per emit, not once

redacted() clones the event and scrubs the clone, so the event you hold after record is unchanged, and each sink scrubs independently. With a MemorySink in a test you are therefore asserting against the raw event — useful when you want to check what the caller passed, and worth remembering when you are trying to prove that redaction happened. To test redaction, assert on the FileSink output, or call event.redacted() yourself.