API Keys and Service-to-Service Auth
The scanner app at the door is not a person. It does not have a password, it does not sign in with Google, and it does not consent to anything; it holds a key that names the organizer it works for, and it presents that key on every GET /tickets/{code}, 2,000 times in the 40 minutes before a show. An API key is the simplest credential there is, a long random string, and its simplicity is the trap. Stagedoor's scanner shipped with one key, the same string on every phone at every venue in the country, stored in plaintext in api_keys, sent as a query parameter, and never rotated in two years.
This topic is keys done properly: one per client, scoped, hashed at rest, shown once, carried in a header, rotated with an overlap. It then covers the two stronger forms for a machine talking to a machine that Stagedoor also controls, where a static secret that never expires is a liability nobody has to accept: short-lived tokens issued from a key or from the platform's own identity, and mutual TLS, where the connection is the credential and no header carries anything.
What a Key Is
256 bits from the operating system's random source, base64url-encoded to 43 characters, with a prefix in front: sd_live_ for production, sd_test_ for the sandbox. The prefix costs 8 characters and buys two things. A human reading a log or a config file knows what the string is. And a repository scanner knows too: the secret scanner of Chapter 11 matches on the prefix and stops a key from reaching a commit, which is the most common way keys leak, and the hosted scanning services key on well-known prefixes the same way, which is why Stripe's keys have one. The key is shown exactly once, on the response to the request that created it, and the organizer's dashboard says so in red. After that the service can verify it and cannot display it, because it does not have it.
CREATE TABLE api_keys ( id bigint PRIMARY KEY, key_hash text NOT NULL UNIQUE, -- sha256 of the key; the key itself is never stored key_prefix text NOT NULL, -- 'sd_live_…a3f1': first 8 and last 4, for humans and logs organizer_id bigint NOT NULL REFERENCES organizers(id), scopes text[] NOT NULL, -- {'tickets:read'} for a scanner created_at timestamptz NOT NULL, last_used_at timestamptz, revoked_at timestamptz -- set, never deleted: the audit trail outlives the key ); -- verification is one indexed lookup; $1 is the hex SHA-256 the auth ring computed from the header SELECT organizer_id, scopes FROM api_keys WHERE key_hash = $1 AND revoked_at IS NULL;
The row stores the SHA-256 of the key, not the key. That is the same hash Topic 24 forbade for passwords, and it is right here for the reason it was wrong there: a password is a short string a human chose from a small space, and the attacker's job is to guess it, so the hash must be slow. A key is 256 random bits, and no attacker guesses 256 random bits at any speed; the hash only has to be one-way, and SHA-256 at 200 nanoseconds is one-way. The lookup is a single indexed read, which matters at 2,000 scans in 40 minutes. The prefix and suffix are kept separately so that a log line or the dashboard can say which key without saying the key. And a revoked key keeps its row, with revoked_at set, so the audit log's "key sd_live_…a3f1 read ticket 4411" still resolves to a real row a year later.
One Key per Client, Scoped
A key belongs to one organizer and carries a list of scopes. The scanner's key has tickets:read; it can look up a ticket by its code and mark it scanned, and it cannot list orders, see buyers' emails, or issue refunds, because the route dependency of Chapter 4 checks the key's scopes before the handler runs. The organizer's box-office integration gets a second key with orders:read and events:write. Neither key can be used for the other's job, and a phone lost at one venue revokes one key, at one venue, tonight, with the box office unaffected.
The shared key of the old design meant the opposite of each of those sentences. One phone lost in one city, and the honest response was to revoke the key, which would have stopped every door in the country until every phone was updated, which took a week the last time. So the key was not revoked, and the lost phone could read any ticket for any event for as long as its battery lasted. A credential that cannot be revoked without an outage is a credential that will not be revoked, which is why "one key per client" is the design rule and not a nicety.
Presentation and Rotation
The key travels in a header, Authorization: ApiKey sd_live_..., the scheme Chapter 2 chose for it, and never in the query string. A URL is logged by every proxy, load balancer and CDN between the scanner and the service, stored in browser history and sent in referrer headers; a key in the URL is a key in a dozen logs the security team has never seen. The header is logged by nothing on that path, and the access-log ring of Chapter 4 redacts it before writing.
Rotation is create, deploy, revoke, in that order, with an overlap. The organizer creates a second key, updates the phones and the box-office integration to use it, and then revokes the first. The overlap can be an hour or a month; what it cannot be is zero, because a rotation that revokes the old key before the new one is deployed is an outage by design. The last_used_at column, updated on each use to the minute rather than on every request, is what tells the organizer whether the old key is still in use before she revokes it: a key last seen 20 days ago is dead, a key last seen 4 minutes ago is on a phone at a door somewhere, and the dashboard shows both.
Short-Lived Tokens for Services
When the caller is Stagedoor's own worker-01 calling the API's internal endpoints, or the pricing service Stagedoor may one day run beside it, a key that never expires is a liability with no compensating benefit: Stagedoor controls both ends and can make them do anything. The OAuth client-credentials flow is the standard form: the worker holds a client id and secret, posts them to the token endpoint, and receives a 15-minute access token, a JWT of Topic 26 with the worker's identity in sub, which it presents on every call and renews before it expires. The static secret is still there, but it is used once every 15 minutes against one endpoint, and the thing on every request is worthless 15 minutes after it was stolen.
Better still is to have no static secret at all. Every platform Stagedoor might run on can issue an identity to a workload: a cloud service account whose token the metadata server hands to the process, a Kubernetes service-account token projected into the container, short-lived and bound to an audience. The worker starts, asks the platform who it is, and receives a token that proves it. Nothing to store, nothing to rotate, nothing to leak from a config file, and the platform rotates the underlying keys on its own schedule. The handoff to that machinery is Chapter 11; the point here is that between two services you control, the right number of long-lived secrets is zero.
Mutual TLS
In ordinary TLS the server presents a certificate and the client checks it; in mutual TLS the client presents one too, and the server checks that. The connection itself is then the credential: the server knows which workload is on the other end before a single byte of HTTP arrives, and no header carries anything that could be logged or replayed. It is the strongest form of service-to-service identity, and it is the most operational, because somebody has to issue a certificate to every workload, rotate it before it expires, and revoke it when the workload is compromised. Stagedoor does not run that machinery itself. Inside a service mesh, the sidecar does it, with certificates that live an hour and rotate without the process knowing; SPIFFE is the standard for the identity in the certificate, and the mesh is the subject of the Middleware course.
Keys Are Not Users
The principal produced by the auth ring for a key is the organizer plus the key's scopes, not a human. The audit log line for a scan reads "key sd_live_…a3f1 (Riverside Hall) read ticket 4411," which names the key, its owner and the action, and there is no user id because there was no user. The rate limit of Chapter 14 is keyed on the key id, not the organizer and not the source address, because a venue's ten phones behind one Wi-Fi router share an address and must not share a limit, and two of the organizer's integrations must not compete for one budget. Everything that Topic 29 does with a buyer's principal, it does with a key's: the same Principal type, a different via, and the ownership check reads organizer_id from it the same way.
An API key is a static secret presented on every request: simple to issue, simple to use from any language or device, revocable by its hash, and long-lived by nature. Use it for third-party clients you cannot make do anything more, the scanner and the organizer's box-office software.
Client credentials exchange a static secret for a short-lived token: one more round trip every 15 minutes, and the thing on the wire expires in minutes. Use it, or better the platform's workload identity, between services you control, where the extra round trip is yours to pay and the long-lived secret is yours to lose.
- One key for every client — revoking it after one lost phone stops every door in the country, so it is never revoked, and the lost phone reads any ticket for any event.
- Keys stored in plaintext — the database leak is every organizer's door and every box office's order list, when a SHA-256 column would have made the stolen rows worthless.
- Keys in the URL — logged by every proxy, load balancer and CDN between the scanner and the service, kept in browser history, and sent in the referrer to whatever page loads next.
- No
last_used_at— the rotation revokes a key that a venue's phones still present tonight, and the door opens late while somebody finds the new one. - A long-lived key for your own worker — a secret that never rotates because nothing ever forced it to, in a config file that has been copied to three laptops.
- Rate limits keyed on the organizer — ten phones at one venue share one budget, the tenth phone is refused during the rush, and a second integration can starve the door.
- Generate keys from 256 random bits with a recognizable prefix, store only their SHA-256, keep the prefix and last 4 characters for display, and show the full key exactly once.
- Issue one key per client with an explicit scope list, revoke keys individually by setting
revoked_at, and rotate by creating the new key, deploying it, then revoking the old. - Carry the key in
Authorization: ApiKey, never in the query string, and redact the header in the access log. - Use client credentials or the platform's workload identity between your own services, so that no long-lived secret exists on the internal paths.
- Make the key's owner the principal, key the rate limit on the key id, and write audit lines that name the key's prefix and owner.
sk_live_ prefix, the model for the format aboveGitHub fine-grained tokens, scoped and expiring, with secret scanning that matches the prefixAWS IAM roles and GCP service accounts, workload identity with no stored secretSPIFFE/SPIRE and Istio the identity and the mesh behind mutual TLSKong, Tyk and the cloud API gateways, key management at the edgeKnowledge Check
Topic 24 forbade SHA-256 for passwords. Why is it the right hash for API keys in the same codebase?
- The key is 256 random bits, so nobody can guess it and the hash only needs to be one-way
- Keys are not secrets in the way passwords are, so a leaked hash table would not matter
- Keys are peppered before hashing, which makes even a fast hash safe against a stolen table
- The scanner makes 2,000 requests a night, and a slow hash would make each lookup too expensive
A phone with a scanner key is lost at one venue. Under the old design with a single shared key, what actually happened, and why?
- The key was revoked within minutes, and every other venue kept working on the same key
- The key was left in place, because revoking it would have stopped every door in the country
- The lost phone's copy was disabled by device id, and the key itself stayed valid elsewhere
- The key's scopes were narrowed so the lost phone could scan but no longer read ticket details
Stagedoor's worker calls the API's internal endpoints. Why does the book prefer client credentials or workload identity over a static key for that path?
- Because a static key cannot be revoked once issued, while a token can be
- Because verifying a signed token is faster than the indexed key lookup
- Because both ends are Stagedoor's, so a secret that never expires has no reason to exist
- Because the worker cannot send an Authorization header on its internal calls
What does mutual TLS replace, compared with an API key in a header?
- The authorization check, since the certificate already says what the caller may do
- The encryption of the header, since certificates protect the key better than TLS does
- The need to rotate anything, since a certificate is issued once and never changes
- The credential in the header, since the connection itself identifies the caller
You got correct