Add OpenAI project key validity check
What does this MR do and why?
This adds a validity check for OpenAI project keys (starting with prefix sk-proj-).
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. OpenaiClient.
The verifier calls GET https://api.openai.com/v1/models (reference) with the token as a Bearer credential.
We map the responses to the following statuses/outcomes:
| Response | Outcome |
|---|---|
200 |
active |
401 with invalid_api_key |
inactive |
any other 401 |
unknown |
429 |
RateLimitError |
500, 502, 503, 504 |
NetworkError |
| anything else | unknown |
Reporting a live token as dead is worse than reporting nothing, so unmapped statuses are always unknown.
Why the status code is not enough
OpenAI answers 401 for four documented reasons, and only one of them means the key is no good. From
its error code reference:
Documented 401 |
What it indicates |
|---|---|
Incorrect API key provided |
The key is not valid |
Invalid Authentication |
"Ensure the correct API key and requesting organization are being used" |
You must be a member of an organization to use the API |
The account belongs to no organization |
IP not authorized |
The request IP is outside the organization's allowlist |
The last three describe a live key that this particular request cannot use, so treating a bare
401 as revoked would report a live credential as dead. The verifier reads the error body instead and
resolves inactive only for the documented invalid-key code:
{ "error": { "message": "Incorrect API key provided: sk-proj-*******key...",
"type": "invalid_request_error", "code": "invalid_api_key" } }A revoked key returns that same code, confirmed against the live API with a real key before and after
revoking it. Every other 401, including one carrying no code at all, resolves to unknown: the
official Python client models code as an optional free-form string with no enumeration, so absent or
unfamiliar codes are expected rather than exceptional.
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. OpenAI has no figure for this endpoint, so this is a cautious cap of our own.
- Unmapped statuses are covered by shared example
a partner token client, which is added 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/openai_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('OpenAiProjectKey') # => #<Security::SecretDetection::PartnerTokens::OpenaiClient> Gitlab::ApplicationRateLimiter.period_for(:partner_openai_api) # => 60 seconds -
Optional, with a real credential — a revoked token should come back
inactiveand a live oneactive:Security::SecretDetection::PartnerTokens::Registry .client_for('OpenAiProjectKey').verify_token(ENV['TOKEN']).status
MR acceptance checklist
I have evaluated this MR against the MR acceptance checklist.