Skip to content

reapRotatedSecret deletes the IN-FORCE ciphertext when a SettingsEngine adapter drops context, leaving a dangling value_enc and an unreadable setting #8262

Description

@os-zhuang

Found while measuring #8103 (report-only orphan classifier, PR #8261). Filed unassigned for triage; not touched in that PR, since the reaping landed in #8063 and re-litigating it was explicitly out of scope there.

What was measured

SettingsService.reapRotatedSecret deletes the handle that upsertRow reports as previousEnc. It never confirms the repoint it is cleaning up after actually took effect — it infers that from previousEnc !== nextEnc.

That inference holds for the shipped adapter, which forwards context: { isSystem: true }. It does not hold for an adapter that drops context — and SettingsEngine's own doc comment contemplates exactly that reader ("⛔ An adapter over IDataEngine MUST forward this. Dropping it restores the defect silently").

Driving a real ObjectQL over the real SysSetting / SysSecret schemas with such an adapter, three successive writes of an encrypted specifier give:

writes sys_secret rows sys_setting.value_enc
pre-#8063 (no reaper, context dropped) 1 → 2 → 3 pinned at the first handle
today's reaper + context dropped 1 → 1 → 2 pinned at the first handle — which no longer exists

Sequence on the second write: the new ciphertext H2 is inserted; the UPDATE has value_enc stripped (it is readonly: true and the caller is not system), so the row still names H1; upsertRow returns previousEnc = H1; the reaper then deletes H1the row still in force.

Why this is worse than the defect it descends from

materialiseRow dereferences the handle, gets nothing back, logs and returns null. So the setting silently reads as empty and the stored credential is gone — not recoverable, since the audit trail records digests rather than handles or ciphertext.

Before #8063 the same adapter bug was bad but non-destructive: the rotated-away credential merely stayed in force. After it, the same bug destroys the value. The warning in settings-service.types.ts still describes only the old consequence ("the rotated-away credential stays in force"), so an adapter author reading it will not learn that the failure is now data loss.

Suggested direction (not a design ruling)

Make the reaper verify rather than infer — re-read the row and delete previousEnc only when value_enc really is nextEnc. That turns a non-forwarding adapter back into a recoverable failure and costs one read on a path that already awaits I/O. The doc comment's consequence sentence wants updating either way.

Repro

packages/services/service-settings/src/sys-secret-orphan-report.test.ts on PR #8261 carries the harness: boot({ forwardContext: false, withDelete: false }) is the faithful pre-fix instance; flipping withDelete back on reproduces the 1 → 1 → 2 table above.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions