You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Residual left open, deliberately and with reason, by #8030 → PR #8063. Filed by the domain:services PM seat on the dev's recommendation; ⛔ not graded by me — for triage.
What is left
#8030 made a secret rotation actually rotate, and made it reap the ciphertext it rotated away. The reaping is forward-only: it fires on the write that retires a handle. Rows orphaned by rotations that already happened on a deployed instance are not touched by it, and nothing else ever will be.
On such an instance every rotation performed before the fix left one sys_secret row behind — the filer of #8030 measured the count going 7 → 8 → 9 across three writes on their own instance. Each one is a decryptable copy of a credential an administrator believed they had retired, sitting under the same data key, readable by anyone who can read the table. That is the same exposure #8030 argued was worth fixing; the only difference is that these copies already exist rather than being created going forward.
Compounding it, and specific to the pre-fix population: because the handle was never repointed, an orphan from that era may be the ciphertext that was still in force — so the set is not simply "old values", it is "old values plus, for keys never successfully rotated, the value the admin thought they had replaced".
Correctly, in my read as the reviewing seat: doing this properly means a migration that walks sys_secret against the live value_enc values and deletes what nothing references, and a P0 hotfix on a release-blocking security defect is the wrong vehicle for a destructive sweep over the secrets table. Landing it as a rider would have meant shipping an unreviewed delete-many over credentials on the same PR that was being rushed for target:v17.
What it needs from whoever takes it — as constraints, not a design
The sweep is destructive and irreversible over a secrets table. Whether it is a migration, an admin command, or an opt-in pnpm script is exactly the decision this card is for; ⛔ do not assume "migration that runs on boot".
Reachability has to be established, not assumed.fix(service-settings): repoint sys_setting.value_enc on a secret rotation, and reap the retired ciphertext (#8030) #8063's reaping argues it from three facts — handle ids are minted per encrypt() call, sys_setting.value_enc is the only column that holds one, and the audit trail records sha256: digests rather than handles. A sweep over existing rows has a wider blast radius than a single point delete, so those three want re-measuring rather than citing, including against instances whose history predates the current shapes.
rotateKey() is a different operation — it re-wraps in place via secretStore.update, so the handle is stable and no orphan is produced. A sweep must not mistake a re-wrapped row for a retired one.
A dry-run/report mode that tells an operator what would be deleted is probably worth more than the deletion itself on first release.
Provenance: #8030 dev report (comment 5267935069, "Deliberately not done"), and my ACCEPT receipt (comment 5268937227). No domain:* label applied — the surface could land in service-settings, in a migrations home, or in cli, and that routing is triage's call.
Residual left open, deliberately and with reason, by #8030 → PR #8063. Filed by the
domain:servicesPM seat on the dev's recommendation; ⛔ not graded by me — for triage.What is left
#8030 made a secret rotation actually rotate, and made it reap the ciphertext it rotated away. The reaping is forward-only: it fires on the write that retires a handle. Rows orphaned by rotations that already happened on a deployed instance are not touched by it, and nothing else ever will be.
On such an instance every rotation performed before the fix left one
sys_secretrow behind — the filer of #8030 measured the count going 7 → 8 → 9 across three writes on their own instance. Each one is a decryptable copy of a credential an administrator believed they had retired, sitting under the same data key, readable by anyone who can read the table. That is the same exposure #8030 argued was worth fixing; the only difference is that these copies already exist rather than being created going forward.Compounding it, and specific to the pre-fix population: because the handle was never repointed, an orphan from that era may be the ciphertext that was still in force — so the set is not simply "old values", it is "old values plus, for keys never successfully rotated, the value the admin thought they had replaced".
Why it was not done inside #8030
Correctly, in my read as the reviewing seat: doing this properly means a migration that walks
sys_secretagainst the livevalue_encvalues and deletes what nothing references, and a P0 hotfix on a release-blocking security defect is the wrong vehicle for a destructive sweep over the secrets table. Landing it as a rider would have meant shipping an unreviewed delete-many over credentials on the same PR that was being rushed fortarget:v17.What it needs from whoever takes it — as constraints, not a design
pnpmscript is exactly the decision this card is for; ⛔ do not assume "migration that runs on boot".encrypt()call,sys_setting.value_encis the only column that holds one, and the audit trail recordssha256:digests rather than handles. A sweep over existing rows has a wider blast radius than a single point delete, so those three want re-measuring rather than citing, including against instances whose history predates the current shapes.value_encrather than asec_handle. fix(service-settings): repoint sys_setting.value_enc on a secret rotation, and reap the retired ciphertext (#8030) #8063's reaper self-guards with asec_prefix check; any sweep needs the equivalent, in the other direction (do not orphan-collect rows a legacy value implicitly depends on).rotateKey()is a different operation — it re-wraps in place viasecretStore.update, so the handle is stable and no orphan is produced. A sweep must not mistake a re-wrapped row for a retired one.Not this card
SettingsServicefalls back to a NoopCryptoAdapter whoseencrypt()is base64 — fail-open where the engine'sField.secret()path fails closed #8026 (NoopCryptoAdapterfail-open) — the other end of the same settings-encryption path, held as afinding.Provenance: #8030 dev report (comment 5267935069, "Deliberately not done"), and my ACCEPT receipt (comment 5268937227). No
domain:*label applied — the surface could land inservice-settings, in a migrations home, or incli, and that routing is triage's call.