Skip to content

Credential coverage

These are API keys, access tokens and private key material, found by the same pii detector as the personal data and annotated under credential/… rather than pii/…. They are on by default and cost a scan nothing extra: the same tables, the same prefilter, the same streaming horizon. A rule can select the whole namespace, since a.category == "credential" covers every row here, including ones added after the rule was written. The namespace exists for that reason.

19 recognizers · 19 entity types · 22 patterns · 1 checksum-validated.

A literal prefix carries a row, in every case but one. AKIA, ghp_, sk-ant-, glpat- do not otherwise occur in prose, so precision comes from the prefix rather than from a shape, the scores are high, and an approximate length costs little. Every row is case-sensitive for the same reason. akia is not a key, and matching it would throw away most of what the prefix buys.

A prefix cannot tell a real key from a placeholder wearing its shape. ghp_ and thirty-six x’s is annotated like any other token. One row declares a checksum and refuses those outright; the rest are exempted in configuration, and Exempt placeholders from a credential rule is how.

Max span is the longest match a recognizer can produce, in bytes, and it is what a streamed response is held back by while this type is enabled (see the commit horizon); a † means a pattern has an open repeat charged at a cap rather than measured. Credentials are structurally long, and the widest here is 1082 bytes, though the horizon is the maximum over every enabled recognizer, so a row is only ever as expensive as the widest thing running beside it.

Entity Recognizer Patterns Score +context Checksum Max span Context Format
ANTHROPIC_API_KEY AnthropicKeyRecognizer 1 0.90 1.00 395 anthropic, anthropic_api_key, claude, api key, x-api-key betterleaks has converged on sk-ant-, a versioned kind, ninety-three characters and a trailing AA; Anthropic publishes no format.
AWS_ACCESS_KEY_ID AwsAccessKeyRecognizer 1 0.90 1.00 68 aws, aws_access_key_id, access key, secret access key, credentials, amazon The published prefixes for access key ids (AKIA long-lived, ASIA temporary) with base32’s sixteen-character tail; principal ids (AIDA, AROA, AIPA) are excluded because they are not secret.
AWS_BEDROCK_API_KEY AwsBedrockKeyRecognizer 2 0.90 1.00 1082 bedrock, aws, aws_bearer_token_bedrock, api key, amazon, model Bedrock’s bearer-token API keys, long-lived (ABSK) and short-lived; the short-lived pattern matches the published marker only, because the base64 that follows it has no documented length.
CEREBRAS_API_KEY CerebrasKeyRecognizer 1 0.90 1.00 196 cerebras, cerebras_api_key, api key, authorization, bearer csk- and forty-eight characters; Cerebras publishes no format.
GITHUB_TOKEN GithubTokenRecognizer 2 0.90 1.00 339 github, gh, personal access token, pat, token, git GitHub’s published format. Four-character prefix, thirty of entropy, six of base62 CRC32 checksum; the checksum is not declared because GitHub does not publish what it is computed over, and a wrong one would drop every real token.
GITLAB_TOKEN GitlabTokenRecognizer 1 0.90 1.00 86 gitlab, glpat, personal access token, ci_job_token, token glpat- and twenty characters, the shape betterleaks carries.
GOOGLE_API_KEY GoogleApiKeyRecognizer 2 0.90 1.00 191 google, gcp, api key, googleapis, firebase, gemini, ai studio The long-published AIza shape and AI Studio’s AQ.Ab8RN6 shape, filed as one entity because they are one vendor’s API key in two formats.
GROQ_API_KEY GroqKeyRecognizer 1 0.90 1.00 212 groq, groq_api_key, api key, authorization, bearer gsk_ and fifty-two characters; Groq publishes no format, so the shape is upstream’s, with the case fold dropped from the prefix.
HUGGINGFACE_TOKEN HuggingFaceTokenRecognizer 1 0.90 1.00 139 huggingface, hugging face, hf_token, token, transformers hf_ and thirty-four characters, the shape betterleaks carries; HuggingFace publishes no format.
NPM_TOKEN NpmTokenRecognizer 1 0.90 1.00 148 npm, npmrc, authtoken, registry, node npm_ and thirty-six characters, the shape betterleaks carries.
OPENAI_API_KEY OpenAiKeyRecognizer 1 0.90 1.00 654 openai, openai_api_key, api key, authorization, bearer Matched on the T3BlbkFJ infix, “OpenAI” in base64, which every key carries, so the row does not depend on a length OpenAI has changed twice.
OPENROUTER_API_KEY OpenRouterKeyRecognizer 1 0.90 1.00 268 openrouter, openrouter_api_key, api key, authorization, bearer The versioned sk-or-v1- prefix and sixty-four hex characters; a future v2 prefix will miss rather than mismatch.
PERPLEXITY_API_KEY PerplexityKeyRecognizer 1 0.90 1.00 197 perplexity, pplx, perplexity_api_key, api key, authorization pplx- and forty-eight characters; Perplexity publishes no format.
PISTRA_KEY PistraKeyRecognizer 1 0.90 1.00 yes 203 pistra, virtual key, api key, authorization, bearer pistra’s own virtual key format. The prefix, a 43-character base62 body and six characters of CRC-32 over that body, checked here; it is the one row whose algorithm is ours, so the check is proven rather than inferred and a placeholder is refused.
PRIVATE_KEY PrivateKeyBlockRecognizer 1 0.95 1.00 160 private key, pem, ssh, rsa, openssh, pgp, certificate The opening armour line only. Matching to the closing line would make the streaming horizon the length of the key block, and the header already identifies the material.
SLACK_TOKEN SlackTokenRecognizer 1 0.90 1.00 293 slack, slack_token, bot token, webhook, xoxb The xox prefix and its kind letter, then the dash-separated body; the shape betterleaks carries, loosened to one pattern across the kinds.
STRIPE_SECRET_KEY StripeKeyRecognizer 1 0.90 1.00 404 stripe, stripe_secret_key, secret key, restricted key, payment Secret (sk_) and restricted (rk_) keys only; the publishable pk_ key is public by design and is deliberately not matched.
TOGETHER_API_KEY TogetherKeyRecognizer 1 0.90 1.00 182 together, togetherai, together_api_key, api key, authorization The versioned tgp_v1_ prefix and forty-three characters; Together publishes no format.
XAI_API_KEY XaiKeyRecognizer 1 0.90 1.00 484 xai, grok, xai_api_key, api key, authorization xai- and a body between seventy and a hundred and twenty characters, the range upstream converged on; xAI publishes no format.

