Roll back a credential rotation when revoking the old one fails - #54
Roll back a credential rotation when revoking the old one fails#54zopeVaibhav wants to merge 1 commit into
Conversation
|
Closing this one, and I want to be specific about why, because the bug you found in #53 is real and worth fixing — it is the chosen remedy I cannot take. The rollback can leave no working credential at all.
For a The compensation is silent, and it is silent exactly when it matters.
It does not survive the process dying. If the server is killed between the write and the revoke — a rolling-deploy SIGTERM, an OOM — the compensation never runs and the orphan persists exactly as it does on The one-step fix is available here. The PR body gives as its reason that create and revoke cannot be made atomic. That is not the case: both rows are in the same Postgres table, Two things worth folding in while you are there, both surfaced reviewing this:
Credit where it is due: the secret handling on this path is clean — nothing is logged, and no plaintext reaches an audit payload or the rethrown error. If you want to send the transaction version I will review it quickly. |
Closes #53.
The problem
rotateCredentialwrites the new credential, then callsstore.revokeon the previous one. The two steps are independent, so a failure on the revoke leaves the new credential live in the vault while the caller sees an error and treats the rotation as failed.readModelSecretorders bycreatedAtdesc, so the orphan is what the runtime resolves to next time. Nocredential.rotatedaudit event is written for a failed attempt, so nothing on the audit trail says two live credentials exist for the same(provider, keyId). A caller that retries writes another orphan each time.The approach
Wrap the revoke in a try/catch. If it throws, revoke the credential that was just written and rethrow the original error. The secondary revoke swallows its own error so the caller still sees the real cause. The
credential.rotatedaudit event stays where it was, past the try/catch, so the trail continues to reflect only rotations that actually happened.Either both sides moved or neither did.
What is not covered
persistCredentialand the revoke (process killed, node lost) still leaves an orphan. A durable compensation would need a workflow table or a two-phase write; out of scope for the bug on the table.createandrevoke. The store interface does not expose a transaction, and adding one touches every backend that implementsCredentialStore.Verification
rolls back the new credential when revoke of the previous one failsinserver/tests/credentials.test.ts. It fails onmainwithExpected to contain: "credential-new" / Received: [ "credential-old" ]and passes with the fix. All 13 tests in that file green.bun run testreports 684 pass, 5 skip, 0 fail across 78 files.bun run typecheck,bun run format,bun run buildall clean.bun run lintreports the same 25 warnings that are already onmain.