A partner station posts telemetry with a long-lived API key that never expires — what does your ingest service store for it?
answer
- possession is the whole authority
- the server recognises, it does not keep
- store a verifier, not the secret
- prefix and last four for display
- shown once, replaced not recovered
basics
~20 sStore a one-way digest of the key, never the key itself, alongside a non-secret prefix and last characters for display, the owning station, the key's permissions, and timestamps. The plaintext is shown once at creation and is not recoverable afterwards.
solid answer
~40 sA long-lived machine key is a bearer secret: whoever holds the string is the station, so the server needs to *recognise* it, not *keep* it. The row holds a one-way digest of the secret, a short non-secret key identifier used to find the row, a display stub (leading segment plus last few characters), the owning station, the permissions bound to this key, and `created_at` / `last_used_at` / `revoked_at`. The plaintext is generated, shown once, and never stored or retrievable — a lost key is replaced, not recovered. An unrecognised key gets `401` with `WWW-Authenticate`; a recognised key that lacks the permission for the call gets `403`.
code
json · 12 lines{
"key_id": "7f3a91c4",
"display": "seis_live_7f3a91c4...4KQ2",
"verifier": "sha256:9d1f0c2b7a...e41",
"station": "GSN-0142",
"label": "field logger, vault A",
"scope": ["readings:write:GSN-0142"],
"quota_per_minute": 600,
"created_at": "2026-03-04T08:12:00Z",
"last_used_at": "2026-09-19T02:41:07Z",
"revoked_at": null
}go deeper
Recall the one sentence everything follows from: possession of the string is the authority. So the server stores a one-way digest plus non-secret details, shows the key once, and can never give it back.
Explain the split between the non-secret identifier used to find the row and the secret part that is digested and compared, and why that split is what keeps verification a single indexed read.
Show that you have operated this: the display stub, the label and last-used timestamp are what make a one-time reveal survivable in a real support queue, and the refusal codes are a contract you are consistent about.
Frame it as custody. The organisation is choosing not to hold four hundred partner credentials, accepting the support cost of unrecoverable keys in exchange for a table that is worthless when it leaks.
## The credential this leaf is about Four hundred seismograph stations post readings to a telemetry ingest service. Some are national observatories; some are a university department with one part-time technician. None of them has a human at a browser at 03:00 when a tremor arrives, so none of the interactive machinery — a login form, a second factor, a short-lived credential a user refreshes — applies. Each station holds one long string, sends it on every request, and that string is the whole authority. **Possession is authority**: there is nothing else to check. That single sentence drives every decision below. Because the credential is a bearer secret with no expiry and no second proof, the two questions that matter are *what does the server keep* and *how fast can you take it away*. ## What the key row holds | Column | Example | Why it exists | |---|---|---| | key identifier | `7f3a91c4` | A **non-secret** handle. Finds the row in one indexed read, and is safe to put in logs, dashboards and support tickets. | | verifier | a digest of the secret | Lets the server recognise a presented key without being able to produce one. | | display stub | `seis_live_7f3a91…4KQ2` | So an operator can tell two keys apart in a console without the console ever holding a secret. | | owner | station `GSN-0142` | Which partner this key speaks for; every accepted write is attributed to the key, not just the station. | | permissions | `readings:write` on its own series | The authority this key carries, which is narrower than the account behind it. | | timestamps | `created_at`, `last_used_at`, `revoked_at` | Age, evidence of use during a rotation, and the kill switch. | The plaintext appears exactly once, in the response that creates the key. After that the service cannot produce it, and neither can anyone who steals the table. ## Why the plaintext never stays - **A dump of the table would otherwise be four hundred working stations.** With digests it is four hundred useless rows; the attacker still has to find the originals. - **Support cannot be socially engineered into reading a key out**, because support genuinely cannot see it. 'I lost our key, can you read it back to me' has exactly one answer: issue a new one. - **Nobody inside the operator's own organisation can quietly copy one either.** An administrator can create, scope and revoke keys; they cannot exfiltrate an existing one. - **Reversible encryption is not a substitute.** If the ingest service can decrypt the column on demand, so can anything that gets the service's key material, and the property you wanted — that the server does not hold the credential — is gone. ## What 'shown once' costs, and what you give instead The operator has a real problem: three keys in a console and no way to tell which is on the field logger. You solve it with things that are not secret — a label the operator types at creation, the display stub, `created_at`, and above all `last_used_at`, which answers 'is this one still in use?' during a rotation. None of these lets anyone reconstruct the key, and together they make the one-time reveal tolerable. ## What the ingest endpoint does with a presented key 1. Read the credential from the `Authorization` header as a `Bearer` value. 2. Split it into the non-secret identifier and the secret part. 3. Look up the row by identifier — one indexed read, not a scan over every key. 4. Compute the digest of the presented secret and compare it against the stored verifier, using a constant-time comparison. 5. Reject if the row is revoked. 6. Check the key's permissions against the call being made. 7. Apply the key's own quota, and record `last_used_at` and the request against the key identifier. Steps 3 and 4 are why the identifier is stored separately from the verifier: without it you would have to hash the candidate against every row. ## What this model does not give you A bearer secret proves only that the caller has the string. It does not prove the request is fresh, it does not prove the caller is the station rather than someone who read the string out of a log, and it does not bind the credential to the request body. A scheme where the caller **signs** a canonical view of the request instead of sending the secret is a different model with different properties, and it is a different subject. Within the bearer model, the honest answers are: keep the credential off every channel that persists it, keep the permissions narrow, and be able to revoke quickly. Finally, be precise about refusals. A key the server cannot match to any row means *I do not know who you are*: `401`, with `WWW-Authenticate` naming the scheme. A key the server matched, belonging to a live station, that simply may not perform this call means *I know exactly who you are, and no*: `403`.
- The station operator calls and says they have lost the only copy of the key. What happens?They get a new key. The service issues a second key for the same station, the technician deploys it, and the lost one is revoked on a short deadline. Recovery is impossible by construction, and that is the property you paid for — if support could read a key back, so could anyone who convincingly impersonated the operator on the phone.
- Why store a separate non-secret key identifier at all, rather than looking the key up by its digest?You can look up by digest, and it works, but the identifier is what makes the key usable everywhere else: it is safe in logs, in a rate-limit counter, in an audit trail and in a support ticket, while a digest of the live credential is none of those things. It also lets the operator console show which key is which.
- Does this row need an expiry column?Add one, even if the default is 'none'. An optional expiry lets you issue a genuinely temporary key for a contractor or a migration, and it lets a partner opt into a yearly renewal. What you must not do is rely on expiry as your revocation mechanism — a leaked key with eleven months left is live for eleven months.
It is the difference between a hotel keeping a photocopy of your passport and keeping only enough of its details to recognise it when you present it again. The second still identifies you at the desk; the first turns a burgled filing cabinet into four hundred usable passports.
saying these in an interview costs you the question
- Encrypts the key reversibly so support can read it back to a locked-out partner
- Stores the key in plain text and argues that database access is already restricted
- Thinks a machine key must be presented with a username, like a user login
- Returns 403 for a key string the server cannot match to any stored record
- Treats a distant expiry date as the revocation story for a leaked key
- Logs the full credential at the edge because it is 'just an API key, not a password'