controller v0.93.0: NAS Part B off-box backup target (restic-over-SFTP)

Encrypted restic repo over SFTP for the app-data tier (the off-site 3-2-1 leg). A dead
NAS fails fast via -oConnectTimeout (spike Q8), never hangs the runner; secrets are 0600
files (ride DR via PBS whole-CT); init-if-absent, retention forget --prune, restore,
single-flight, per-app toggle + UI. restic re-added to the image.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
This commit is contained in:
2026-06-30 15:26:38 +02:00
parent ddeb509d1d
commit 2a7deadc93
11 changed files with 1059 additions and 0 deletions
+31
View File
@@ -1,5 +1,36 @@
## Changelog
### v0.93.0 — NAS Part B: off-box backup target (restic-over-SFTP) (2026-06-30)
Closes the NAS arc: back the app-data tier (each off-box app's recovery unit + DB dumps + volume tars) up
to the customer's NAS as an **encrypted restic repo over SFTP** — the "1 off-site" leg of 3-2-1, distinct
from the local cross-drive rsync copy and the agent's PBS whole-CT DR. No kernel mount; restic talks SFTP
to the NAS directly. Spike-validated (SPIKE-nas-storage Q8).
- **`Dockerfile`:** restic was dropped when cross-drive migrated restic→rsync — re-added `restic` +
`openssh-client` (restic's sftp backend shells out to `ssh`); version pinned by the Debian release.
- **`internal/backup/offbox.go` (NEW):** the restic-SFTP backend + orchestration.
- **Fail-fast (the load-bearing spike Q8 lesson):** every restic call carries
`-o sftp.args=…-oConnectTimeout=10…` so a dead NAS errors in ~10 s instead of a multi-minute TCP hang.
Also `-oStrictHostKeyChecking=yes -oUserKnownHostsFile=<pinned>` (no blind TOFU) + `-oBatchMode=yes`.
- init-if-absent (idempotent — a present repo is reused, never re-init), per-app `restic backup --tag`,
`forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune` retention, single-flight (shares
`m.running`) + migration-guard, restic's **own exit code** checked (never pipe-swallowed), restore via
`restic restore latest --tag <app> --target <scratch>` (non-destructive).
- **Secrets:** the SSH private key + the auto-generated repo password are 0600 files in the data dir —
never logged, never in a committed/non-0600 file; the repo is encrypted so the NAS sees only ciphertext.
They ride DR via the PBS whole-CT snapshot of the rootfs (the data dir), so a rebuilt box can reach the
off-box repo (the recovery-unit/dr-recipe stay secret-free).
- **`internal/settings/settings.go`:** `OffboxTarget` (host/port/user/repo path/schedule + runtime status,
no secrets) + per-app `AppBackupPrefs.Offbox` toggle + helpers.
- **`cmd/controller/main.go`:** daily `offbox-backup` schedule (04:15) gated on enabled+configured; a
failure (incl. fail-fast dead-NAS) alerts the operator via the allowlisted `backup_failed` event.
- **UI (`backups.html`):** "Külső (NAS) mentés" section — target config (host/port/user/repo + out-of-band
SSH key + known_hosts textareas), status (last run / repo size / snapshots), per-app toggles, run-now,
restore-to-scratch.
- Tests: ConnectTimeout present in base args + **the fail-fast companion red-proof** (a fake SSH transport
hangs to the ctx deadline WITHOUT the arg, fails fast WITH it); dead-NAS run fails fast + alerts + records
status; restore round-trip byte-identical (SFTP-shaped seam); single-flight skip; repo idempotency;
secrets are 0600. `go build/vet/test ./...` green.
### v0.92.0 — NAS network storage Part A2: registry + UI + per-share health (2026-06-30)
The controller side of NAS network storage, proxying to the validated agent foundation (felhom-agent
v0.50.0 `/netstorage/*`). An operator can add a customer's NAS share and point a media app at it — all via