WRNexus
Back to the blog
security 6 min read

API keys: rotation, scoping, and the things people forget

Five concrete patterns we ship in WRNexus to make API keys boring to operate — including the rotation flow that prevents downtime.

Aditi Kapoor

#security · #api · #engineering

API keys are the unsexiest part of an identity product and the easiest part to ship badly. Most teams get the “generate a key and show it once” part right; the parts that go wrong are scoping, rotation, and revocation.

Here are the five patterns we ship in WRNexus.

1. Show the key exactly once, hash it at rest

When a user creates an API key, the response contains the plaintext key and only the plaintext key. After that response, the database stores a SHA-256 hash:

plaintext = "wrn_live_" + secrets.token_urlsafe(32)
hashed = hashlib.sha256(plaintext.encode()).hexdigest()
db.execute(
    "INSERT INTO api_key (workspace_id, name, hash, prefix) "
    "VALUES (%s, %s, %s, %s)",
    workspace_id, name, hashed, plaintext[:12]
)
return {"key": plaintext, "prefix": plaintext[:12]}

The prefix (the first 12 characters) is kept in plaintext so users can recognise their own keys in the admin UI — “ah, that’s the production deploy bot.” The rest of the key never appears anywhere after creation.

If a key leaks into a git history, we can revoke it without ever needing to know the plaintext. If our database leaks, no plaintext keys leak with it.

2. Scope every key

A key with full account access is a footgun. Every WRNexus key carries a scopes field — a comma-separated list of permissions like workspace:read, audit:read, billing:write. The admin UI prompts the creator to pick scopes, and we record the chosen set at create time so an audit later shows exactly what the key could do.

The default scope for a new key is the most restrictive set that still lets it do its job, not “everything.” This is a small UX choice that pays off for years.

3. Rotation without downtime

The single biggest API-key UX failure is the team that knows they should rotate their key but can’t, because it’s hardcoded in fifteen places and they’d take downtime swapping it. We ship the dual-key flow:

  1. Click Rotate on a key.
  2. We generate a new key, leaving the old one valid for 24 hours.
  3. You deploy the new key everywhere.
  4. After 24 hours (or sooner, if you click Revoke old), the old key stops working.

The key’s id and name stay the same — only the secret changes — so audit-log queries that filter on the key still match across the rotation. This is how every API key in production at WRNexus has been rotated at least once without a single customer-visible blip.

4. Expirations, not “valid forever”

Every key has an expiry. Defaults: 90 days for keys created from a human’s session, 1 year for keys created from the API itself (typically by CI). We email the workspace 14 days before expiry, 1 day before, and at the time of expiry.

Yes, this annoys some users. It also catches “an intern’s laptop walked out the door with the production deploy key” before it walks back in. The trade is a clear win.

5. Revocation that’s actually instant

Revoking a key should be one click, and the next request from that key should fail immediately — not “after the cache TTL.” We store the revocation state in Redis with a KEY_REVOKED set, and every auth check consults it:

async def authenticate(token: str) -> Auth | None:
    digest = sha256(token.encode()).hexdigest()
    if await redis.sismember("api_key:revoked", digest):
        return None
    # ... rest of the auth path ...

The Redis check is sub-millisecond and the set rarely has more than a few thousand entries (we expire old revocations after 30 days since the underlying key is gone from the database too). It is the last line of defence between “leaked key” and “leaked key being used,” and it has to be measurable, not theoretical.

A sixth, bonus pattern: leak detection

We scan every push event from connected GitHub repos for the wrn_live_ prefix. If we find one, we revoke the key automatically, notify the workspace, and log an incident with the commit URL. This has caught real leaks four times in the last year, all from well-meaning engineers who pasted a key into a snippet for debugging and forgot to remove it.

If you ship an API key product and you’re not doing this, start today. The implementation is two endpoints and a webhook.

Putting it together

API key hygiene is one of those areas where the difference between a team that’s been audited and a team that hasn’t is plainly visible. The patterns above are not hard — they’re just often skipped because they don’t move a product metric. The next breach you don’t have will absolutely have moved one.