Probe the oldest stored values before recording an encryption key

The boot key check recorded whatever key was configured whenever its probe
found nothing to try. It only read string columns, so an install whose
ciphertext sat in Bytes columns (notes, documents, ECG, symptoms) read as
empty; a probe that ran out of time did the same; and rows written by a
process that served under a wrong key could open on the next boot and get
that key recorded as the right one.

The probe now reads every registered column, string and Bytes, in all four
stored layouts, and tries the oldest values first, ordered by the row's
updatedAt (else createdAt, else primary key). A key is recorded only when
the oldest value that opens is preceded by at most one that does not, or
when no column holds a single value under the key id. A time-out, an
unreadable table, oversized values or mixed results are inconclusive: the
process serves, records nothing and checks again at the next start.

When the first insert loses a race, the stored record is read back and must
open under this process's key, else the outcome is a mismatch.

A rotation that walked every column without errors removes the record of
each previous key id that no longer holds any value. The refusal log block
and the self-hosting pages explain how to reset the check for a key id.
This commit is contained in:
Marc-André Bombeck committed 2026-10-03 23:29:55 +02:00
1 parent 3a99c8cfee
commit 2c295bbea8
9 files changed
+1142 -199

No files matched your search

+7
View File
@@ -103,6 +103,13 @@ under `v1` (the synthetic id assigned to the existing `ENCRYPTION_KEY`).
a full `Columns walked` line. Both read the same registry the rotation
walks.
A run that walked every column without an error also removes the boot
key check's record of each previous key id that no longer holds a single
value (`encryption_key_canaries`); the script prints which ids it removed
and, for an id that still holds values, the columns that do. With the record
gone, the id can later carry a different key without the start-up check
refusing it.
A zero only means "nothing left" when the run also says it walked every
registered column. A zero from a run that skipped columns means "never
looked", and dropping the old key on it makes those rows permanently