skip to content

Your server stores each sensor's long-lived refresh credential in a table — why store a hash of it, and one row per device?

level: juniorimportance: must knowfreq 58%

answer

  1. the table is itself a credential store
  2. leaked read, nothing presentable
  3. high entropy, so no stretching needed
  4. the digest is the lookup key
  5. one row per device, targeted kill

basics

~20 s

Storing only a digest keeps the table from being a pile of live bearer credentials: a leaked read yields nothing a sensor could present. One row per device keeps revocation targeted, so killing one sensor does not strand the whole fleet.

solid answer

~40 s

A refresh credential is a bearer credential — whoever holds the string is that sensor. If the column holds the string, the table is forty thousand live credentials reachable by every backup, replica and ad-hoc query. So the row holds a fast cryptographic digest instead: the server hashes what the sensor presented and looks the row up by that digest, and never needs the original back. That also means the credential is handed over exactly once, at issue, and no console can ever show it again. One row per device — carrying device identity, issue time, expiry and a state column — is what makes a kill targeted: you can revoke one sensor without touching its depot siblings, and two devices sharing a row would race each other on every rotation.

code

json · 10 lines
json
{
  "device_id": "sensor-7f31c9",
  "subject": "fleet/cold-chain/sensor-7f31c9",
  "credential_digest": "9f2b...c41a",
  "family_id": "fam-04be21",
  "state": "active",
  "issued_at": "2026-09-19T04:11:07Z",
  "expires_at": "2026-12-18T04:11:07Z",
  "last_presented_at": "2026-09-19T06:40:55Z"
}

go deeper

for a junior

Remember the shape of the answer: the stored value must not be presentable, and each device gets its own row. Name the attack each alternative loses to — a leaked dump for plain text, a fleet-wide lockout for a shared row.

for a middle

Explain why a fast deterministic digest fits a high-entropy secret while a password needs a slow salted one, and why the salt would break the lookup unless you also index a public identifier beside it.

for a senior

Show that you know what the digest does not buy: an already-minted short-lived token survives revocation until its own expiry unless verification consults a suppression list. Say who can read the table today and what each path would yield.

for a principal

Frame it as blast radius per access path. Decide whether re-display of a credential is ever allowed, and make re-enrolment cheap enough that the answer can stay no without operations quietly adding a reversible column.

Forty thousand battery-powered sensors sit in freezers, trucks and depots. Each holds a long-lived refresh credential and, when it wakes on a weak radio link, trades that credential for a short-lived one it uses to post temperature readings. Server-side there is a table with one row per sensor, and the first thing a reviewer asks is what is actually in that row. ## The table you are really building A refresh credential is a **bearer** credential: possession is authority. If the column holds the credential itself, the table is not a set of records *about* credentials — it is forty thousand usable credentials in one place, reachable by every path that reaches a database: - a nightly dump sitting in object storage; - a read replica an analyst can query; - an injected predicate on some unrelated endpoint; - a support engineer with production read access; - a restore copied to a laptop for a debugging session. Storing a **digest** changes what all of those are worth. The server hashes the value the sensor presented and looks the row up by that digest. It never needs the original back, so it never has to keep one. ## Why a fast digest and not a slow verifier hash Password storage reaches for a deliberately slow, per-row-salted function because a human-chosen password has perhaps 20-30 bits of real entropy and can be guessed offline. A refresh credential is machine-generated with 128-256 bits from a cryptographic generator: there is no dictionary and no offline guessing, so stretching buys nothing. A single fast digest such as SHA-256 is the right tool, and it has one property the slow one lacks — it is **deterministic**, so the digest of the presented value is a lookup key. A per-row salt would destroy exactly that: with a different salt per row, finding the row means trying every row. The honest alternative, if you do want a salted slow hash, is to issue the credential as two parts — a public identifier you index on, and a secret you verify — which is how long-lived machine keys are usually shaped. Either design works; what does not work is a salted hash with nothing else to find the row by. | storage choice | what a leaked read gives | can the console show it again | lookup | |---|---|---|---| | the credential in plain text | every sensor's live credential | yes | index on the value | | reversibly encrypted | every credential, once the key leaks too | yes | index on the ciphertext | | fast digest | device identities and lifetimes, nothing presentable | no | index on the digest | ## The column you must not add The temptation is a reversible copy so an operations console can re-display a credential a technician mislaid. That single column puts the whole table back into the first row of the table above, because the key that decrypts it lives in the same deployment that reads it. The correct answer to a mislaid credential is re-enrolment: mint a new one, hand it over once, revoke the old row. ## One row per device, not one per subject One row per device is a revocation decision before it is a schema decision: 1. **Targeted kill.** A sensor recovered from a skip with its flash read can lose its own credential while its depot siblings keep reporting. 2. **No rotation race.** If four sensors share a row, every rotation invalidates the other three's copy, and you get a storm of false replays on a link that already retries. 3. **Attribution.** The row records which device last presented, and when — the only evidence you will have when something looks wrong. 4. **Clean re-enrolment.** A replaced radio board gets a new row rather than mutating a shared one. ## What hashing does not do Hashing the stored credential protects the *table*. It does nothing about a short-lived token already minted from that credential and already in flight. Revoking the row stops future mints immediately; the token the sensor (or a thief) is holding keeps being accepted by verifiers until its own `exp` passes — minutes, typically — unless verification also consults a suppression list. That gap is the revocation lag, and it is a separate piece of bookkeeping from the row you just hardened.

  • If only a digest is stored, what must the issuing path do differently at the moment the credential is created?
    Hand the credential to the device exactly once, in the response that created it, and never log it, echo it into a trace, or keep a reversible copy anywhere. After that the server holds only the digest, so a lost credential has one remedy: re-enrol the device and revoke the old row.
  • Does hashing the stored refresh credential shorten how long a stolen short-lived token keeps working?
    No. The digest protects the table at rest. A token already minted stays acceptable to verifiers until its own `exp`, unless verification consults a suppression list. Those are different mechanisms with different stores, and conflating them is how teams believe they have revoked something they have not.
  • A sensor is re-enrolled after a battery swap. Does it get a new row or an updated one?
    A new row. The old row keeps its device identity, its issue time and its state so the history stays readable and a later presentation of the old credential is still recognisable rather than simply unknown. It ages out on its own expiry.

A hotel keeps the imprint of which key card opened which door, not a drawer of cut spares. Losing the ledger tells a thief which rooms exist; it does not open one.

saying these in an interview costs you the question

  • Store it encrypted so the console can display it again
  • It is random already, so plain text in the table is fine
  • One shared row per depot; its sensors can use the same credential
  • A salted slow hash, with no other column to find the row by
  • Hashing the row also protects tokens already minted from it