v0.105.0: fork-4 offsite password custody — hand-off + atomicity gate + DR inject + coord

Pairs with agent v0.77.0. StageEscrowSecret pushes the repo password to the
agent (POST /escrow/stage-secret) at offsite-enable → EscrowState="pending".
Atomicity gate: RunOffboxBackup (scheduler + handler) refuses until
EscrowState="escrowed" (operator POST /backup/offbox/confirm-escrow after the
escrow ceremony) — no un-recoverable offsite ciphertext can exist. DR:
POST /backup/offbox/inject-password pre-places a recovered 64-hex password 0600
(honored by WriteOffboxSecrets' IsNotExist guard; refuses clobber without
force). DR recipe gains non-secret offsite_restic coords (DRResticCoord); SFTP
key regenerated at DR, not escrowed. New settings.OffboxTarget.EscrowState.
Tests + atomicity & inject companion red-proofs green; UI gates pass. NOT yet
live-validated (supervised ceremony).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-09 15:13:59 +02:00
parent bde43f3a74
commit 0b09a799cb
14 changed files with 498 additions and 5 deletions
+28
View File
@@ -1,5 +1,33 @@
## Changelog
### v0.105.0 — fork-4: offsite password custody hand-off + atomicity gate + DR inject + DR coord (2026-07-09)
Pairs with agent v0.77.0 to make the restic-offsite repo password recoverable at DR (rides the customer-R
escrow) and forbids an un-escrowed offsite copy from existing. Validated design: custody spike `febdc56`.
- **Hand-off** (`internal/agentapi/client.go`): `StageEscrowSecret` pushes the repo password to the agent's
`POST /escrow/stage-secret` over the authenticated pinned local-API channel (value never logged). The
enable flow (`internal/web/offbox_handlers.go`) reads the 0600 password via a new
`Manager.PushOffboxPasswordForEscrow` (the handler never sees the value) and marks `EscrowState="pending"`.
- **Atomicity gate** (`internal/backup/offbox.go`): `OffboxRunnable()`/`offboxEscrowed()``RunOffboxBackup`
(and thus the daily scheduler + the run handler) **refuses to run until `EscrowState=="escrowed"`**, so no
un-recoverable offsite ciphertext can exist. `OffboxConfigured()` is unchanged (config/UI still work).
New `settings.OffboxTarget.EscrowState` (`""|"pending"|"escrowed"`, additive, preserved across edits).
- **Confirm + DR inject** (`internal/web`): `POST /backup/offbox/confirm-escrow` (operator, after the escrow
ceremony) → escrowed; `POST /backup/offbox/inject-password` (DR) → `Manager.InjectOffboxPassword`
pre-places a recovered 64-hex password 0600 (tmp+rename), refusing to clobber without `force` — a
subsequent `WriteOffboxSecrets` then uses it (the pre-place seam). `/backups` shows a pending-escrow
notice + confirm button.
- **DR recipe** (`internal/report/dr_recipe.go`): `DRRecipeAppHalf.OffsiteRestic *DRResticCoord`
{host,user,port,repo_path} — coordinates ONLY (the password is escrowed, the SFTP key is regenerable);
populated from `Manager.OffboxCoord()`; clears the `_NoSecrets` regex.
- Tests: atomicity (pending blocks run; confirm enables) **+ companion red-proof** (gate disabled → runs
while pending → FAIL); DR inject pre-place honored + refuse-clobber + companion (no-inject generates a
DIFFERENT password); `OffboxCoord`; agent stage endpoint (0600 + non-secret ack + cross-guest 403 +
value-not-in-log); `DRResticCoord` no-secrets. Web: run-gate + confirm + inject endpoints.
- **NOT yet live-validated** — the supervised escrow ceremony (enable→stage→escrow-create→confirm→gated run)
is the operator-run follow-up.
### v0.104.0 — off-box unit discovery (durable, deployment-independent) + no-silent-success (2026-07-09)
Fixes the off-box mis-resolution + silent-success landmine surfaced by the Storage-Box spike and pinned by