Skip to content

fix(backups): say how an archive is protected instead of assuming an envelope - #194

Merged
NathaelB merged 1 commit into
mainfrom
chantier/v12-06-archive-protection
Sep 12, 2026
Merged

NathaelB merged 1 commit into
mainfrom
chantier/v12-06-archive-protection

Conversation

@NathaelB

Copy link
Copy Markdown
Owner

Part of #177. Found while writing #182, and worth landing before it.

The defect

#190 and #192 shipped Backup.key: KeyRef and an endpoint that requires key_provider, key_name and key_version on every archive. Both assume every archive is wrapped by a key manager.

The family of backup that actually ships is not. CloudNativePG hands the archive to the object store, which encrypts it under a key the store holds. Nothing wraps a data key for it, so there is no key reference to record.

Writing the reporting side is what surfaced it: I went looking for the value a data plane would put in key_provider for a real archive and there is none. The only field that fits is the installation's configured wrapping key, which unwraps nothing, and which a restore would read back as an instruction to go and fetch it.

The fix

docs/backup-encryption.md already makes this distinction in prose. This makes it in the type:

pub enum ArchiveProtection {
    /// The store encrypted it under a key the store holds. No key reference,
    /// because there is no key this platform could be asked for.
    StoreManaged,
    /// A data key wrapped by a key manager.
    Envelope(KeyRef),
}

A restore can now tell which one it is holding without reading anything, and needs_a_key_manager() answers the question it actually has. The variant is not a detail of how the bytes were written: it answers what a customer asks, which is whether this platform can read their archive.

Both halves of the mistake are silent, so both are refused

An envelope with no key is an archive nobody can open. A key beside a store managed archive is a key that opens nothing. Neither shows up until a restore, which is the worst possible moment, so neither is repaired or filled in:

  • In the type — the variants make both unrepresentable.
  • In the schemabackups_envelopes_name_their_key makes both unwritable, with a test that inserts a keyless envelope through raw SQL and asserts the database refuses it.
  • At the endpoint — the three key fields go together or not at all, with a test for each of the three partial sets.

The migration

Additive and reversible. The key columns become nullable, a protection column says which mechanism was used rather than leaving it inferred from whether the others are there, and the default that backfills existing rows as envelope is dropped immediately afterwards so a new row cannot inherit a protection nobody stated.

Every row written so far was written under the old assumption, and there are none in production, so the backfill is correct by construction rather than by luck.

Verification

cargo test -p aether-postgres --test backups     9 passed, against a real Postgres
cargo clippy --workspace --all-targets           clean
cargo nextest run --workspace                    994 passed, 1 skipped

The full run had every REQUIRE_ guard armed, so nothing skipped itself.

.sqlx and openapi.json were regenerated, not edited.

…envelope

The table and the endpoint were written assuming every archive is wrapped by a
key manager. The family of backup that actually ships is not: CloudNativePG
hands the archive to the object store, which encrypts it under a key the store
holds. Nothing wraps a key for it.

Writing the reporting side is what surfaced this. There was no honest value for
key_provider on a real archive, and the only field that fit was the
installation's configured key -- which unwraps nothing, and would be read back
during a restore as an instruction to go and fetch it.

So the distinction docs/backup-encryption.md makes in prose is made in the type.
StoreManaged has no key reference because there is no key this platform could be
asked for; Envelope carries one. A restore can now tell which it is holding
without reading anything, and needs_a_key_manager answers the question it
actually has.

The columns follow, including the CHECK that makes both halves of the mistake
unwritable: an envelope with no key is an archive nobody can open, and a key
beside a store managed archive is a key that opens nothing. Neither shows up
until a restore, which is why they are refused rather than repaired.

The endpoint takes the three key fields together or not at all. A partial set is
refused for the same reason.
@NathaelB NathaelB added the bug Something isn't working label Sep 12, 2026
@NathaelB NathaelB self-assigned this Sep 12, 2026
@NathaelB
NathaelB merged commit 403cdcb into main Sep 12, 2026
16 checks passed
@NathaelB
NathaelB deleted the chantier/v12-06-archive-protection branch September 12, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant