Exempt placeholders from a credential rule
A credential recognizer is carried by its prefix. ghp_ followed by
thirty-six characters is a GitHub token, and the recognizer cannot tell
whether those thirty-six characters came from GitHub or from the
.env.example in your repository. A coding assistant carries more of
that text than of any real key.
So this happens:
guardrails: rules: - name: no credentials to the model when: 'annotations.exists(a, a.category == "credential")' action: deny message: "that looks like an API key"and a developer asking about their own example configuration gets a
403. The rule is right, the annotation is right, and the request
should have gone through anyway.
There are two places to say so, and they are not equivalent.
Prefer the rule
Section titled “Prefer the rule”Narrow the condition. The annotation stays, so the audit trail still records that something key-shaped was in the request, and the exemption applies to this one rule rather than to the whole engine.
guardrails: rules: - name: no credentials to the model when: >- annotations.exists(a, a.category == "credential" && !a.text.matches("(x{8,}|X{8,}|0{8,})")) action: deny message: "that looks like an API key"a.text is the matched span. Everything CEL can do to a string is
available, so the exemption can be as narrow as the thing you are
actually exempting:
| what you are exempting | condition to add |
|---|---|
| filler runs | `!a.text.matches(“(x{8,} |
| one known example key | a.text != "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" |
| a vendor’s documented value | !a.text.endsWith("EXAMPLE") |
| anything in your fixtures directory | a.ref != "messages[0].content", or better, use kinds: |
Test it before it ships:
$ pistra -config pistra.yaml -test-rules pistra.tests.yamlThat is the cheap way to find out that the exemption you wrote also exempts something you meant to deny. See Test your rules before they ship.
Or drop the annotation entirely
Section titled “Or drop the annotation entirely”allow_list is the detector-layer answer. It removes the finding before
any rule sees it:
guardrails: allow_list: - '^ghp_x+$' - '^AKIA[X0]+$' allow_list_match: regex detectors: - type: piiReach for this when the value is genuinely not a secret anywhere in the deployment, your own demo card number, the support mailbox, the fixed example key in your onboarding docs. It is the blunter instrument. The finding is gone for every rule, and gone from the evidence.
Anchor every entry
Section titled “Anchor every entry”allow_list_match: regex is Presidio’s reading, and Presidio’s reading
is wider than it looks. Entries are joined into one alternation, matched
anywhere inside the span, case-insensitively. So this:
allow_list: ['x{8,}'] # do not do this allow_list_match: regexdoes not allow “the placeholder”. It allows every span containing eight x’s in a row, including a real token that happens to contain one, and including entity types that have nothing to do with credentials:
allow_list: [x{8,}] ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx suppressed (intended) ghp_xxxxxxxxxxURq30rOdYdoJ47aDAZYtitYX suppressed (a real token) xxxxxxxxxx@example.com suppressed (an address)^ghp_x+$ says what you meant. The default is exact and
case-sensitive for this reason. Widening this list is not a convenience,
it is a hole. It is a quiet one, because a suppressed finding leaves
nothing behind to notice.
What neither of these fixes
Section titled “What neither of these fixes”A placeholder that was generated rather than typed is indistinguishable from a key. It is not hard to distinguish but indistinguishable, because it was produced the same way:
AIzaSyAnLA7NfeLquW1tJFpx_eQCxoX-oo6YyIsAIzaSyCkEhVjf3pduRDt6d1yKOMitrUEke8agEMThose are two of sixteen fake Google keys that circulate widely enough that secret scanners carry them by name. No statistic separates them from a real key, and pistra does not try. There is no entropy floor and no token-efficiency check, because both eventually drop a real credential. A dropped credential is silent where a wrongly denied request is not.
If values like these appear in your traffic, list them. That is what everyone else does with them too.
The one row this does not apply to
Section titled “The one row this does not apply to”PISTRA_KEY declares a checksum, so pistra’s own virtual keys are the
one format where a placeholder is refused rather than reported.
pistra_ followed by forty-nine A’s is not annotated at all. Where a
vendor publishes a checksum precisely enough to reimplement, a row can
do the same. Most do not, and a guess is worse than nothing. GitHub
documents “a 32 bit checksum in the last 6 digits” without saying what
it is computed over, so a validator built on that would drop every real
token of that format and say nothing. The
credential coverage page says,
per row, which ones carry a check and why the rest do not.
Related
Section titled “Related”- Give a team its own policy, a rule can be scoped to one profile, so the exemption need not be deployment-wide either
- Test your rules before they ship, fixtures for the condition you just narrowed
- The guardrail pipeline, why detectors annotate and only rules decide, and why the rule layer is the better place for this
- Credential coverage, every credential format, and which of them carries a checksum