Rotation reports failure for a rotation that already succeeded #144

Open
opened 2026-09-12 19:32:44 +02:00 by lz · 0 comments
Owner

Found in the /pr-review pass over feat/mcp.

In _handleRotate (routes/api/auth/rotate-passphrase/+server.ts) the sequence is:

store.destroyAll();
const { id } = store.create(Buffer.from(result.key));
result.key.fill(0);
logger.info('auth: passphrase rotated');
...
options: await sessionCookieOptions(database)   // <-- can throw, after the commit

sessionCookieOptions runs after the new wrapper is committed and after every session has been destroyed, and nothing catches it. A throw there produces a 500 {"message":"Internal Error"} in the rotation modal while the log says auth: passphrase rotated and the new passphrase is already the live one.

Measured trigger: FORCE_SECURE_COOKIE=yes makes coerce throw expected bool, got "yes". Any settings-read failure (SQLITE_BUSY on the read microseconds after the committed write) does the same.

Operator sees "rotation failed", tries the old passphrase, and is locked out until they work out that the new one is live.

The same shape exists more mildly at unlock/+server.ts:18-19: a throw between sessions.create(key) and cookies.set(...) orphans a live-DEK session and the operator cannot log in.

Fix

Resolve the cookie options before the irreversible part, or catch around the tail and return a success that names the cookie problem separately. The write must not be able to succeed while the response says it failed.

silent-failure-hunter also noted that the successful legacy upgrade — a one-time irreversible re-key of every sealed column — is currently unlogged, as are all failed unlocks and both rotation failure paths. Worth folding into the same pass.

Found in the `/pr-review` pass over `feat/mcp`. In `_handleRotate` (`routes/api/auth/rotate-passphrase/+server.ts`) the sequence is: ``` store.destroyAll(); const { id } = store.create(Buffer.from(result.key)); result.key.fill(0); logger.info('auth: passphrase rotated'); ... options: await sessionCookieOptions(database) // <-- can throw, after the commit ``` `sessionCookieOptions` runs **after** the new wrapper is committed and after every session has been destroyed, and nothing catches it. A throw there produces a 500 `{"message":"Internal Error"}` in the rotation modal while the log says `auth: passphrase rotated` and the **new passphrase is already the live one**. Measured trigger: `FORCE_SECURE_COOKIE=yes` makes `coerce` throw `expected bool, got "yes"`. Any settings-read failure (`SQLITE_BUSY` on the read microseconds after the committed write) does the same. Operator sees "rotation failed", tries the old passphrase, and is locked out until they work out that the *new* one is live. The same shape exists more mildly at `unlock/+server.ts:18-19`: a throw between `sessions.create(key)` and `cookies.set(...)` orphans a live-DEK session and the operator cannot log in. ## Fix Resolve the cookie options **before** the irreversible part, or catch around the tail and return a success that names the cookie problem separately. The write must not be able to succeed while the response says it failed. ## Related `silent-failure-hunter` also noted that the successful legacy upgrade — a one-time irreversible re-key of every sealed column — is currently unlogged, as are all failed unlocks and both rotation failure paths. Worth folding into the same pass.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus#144
No description provided.