feat(e2): encrypted-backup deploy — Age-at-rest in cron.sh, gated, restore decrypts (Track E)#62
Merged
Merged
Conversation
…store decrypts (Track E)
## What
Wire Age-encryption-at-rest into the canonical backup path per the E1 contract
(scheme X, variant B). The standalone modules/backup/encrypted_backup.sh draft —
which dumped with an argv password (-p"${DB_ROOT_PASS}", the form
cron_security_shape_guard_test.bats forbids) and used a per-second timestamp — is
absorbed and removed. Its encryption logic moves into the live generator
modules/backup/cron.sh behind a new ENABLE_ENCRYPTED_BACKUP flag (default off,
threaded into the render exactly as ENABLE_S3_STORAGE).
Producer (cron.sh): when the flag is on, each DB dump and (S3-off) files archive is
encrypted after its dump+checksum+integrity step with
age -r "$(cat ${INSTALL_DIR}/.age-public-key)" -o "<artifact>.age" "<artifact>";
the .sha256 is taken over the CIPHERTEXT; on a successful encrypt+verify the plaintext
artifact + its sidecar are removed (integrity-or-delete: on failure the partial .age is
removed instead). The secure umask-077 --defaults-extra-file dump shape is unchanged
(encryption is a post-dump host-side step touching no credential), so the draft's
argv-password flaw never ships. Retention and the rclone push are extended to cover
*.age / *.age.sha256. The daily <YYYY-MM-DD> timestamp is kept (per-second stays PITR/C).
Consumer (cli/actools restore arm): selects the newest of <env>_db_*.sql.gz or
*.sql.gz.age; requires ${INSTALL_DIR}/.age-key.txt when the selection is encrypted;
decrypts with age --decrypt -i before gunzip | mariadb; and — hardened — ABORTS before
any DROP DATABASE when a present .sha256 fails (a missing sidecar still only warns).
No dispatch command added or removed (REGISTERED stays 30).
Contract (docs/backup-format-contract.md): variant B moves from TARGET / NOT YET LIVE to
LIVE (gated); the scheme-X timestamp grammar drops "encrypted" from the per-second
producer list (encrypted-daily uses the daily timestamp). A is unchanged in substance;
C stays not-yet-live.
Guards: backup_format_contract_guard_test.bats gains arms 7-10 (encrypted producer,
encrypted consumer, anchored-root non-vacuity, ciphertext-checksum non-vacuity), makes
arm 6's consumer-drift sed global (the restore arm now has two "${env}"_db_ globs), and
anchors the consumer-root check to the closing quote. cron_security_shape_guard_test.bats
gains arm 5 (secure shape holds with encryption on). The backup-cron golden is regenerated
(not hand-edited); golden_drift 6/6 and backup_cron_drift 3/3 stay green.
e2e (.github/workflows/e2e.yml): the install env sets ENABLE_ENCRYPTED_BACKUP=true and a
new "Encrypted backup round-trip" step produces a backup, asserts the encrypted artifact +
verifying ciphertext checksum + no plaintext remains, then restores non-interactively and
asserts the DB reloads.
## Gate
Behavior-CHANGING — a branch e2e MUST be green before merge (the round-trip step is the
live proof; "MariaDB ready." is the real signal, an SSH-timeout is infra -> re-run).
## Scope / verification
- bats -r tests/: 247 -> 252 total (+5 new arms, all green); the 12 pre-existing
jq-dependent secrets/state failures are environmental and unchanged (no regression).
- modules/host/age.sh, tests/helpers/capture_backup_cron.sh, and all PITR/binlog drafts
are byte-identical to baseline 6d06a73.
- Consequential edits (declared): live_module_file_inventory_test.bats manifest and
runtime-authority-map.md drop the removed draft.
- Author actools-pl <feezixmp@gmail.com>; single-author commit, no co-author trailer.
Ledger: add Entry 032 (E2, Pending; behavior-changing; branch-e2e-required); ratify
Entry 031 (E1, 6d06a73/#61, opens Track E), original Pending text preserved. Entries
030..001 byte-identical.
REVIEW (re-derive scope; byte-identity; guard non-vacuity; golden drift; patch reproduces
the tree) then DOC-CHECK then the operator's green branch e2e follow. The coding window
does not self-approve.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Wire Age-encryption-at-rest into the canonical backup path per the E1 contract (scheme X, variant B). The standalone modules/backup/encrypted_backup.sh draft — which dumped with an argv password (-p"${DB_ROOT_PASS}", the form cron_security_shape_guard_test.bats forbids) and used a per-second timestamp — is absorbed and removed. Its encryption logic moves into the live generator modules/backup/cron.sh behind a new ENABLE_ENCRYPTED_BACKUP flag (default off, threaded into the render exactly as ENABLE_S3_STORAGE).
Producer (cron.sh): when the flag is on, each DB dump and (S3-off) files archive is encrypted after its dump+checksum+integrity step with age -r "$(cat ${INSTALL_DIR}/.age-public-key)" -o ".age" ""; the .sha256 is taken over the CIPHERTEXT; on a successful encrypt+verify the plaintext artifact + its sidecar are removed (integrity-or-delete: on failure the partial .age is removed instead). The secure umask-077 --defaults-extra-file dump shape is unchanged (encryption is a post-dump host-side step touching no credential), so the draft's argv-password flaw never ships. Retention and the rclone push are extended to cover *.age / *.age.sha256. The daily timestamp is kept (per-second stays PITR/C).
Consumer (cli/actools restore arm): selects the newest of db*.sql.gz or *.sql.gz.age; requires ${INSTALL_DIR}/.age-key.txt when the selection is encrypted; decrypts with age --decrypt -i before gunzip | mariadb; and — hardened — ABORTS before any DROP DATABASE when a present .sha256 fails (a missing sidecar still only warns). No dispatch command added or removed (REGISTERED stays 30).
Contract (docs/backup-format-contract.md): variant B moves from TARGET / NOT YET LIVE to LIVE (gated); the scheme-X timestamp grammar drops "encrypted" from the per-second producer list (encrypted-daily uses the daily timestamp). A is unchanged in substance; C stays not-yet-live.
Guards: backup_format_contract_guard_test.bats gains arms 7-10 (encrypted producer, encrypted consumer, anchored-root non-vacuity, ciphertext-checksum non-vacuity), makes arm 6's consumer-drift sed global (the restore arm now has two "${env}"db globs), and anchors the consumer-root check to the closing quote. cron_security_shape_guard_test.bats gains arm 5 (secure shape holds with encryption on). The backup-cron golden is regenerated (not hand-edited); golden_drift 6/6 and backup_cron_drift 3/3 stay green.
e2e (.github/workflows/e2e.yml): the install env sets ENABLE_ENCRYPTED_BACKUP=true and a new "Encrypted backup round-trip" step produces a backup, asserts the encrypted artifact + verifying ciphertext checksum + no plaintext remains, then restores non-interactively and asserts the DB reloads.
Gate
Behavior-CHANGING — a branch e2e MUST be green before merge (the round-trip step is the live proof; "MariaDB ready." is the real signal, an SSH-timeout is infra -> re-run).
Scope / verification
Ledger: add Entry 032 (E2, Pending; behavior-changing; branch-e2e-required); ratify Entry 031 (E1, 6d06a73/#61, opens Track E), original Pending text preserved. Entries 030..001 byte-identical.
REVIEW (re-derive scope; byte-identity; guard non-vacuity; golden drift; patch reproduces the tree) then DOC-CHECK then the operator's green branch e2e follow. The coding window does not self-approve.