Relationships
#2482 server tokens: concurrent mints of the same name can pair one mint's record with the other's secret, so a credential authenticates as the wrong principal
Opened by skunk-ape · 9/24/2026· Shipped 10/5/2026
Summary
Two concurrent mints of the same server token name can leave a token record from one mint paired with the secret from the other. The credential then authenticates as the wrong principal. Nothing prevents the race or detects the mismatch.
This came up while planning #2466. It predates that fix and affects every mint path.
How it happens
mint in src/domain/models/access/server_token_model.ts is check-then-act:
- It reads
token-mainand refuses if an active token exists. - It writes the plaintext to the vault under
serverTokenSecretKey(name), which isserver-token-<name>, one key per name. - It writes the
token-mainrecord, which holds theprincipalId.
Steps 2 and 3 are separate writes to separate stores (the _token-secrets control-plane store, and versioned data), each last-writer-wins. If mints A and B for the same name interleave as secret A, secret B, record B, record A:
- The final record is A (principal A), and the final secret is B.
- The holder of credential B presents
<name>.<secret B>. It matches the vault, and the record says principal A, so B authenticates as principal A. - The holder of credential A is rejected.
rotate has the same shape (vault put, then record write).
Where it can happen
- Two serve replicas, or a
--servermint racing a local CLI mint on the serve host. - The OAuth device-login mint (
device_auth_handler.ts) racing either of those for the same token name. - Within one serve process, the sync gate serialises handlers, so a single replica is not exposed on its own.
- The CLI takes a model lock only when the definition already exists (
acquireModelLockskeyed by model id). A first mint has no id yet, so it is unlocked even locally. Two concurrent first mints may also create two auto-definitions with the same name. That part is from reading the code, not verified.
Impact
Both racing callers must be admins, who can already mint a token for any principal, so there is no privilege escalation. It is a correctness and audit problem: a credential authenticates as a principal other than the one its minter asked for, and one mint silently loses.
Options considered
- Fingerprint in the record. Store a hash of the plaintext in
token-mainand check it at auth. This detects the mismatched pairing and fails closed, but one mint is still lost, and its holder gets a confusing rejection. - Per-mint secret key in the record. For example
server-token-<name>-<mintId>, recorded insecretKey. The record then always points at its own secret, so the last complete mint wins cleanly and the loser simply fails. This touches the code that assumesserverTokenSecretKey(name): reveal, revoke, GC, the boot consistency sweep, and migration. - Name-keyed lock around mint and rotate. This is the only option that prevents the race. It needs a datastore lock keyed by token name rather than model id, so it covers a first mint.
These combine well: a lock to prevent the race, plus a per-mint key (or a fingerprint) as defence in depth. All of them change the ServerToken aggregate, so existing records without the new field need a compatibility path.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.