Night 2026-10-17: restore keeps the DB password (R-1106, 07 §7.4), restic 0.19.1, hardening; 8 rows closed, 7 filed; evidence
gates / gates (push) Successful in 6m57s

This commit is contained in:
2026-10-16 23:59:53 +02:00
parent 96e15c15ba
commit 5c36d4c110
39 changed files with 2250 additions and 23 deletions
@@ -952,7 +952,7 @@ rather than being folded into a neighbour. Where the warning is SHOWN on the das
**[DESIGN] restic is a pinned decision, never „whatever the OS ships".** Until v0.322.0 it came from the runtime's apt
list, so a base-image move (Debian 12 → 13) would have swapped restic 0.14.0 for 0.18.0 under the off-site code with
nobody choosing it. Since v0.323.0 the controller image carries the **official restic release binary** of one version
(`ARG RESTIC_VERSION`, today **0.18.1**), checked at build time against restic's signed `SHA256SUMS` (vendored release
(`ARG RESTIC_VERSION`, **0.19.1** since controller v0.324.0 — 0.18.1 in v0.323.0; operator decision 2A, R-1107), checked at build time against restic's signed `SHA256SUMS` (vendored release
key, no keyserver) and against a per-architecture sum pinned in the Dockerfile; any mismatch fails the build. The
Proxmox hosts run no restic at all (measured on all three test hosts): the agent's escrow path only stores and returns
the repository password, and every restic call happens in the guest.
@@ -964,7 +964,7 @@ holds REAL outputs of each version the code was checked against (captured on one
| What the code relies on | 0.18.1 |
|---|---|
| a missing source path is skipped with a warning, exit 0 (§6.3, SP-3.4); an unreadable file exits 3 | same |
| a missing source path is skipped with a warning, exit 0 (§6.3, SP-3.4); an unreadable file exits 3 | 0.18.1 same; **0.19.1 exits 3 for a missing top-level path** (restic #4467) — the capture's stat-filter keeps such paths out of argv, so only a path vanishing in between reaches it, and that race now reads as the app's failure instead of a silent partial copy |
| `backup --json` status lines (`percent_done`, totals, `current_files`) | same field names |
| `snapshots --json`, `snapshots latest --tag … --json`, `stats <id>` / `--mode raw-data` | same shapes (new fields only) |
| the §6.7 classes: wrong password, no repository, refused port / key, unreachable host, dead rclone ssh | same class each |
@@ -975,8 +975,10 @@ holds REAL outputs of each version the code was checked against (captured on one
| a store written by both: 0.14.0 reads and restores a 0.18.1 snapshot after a 0.18.1 `forget --prune`; 0.18.1 restores a 0.14.0 snapshot | proven on a scratch store; on demo-hp's real store both directions are proven (byte-identical trees); 0.14.0 reading after a 0.18.1 prune there waits for the store's next weekly clean-up window |
Exit codes changed (10 no repository, 11 locked, 12 wrong password); nothing in the controller reads a restic exit code.
**Open:** the 0.18.1 binary is built with Go 1.25.1 and carries 50 fixable scanner findings (R-1107 — the operator's
choice of 0.19.x).
The 0.19.1 re-proof (2026-10-16 night): its notes read for every row above; fixtures for 0.14.0, 0.18.1 and 0.19.1
recaptured from one shared store; `TestR1105_*` runs each version against 0.14.0 — same classes, same JSON, the same
forget plan as 0.18.1. The 0.18.1 binary (Go 1.25.1) carried 50 fixable scanner findings; 0.19.1 (Go 1.26.4) is the
reason for the move (`audits/night-restore-2026-10-17/P2/`).
## 7. The recovery chain (D3) — the reason this document exists
@@ -1153,6 +1155,28 @@ password would have failed. Separately, Grafana's `type: password` admin login w
the sentinel value was live in the container and encrypted in the guest, and appeared in **0 files**
anywhere under the backup namespace. Full record: `audits/D5-drive-alone-restore-2026-07-30.md`.
**[DESIGN — operator decision 1A, 2026-10-16 21:57; built in controller v0.324.0 — R-1106, R-127 (b)] A restored
database comes back with the password it was saved with.** A database engine reads `POSTGRES_PASSWORD`,
`MARIADB_ROOT_PASSWORD` and the like only when it initialises an EMPTY data directory; a restored data directory opens
with its own password only. So when a restore puts back an app's database, the database credentials the data was
saved with are written into the app's settings before the app starts, and they REPLACE whatever the current install
holds. The credentials are the app.yaml variables that feed a database service's `*_PASSWORD` keys in the compose
that will run (`appbackup.DBCredentialVars`; 17 catalog apps, 20 fields, every one `type: secret` — the catalog gate
`db-credentials` refuses a template where that stops being true). Per route:
| Route | Where the database password comes from |
|---|---|
| On-box restore („Visszaállítás", `POST /backup/restore`), the update's rollback, „use my kept data", an off-site restore at the snapshot's version | the unit (unit-wins, `unitRestoreEnv`) |
| Off-site full restore that keeps the app's definition (same version — the reinstall case) | the snapshot's unit, written by `stacks.SetStackSecretValues` before the database starts — **only when the snapshot replays the database's data volume**; a dump-only snapshot replays into the current data directory and keeps the current password |
| `.fab` import | the bundle's own app.yaml (it carries the whole env) — unchanged |
**A restore whose unit does not carry the password is REFUSED before anything changes** — unless the current install
is OLDER than the data (then the current value is the one the data was written with). The household reads „Ezt a
mentést nem lehet visszaállítani: nem tartalmazza az adatbázis jelszavát, és nélküle az alkalmazás nem tudna elindulni.
A mostani telepítés érintetlen maradt." **A database password is never generated for restored data** — O4 no longer
reaches these names. On the reinstall path the reinstall's `type: password` admin logins are recorded as restored-login
fields (R-694's rule — the page says to use the old password; R-926).
### 7.2 A tier whose prerequisites cannot be met in the failure it exists for
**[FACT]** Two instances, both current: