Securityapi keyscredentialsrotationidentificationsecrets

API Keys: Identity for Applications

An API key identifies an application — which makes it the natural unit for scoping, rate limiting and metering, and the wrong tool the moment a user is delegating access. Keys are credentials: prefixed, hashed at rest, scoped, and rotatable without downtime.

Follow the failure

Frame the contract

API design starts with a consumer, a design question and a guarantee — never with a URL.

Design question
What does an API key actually identify, what lifecycle must the contract support for it, and when is a key the wrong credential entirely?
Consumers
Partner backends and scripts calling with a key from a config store; platform teams metering and rate-limiting per key; and the security engineer asking, after a leak, "what could this key do and when was it last rotated?"
The promise
Keys that are recognizable (prefixed), safe at rest (hashed server-side, shown once), bounded (scoped, optionally IP/referrer-restricted), attributable (per-key metering and limits), and replaceable (rotation with overlap, revocation that takes effect in seconds).
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

What a key identifies, and what that buys

An API key answers "which *application* is calling" — not which human. That single fact drives everything keys are good at: the key is the natural unit for Scopes: Least Privilege as Contract Surface, for The Rate-Limit Contract buckets, for Quotas vs Rate Limits metering, for billing attribution, and for the kill switch when one integration misbehaves. It is also everything keys are bad at: a key carries no user, no consent, and no way for the person whose data is accessed to revoke just their part.

Design the key itself for operations, not just entropy. A structured format — sk_live_51NxK… — encodes what raw randomness cannot: the sk says secret key (vs a publishable client-side key), live says production (so a test key hitting prod is rejectable with a *specific* error), and a recognizable prefix lets secret scanners find leaked keys in public repos — the mechanism behind providers auto-revoking keys within minutes of a public commit. Store only a hash server-side and show the full key once at creation: keys are passwords for machines, and the Password Storage reasoning applies unchanged — a database dump must not be a credential dump. Display …4242 (last four) thereafter so a human can tell two keys apart without either being exposed.

Anatomy of an operable key
sk_live_51NxKq2eZvKYlo2C9HGbXWpr4
│  │    └── 24+ chars of CSPRNG entropy (~128 bits)
│  └── environment: live | test  → wrong-env calls get a
│                                   specific, debuggable error
└── type: sk secret (server only) | pk publishable (client-safe)

server stores:  SHA-256(key), prefix, last4, scopes,
                created_at, last_used_at, expires_at
shown to user:  full key ONCE at creation · last4 afterwards
scanners match: the sk_live_ prefix — leaked keys in public
                repos are found and revoked in minutes

Lifecycle is contract: create, rotate, revoke — without downtime

The most common key failure is not theft — it is *immortality*. One key, created in 2021, in a config file, with full access: nobody rotates it because rotation means downtime, and rotation means downtime because the contract allows only one active key per account. The fix is a contract clause, not a policy memo: allow multiple concurrent keys, so rotation is create-new → deploy-new → verify-via-telemetry → revoke-old, with zero gap. Without overlap, "rotate quarterly" is a rule everyone endorses and no one follows.

Revocation is the other half: when a key leaks, seconds matter, so revocation must propagate fast — which constrains implementation (a key-validity cache with a 24-hour TTL means a 24-hour breach window after you pressed the button; validity checks need short TTLs or active invalidation). Expiry policy is a genuine trade-off, not a checkbox: expiring keys force rotation hygiene but guarantee that integrations die on a schedule — usually at 2 a.m., in the hands of whoever inherited the integration. Long-lived keys plus last_used_at telemetry, unused-key alerts, and a low-friction rotation flow is an honest alternative; pick one deliberately and document it.

One eternal key, passed in the query string
1GET /v1/orders?api_key=9f3a1c
2
3# one key per account → rotation = downtime → never rotated
4# query string → key lands in access logs, CDN logs,
5# browser history, referrer headers, analytics
6# stored in plaintext server-side "so support can read it"
7# no last_used_at → revoking is a leap of faith
Concurrent keys, header transport, hashed at rest
1GET /v1/orders
2Authorization: Bearer sk_live_51NxKq2
3
4key management API:
5 POST /keysnew key (full value, once)
6 GET /keysprefix+last4, scopes,
7 last_used_at per key
8 DELETE /keys/{id} → revoked in seconds
9
10rotation: create Bdeploy Bwatch A's last_used_at
11 go stalerevoke A (zero downtime)

Every clause in the good version exists to make the safe behavior the easy one: overlap makes rotation routine, header transport keeps logs clean, hashes make the database undumpable, and last_used_at turns "is anything still using the old key?" from a guess into a query.

When a key is the wrong credential

The boundary is delegation. The moment the resource owner is a *user* granting *someone else's app* access to their data, a key is the wrong shape: to give the app a key is to give it your whole identity — no consent screen showing what is being granted, no scope chosen by the user, no way to revoke this app without breaking every app, and no audit trail distinguishing the app's actions from yours. That job needs user-delegated authorization — OAuth 2.x — Delegated Authorization — where consent, scoping and per-app revocation are the protocol's whole point. The pre-OAuth web actually ran on the key-shaped version of this ("enter your password for your email account so we can import your contacts"), and its abandonment is the design lesson.

The other disqualifier is *placement*: a secret key can live only where secrets can live — server-side. A key shipped in a mobile app binary or browser bundle is public within hours of someone opening devtools or unzipping the APK; that is why publishable-key/secret-key splits exist, and why browser and mobile consumers authenticate users (sessions, token flows) rather than embedding application credentials. Keys also compose with, not replace, per-key restrictions: binding a key to source IP ranges or referrers narrows what a thief can do with it — defense-in-depth on top of scopes, in the same spirit as Least Privilege, never the primary control.

  • Key fits: server-to-server integrations, partner backends, CI jobs, internal scripts — the application is the principal and can keep a secret.
  • Key does not fit: user-delegated access (use OAuth 2.x — Delegated Authorization — consent, user-chosen scope, per-app revocation), browser or mobile code (cannot hold secrets), human login (that is Authentication in the Contract's session territory).
  • Always attach: scopes per key, rate-limit identity, last_used_at, optional IP/referrer binding.
  • Never do: plaintext at rest, query-string transport, one-key-per-account contracts, validity caches measured in hours.

Key points

  • A key identifies an application, making it the unit of scoping, rate limiting, metering and kill-switching — and useless for representing a user's consent.
  • Structure the key: type + environment prefix and last4 make keys operable, scannable in leaked repos, and debuggable on wrong-environment calls.
  • Store only hashes, show the full key once — keys are machine passwords and the password-storage rules apply unchanged.
  • Multiple concurrent keys are what make rotation possible without downtime; one-key contracts guarantee immortal credentials.
  • Revocation speed is a contract property bounded by your validity-cache TTL — decide the number, then document it.
  • User-delegated access is OAuth's job, not a key's: consent, user-chosen scopes and per-app revocation cannot be retrofitted onto an application credential.

Follow the failure

How the contract fails or gets misused, hop by hop — and what it costs when it completes.

  1. 1
    Provider → contract: one key per account, full access, passed as ?api_key=, stored plaintext "for support".
  2. 2
    Consumer → config: the key goes into a repo; a fork later goes public with the key in config.yml history.
  3. 3
    Scanner or attacker → key: the unprefixed key is indistinguishable from any hex string — no scanner flags it; an attacker's does not need to.
  4. 4
    Attacker → API: full-access reads at modest rates for weeks; per-key metering does not exist, so nothing looks unusual.
  5. 5
    Provider → remediation: revoking means breaking the customer's production; rotation was never designed in, so the breach window stretches while a maintenance window is negotiated.
What breaks
  • Leaked keys grant full, silent, long-lived access — the standing-credential breach that per-key scopes, prefixes and telemetry exist to bound.
  • Rotation-hostile contracts turn "revoke the leaked key" into "schedule customer downtime", stretching breach windows from minutes to weeks.
  • Keys misused for user delegation concentrate every user's authority into credentials scattered across third-party databases, unrevocable per-app.

Design, observe, evolve

A contract decision is incomplete until you know how you would notice it failing and how it changes later.

Design the contract
  • • Ship key management as API surface: create, list (prefix+last4+last_used_at), revoke — with multiple concurrent keys so rotation is a deploy, not an outage.
  • • Prefix keys by type and environment, hash at rest, display once; register the prefix with secret-scanning ecosystems.
  • • Attach scopes, rate-limit identity and metering to every key at creation; default new keys to the narrowest scope the flow allows.
  • • Transport in the Authorization header only; bound revocation latency explicitly and test it like any other SLO.
Observe in production
  • • `last_used_at` and per-key usage curves: an unused key is revocable risk; a key's sudden geography or volume shift is the leak signal.
  • • Age of active keys as a fleet metric — a dashboard where the p50 key age is three years is a finding, before any breach.
  • • Wrong-environment and revoked-key call rates: both indicate consumer confusion or automation still holding dead credentials.
Evolve without breaking
  • • Introducing prefixes, scopes or expiry to an existing key population is a migration: new keys get the new shape, old keys work under a deprecation clock with per-key usage telemetry driving outreach.
  • • Tightening defaults (narrower initial scopes, shorter validity caches) applies cleanly to new keys; applying it to live keys is a breaking change to running integrations and needs the full notice machinery.
What it costs
  • • Real key management — hashing, overlap, scoping, telemetry — is a product surface with UI, docs and support cost; the naive one-key table is a fraction of the work and all of the breach.
  • • Hash-at-rest means support can never read a customer's key back — the correct property, and a recurring support friction you should plan scripts around.
  • • Expiring keys enforce hygiene at the cost of scheduled integration outages; non-expiring keys with telemetry trade guaranteed pain for monitored risk — neither is free.

Misconceptions

Claim
“API keys are a lightweight alternative to "real" authentication.”
Reality
A key is real authentication — of an application. It is a bearer credential with the full obligations of one: hashed storage, rotation, revocation, scoping. "Lightweight" thinking is how keys end up plaintext in a database and immortal in a config file.
Claim
“We can just have users paste their API key into third-party tools.”
Reality
That hands the tool the user's entire authority with no consent granularity, no per-tool revocation, and no audit separation. The moment access is user-delegated, the job is OAuth 2.x — Delegated Authorization's — the entire protocol exists because key-pasting was the status quo and it was a disaster.
Claim
“Restricting a key to our partner's IPs makes leaks harmless.”
Reality
IP binding narrows what a thief can do from *elsewhere* — it does nothing against an attacker inside the partner's infrastructure, which is where integration keys actually leak from. It is worthwhile defense-in-depth on top of scopes and rotation, never a substitute.