Add Anthropic API key validity check
What does this MR do and why?
This adds a validity check for Anthropic API keys (starting with prefix sk-ant-).
A user can then tell whether the exposed credential is still live rather than only that it was leaked.
Implementation
This follows ADR-006 and uses the registry directly to map the token straight to the verifier class/client, i.e. AnthropicClient.
The verifier calls GET https://api.anthropic.com/v1/models (reference), sending the token in the x-api-key header alongside anthropic-version. The endpoint returns read-only model metadata: it spends no tokens and changes nothing, and a 200 is positive proof that the key authenticated.
We map the responses to the following statuses/outcomes:
| Response | Outcome |
|---|---|
200 |
active |
401 |
inactive |
429 |
RateLimitError |
500, 502, 503, 504, 529 |
NetworkError |
| anything else | unknown |
Please note that aside from 401, no other status falls to inactive.
Reporting a live token as dead is worse than reporting nothing, so unmapped statuses are always unknown.
Why 403 is not inactive
Per Anthropic's documented error taxonomy, a 403
is permission_error: "Your API key does not have permission to use the specified resource." An
unrecognised key returns 401 instead, so a 403 means the key authenticated and simply lacks
access. Mapping 403 to inactive would tell a user a working credential is dead and does not
need rotating, so it falls through to unknown. The same reasoning covers 402 billing_error
– a valid key on an account with a payment problem. It is unmapped, so it also resolves to unknown.
Why not use the messages endpoint
An earlier version of this check sent an empty body to POST /v1/messages and read the resulting
400 as proof of life: the request shape was rejected, so authentication must have passed. That
worked out a live key from a rejection, and it relied on Anthropic checking credentials before the
request body, which the API does not document. If that order ever changed, a revoked key would answer
400 too and we would report it as active. GET /v1/models removes the guesswork, and 400 now
falls through to unknown like any other unmapped status.
Checked against the live API with a real key: 200 while active, then after revoking it, 401 with
{"type": "authentication_error", "message": "API key is invalid."}. Anthropic puts the signal in
type, so no body parsing is needed here, unlike OpenAI's code: invalid_api_key.
Stack Position
We have 8 MRs for all new validity checks generated during Sec Challenge #6, with the base targeting master.
Each MR in the stack adds exactly one validity check: the client class, its spec, the registry entry, and the rate limit rule.
Stack (in review order)
| # | Check | Token type |
|---|---|---|
| 1 | GitHub PAT | Github Personal Access Token |
| 2 | OpenAI project key | OpenAiProjectKey |
| 3 | Anthropic API key | anthropic_key |
| 4 | Slack access tokens | Slack token |
| 5 | Stripe live secret key | StripeLiveSecretKey |
| 6 | Datadog API key | DataDogAPIKey |
| 7 | SendGrid API token | Sendgrid API token |
| 8 | Heroku API key | Heroku API Key |
Notes
- Token type and pattern were verified against the authoritative rule in
secret-detection-rules. - Rate limit: 50 checks per minute per project. Anthropic's lowest published tier is 1,000 requests per minute, so this stays well below it.
- Unmapped statuses are covered by the shared example
a partner token client, which this stack adds once and every verifier reuses. - Verifier code originates from the Sec Challenge #6 PoC branch
ghavenga-summit-ch6-validity-checks.
How to set up and validate locally
-
Run the specs:
bundle exec rspec ee/spec/lib/security/secret_detection/partner_tokens/anthropic_client_spec.rb \ ee/spec/services/security/secret_detection/partner_tokens/registry_spec.rb -
Confirm the registry resolves the token type and that the rate limit rule exists:
Security::SecretDetection::PartnerTokens::Registry.client_for('anthropic_key') # => #<Security::SecretDetection::PartnerTokens::AnthropicClient> Gitlab::ApplicationRateLimiter.period_for(:partner_anthropic_api) # => 60 seconds -
Optional, with a real credential – a revoked token should come back
inactiveand a live oneactive:Security::SecretDetection::PartnerTokens::Registry .client_for('anthropic_key').verify_token(ENV['TOKEN']).status
MR acceptance checklist
I have evaluated this MR against the MR acceptance checklist.