Every row scores between 0.90 and 0.95 before anything corroborates it. It is high because a literal prefix is strong evidence, and uniform because the prefix is doing the work in every case rather than a shape that some rows have and others do not.

Every row also carries context words, and the enhancer’s increment of 0.35 takes all of them to the ceiling, so any of these near a match makes it 1.00. The practical consequence is that a score threshold is not a useful lever on credentials. There is no value that separates these rows from each other, and any value above 0.90 turns the whole namespace off. Select on a.category == "credential" and, where you need to be narrower, on a.entity_type or the matched text. See Stop a placeholder denying a request.

A score cannot tell you whether the key is real. A placeholder wearing a known prefix scores what a live key scores. The answer to that is an exemption you write and not a number you tune. The one exception is the row with a checksum, which refuses a wrong body outright. See the PII page’s score section for how scores work across both halves of this detector.

Each of these is a decision rather than a gap, and each is argued in guardrails/pii/native/credentials.yaml:

  • High-entropy strings. “A long random-looking base64 run” is not a pattern: no prefilter absorbs it, its streaming horizon is unbounded, and on the traffic an AI gateway carries it fires on hashes, ids, minified code and base64 images. This is the detector people learn to switch off, and one that is off is worse than one that was never claimed.
  • Assignment shapes, password = "…" and api_key: …. The same failure at a lower score, and the thing being assigned is usually a placeholder. Their precision lives in scaffolding that exists in a configuration file and not in prose.
  • JWTs. Common, usually not secret, and in an AI gateway they arrive in the request’s own authorization header on the way past.
  • AWS principal ids (AIDA, AROA, AIPA) and secret access keys. The first are published in policy documents on purpose; the second are forty characters with no prefix, which is the high-entropy case wearing a name.
  • Providers with no prefix to carry them. DeepSeek, Mistral, Cohere and Voyage are all routed by this gateway and all absent, because every published pattern for them is the assignment shape above.

Detectors read request bodies by default. apply_to: [header, query] is opt-in, and turning it on with these rows enabled means every request annotates the credential it just authenticated with. That is correct, and it is not what most people mean by “find credentials in my traffic”. Scope the rule rather than giving up the namespace: envelope annotations carry an addressable ref, so

annotations.exists(a, a.category == "credential" &&
a.ref != ".headers.authorization")

keeps a credential in a query string, which every proxy on the path logs and which nothing else can see, while ignoring the one the request presented at the door.

A vendor’s published format where there is one, and otherwise the shape betterleaks (MIT) has converged on across its corpus. The Format column says which for each row. Rows read from upstream record the rule they came from and quote the pattern it had at a pinned revision, so a vendor rotating a format or upstream narrowing a class after a false positive fails a test here rather than going unnoticed. make leak-rules reports what upstream has that this list does not.

Regenerate with make gen-docs; re-extract from upstream with make gen-recognizers.