API Key Security Runbook
Operating API keys safely in production — rotation, revocation, incident response, and release checks.
API Key Security Runbook
Operational procedure for API keys. For the user-facing guide to creating and using keys, see API Keys.
What the system does for you
- Stores only a hash. Keys are hashed with HMAC-SHA256 using
API_KEY_HASH_SECRET. The key value itself is never written to the database and cannot be recovered from it. - Keeps a display prefix and suffix, so a key is identifiable in the UI without being reconstructable.
- Caches auth lookups in Redis for
API_KEY_AUTH_CACHE_TTL_SECONDS(default 300). - Drops the cache entry on revocation, so a revoked key stops working immediately rather than at the end of its TTL.
- Invalidates keys when a subscription lapses. The billing webhook busts the auth cache the moment it processes a cancellation.
- Throttles
lastUsedwrites toAPI_KEY_LAST_USED_TOUCH_INTERVAL_SECONDS(default 300) instead of writing on every request, so the timestamp is deliberately approximate. - Rate-limits per key, in addition to the general per-route IP limiter.
Normal operating rules
- Create each key with the smallest scope set that makes its integration work. There is no
roadmap:writeorgithub:write— those surfaces are read-only by design. - Set a realistic expiry. The 365-day default is generous; shorten it for anything sensitive.
- Lower the per-key rate limit for integrations that do not need heavy traffic. A key that should make ten calls an hour has no business being allowed twenty per fifteen minutes.
- Rotate on a schedule and whenever someone with access to the key leaves.
- Never log the full key value — not in application logs, not in error reports, not in support tickets.
Safe rotation
Rotation issues a replacement and retires the old key, so you can cut over without a service gap.
- Rotate the key and copy the new value.
- Deploy the new value to the consuming application.
- Confirm the application works against the new key.
- Revoke the old key.
Use rotation when the integration continues. Use revocation when the key should cease to exist.
If a key is exposed
- Revoke it immediately. Do not wait for a rotation window.
- Check the key's
lastUsedtimestamp and review API logs for unfamiliar source IPs and request patterns. - Rotate credentials in any integration that shared the same secret store.
- If the key carried
ai:write, check the account's credit transaction history for unexplained debits — that scope spends real credits. - If the database itself was exposed, rotate
API_KEY_HASH_SECRETas well. Doing so invalidates every existing key, so plan the reissue before you make the change.
Incident checklist
When key authentication behaves unexpectedly:
- Did the key expire? Check
expiresAt. Keys created without an explicit expiry default to 365 days. - Was the key revoked? Check
revokedAtandisActive. - Did the owning subscription lapse? Keys are invalidated when the account's subscription moves to cancelled or inactive. This is expected behaviour, not a bug.
- Is the scope actually granted? A
403from a scoped route means the key is valid but lacks the required scope. - Is Redis healthy? Auth caching and per-key rate limiting both depend on it.
- Are rate limits being hit? A
429may come from the per-key limit or the general per-route IP limiter.
Release checklist
Before a production release, confirm:
API_KEY_HASH_SECRETis set and differs fromAUTH_SECRET. The boot validator enforces this in production, so a mismatch fails the deploy rather than shipping silently.API_KEY_AUTH_CACHE_TTL_SECONDSandAPI_KEY_LAST_USED_TOUCH_INTERVAL_SECONDSare set to values you have deliberately chosen.API_KEY_DEFAULT_RATE_LIMITmatches product expectations.API_KEY_DEFAULT_SCOPESis still the minimaluser:read.ALLOWED_ORIGINSlists only real frontend domains.
Rule of thumb
If a problem can be solved by giving a key more access, that is usually the wrong fix. Start from less access, a shorter lifetime, and faster revocation, and widen only when something concretely requires it.