Files
felhom.eu/documentation/audits/VALIDATION-offbox-storagebox-2026-07-09.md
T
admin 463ea369c4 docs(audit): offbox v0.104.0 VALIDATED live against the Storage Box (supervised)
Real endpoints, fresh box sub-account. Discovery proof: immich (undeployed,
USB-only unit — the v0.103.0 mis-resolution case) → ok/1 snapshot on the USB
path; 45MB db-dump restore sha256-matched. No-silent-success on the live box:
0/N → error + backup_failed alert; partial → ok + LastWarning. Persists across
restart. Forks 1+2 confirmed (direct; sub-account /home/<name>, ssh-copy-id -s
-f, console key field still broken F3); fork-4 custody surface recorded, escrow
design OPEN (repo pw + key not independently recoverable). Kept as standing
demo-offsite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-09 13:17:52 +02:00

114 lines
8.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VALIDATION — offbox v0.104.0 live against the Hetzner Storage Box (supervised)
**Date:** 2026-07-09 · **Class:** supervised operational validation (RUNBOOK) · **Executor:** Claude Code
live, with Viktor on the 🛑 STOP points. **No controller code changed; no version bump.** Closes the offsite
arc: proves v0.104.0's offbox (discovery + no-silent-success) end-to-end against the real box through the
real endpoints, in the exact scenario v0.103.0 got wrong.
---
## 1. VERDICT
**offbox v0.104.0 direct-to-Storage-Box is VALIDATED** through the real `/backup/offbox/*` endpoints. The
discovery fix works against the **real drive layout** (not just `t.TempDir()`): an **undeployed** app whose
recovery unit exists **only on the USB drive** — the precise case the old `systemDataPath` fallback
mis-resolved — was found and backed up. The no-silent-success behaviors both fired against the live box:
0/N → hard error + operator alert; partial → customer warning. A 45 MB db-dump round-tripped byte-perfect.
---
## 2. Confirmed inputs
| Thing | Value |
|-------|-------|
| controller | `:0.104.0` @ `908e4b9` on guest 9201, `Up (healthy)` |
| box | `u629193.your-storagebox.de`, port **23**, IPv4 `167.235.97.81`, ~869 GB free |
| account | fresh **sub-account `u629193-sub1`** (old compromised one deleted; console-created) @ own host `u629193-sub1.your-storagebox.de`, base dir `/felhom-demo` |
| RepoPath | **`/home/felhom-demo-repo`** (sub-account chroot presents its base dir as `/home` → RepoPath `/home/<name>`) |
| key install | console SSH-key field again didn't apply (**F3 persists**) → installed via `ssh-copy-id -p 23 **-s -f** -i <pub>` with the sub-account password (Viktor, 🛑) |
| known_hosts (public — safe) | `[u629193-sub1.your-storagebox.de]:23 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIICf9svRenC/PLKIL9nk6K/pxQgoiFC41wTNvoIncOxs` |
| host-key MITM cross-check | ed25519 key **byte-identical** to ep0's pinned `known_hosts` entry → PASS |
| method | config/toggle/run/restore all via **real endpoints** (curl to in-container `127.0.0.1:8080`; demo dashboard is open — localhost authoritative, avoids the public-URL POST no-op). Anti-F9 satisfied. |
**Actors:** DISCOVER = **immich** (undeployed; unit only on USB; real 45 MB `db-dumps/immich-postgres.sql`).
MISSING = **vaultwarden** (no unit anywhere). Partial 2nd = **audiobookshelf** (compose+manifest unit on USB).
## 3. Phase 3 — DISCOVERY PROOF ✅
immich toggled (NOT deployed; live `app.yaml` absent; unit only at `…/felhom-usb/backups/primary/immich`),
run via `POST /backup/offbox/run`:
```
[offbox] initialized restic repo
[offbox] backed up immich (/mnt/felhom-drives/felhom-usb/backups/primary/immich)
[offbox] backup OK: 1 app(s) backed up, 1 snapshot(s), 34s
```
`LastStatus=ok`, `SnapshotCount=1`, no error. The backed-up **src is the USB path** — proving discovery
scanned the storage registry (schedulable path systemDataPath) and found the USB unit. **Pre-fix
(v0.103.0)** this resolved to `systemDataPath` (`/mnt/sys_drive/felhom-data/backups/primary/immich`, which
does not exist) → 0 snapshots reported `ok`. The regression scenario is closed against the real box.
## 4. Phase 4 — NO-SILENT-SUCCESS ✅
- **0/N hard error:** only vaultwarden toggled (no unit). Run →
`LastStatus=error`, `LastError="off-box backup produced no snapshots: 1 app(s) toggled but no recovery
unit was found on any connected drive (missing: vaultwarden)"`, and the operator alert fired:
`[ERROR] [offbox] backup failed after 5s: …``[INFO] Event pushed: backup_failed (error) — Off-box
(NAS) mentés sikertelen`. (Controller-side alert pipeline confirmed via the pushed `backup_failed`
event; downstream hub→Resend email delivery is the hub's leg — operator inbox confirmation is Viktor's.)
- **Partial warning:** immich + audiobookshelf + vaultwarden toggled. Run →
`LastStatus=ok`, 2 backed up (`2 app(s) backed up, 1 skipped (no unit), 2 snapshot(s), 45s`),
`LastWarning="Figyelmeztetés: 1 alkalmazásnak nincs elérhető mentése, ezek kimaradtak: vaultwarden"`,
**rendered on `/backups`** (confirmed in the page HTML, `--warn` style). No false "all good".
## 5. Phase 5 — ROUND-TRIP INTEGRITY ✅
Restore immich via `POST /backup/offbox/restore` → non-destructive to `<DataDir>/offbox-restore/immich`.
Full recovery unit came back (`compose/{app.yaml,.felhom.yml,docker-compose.yml}`, `manifest.json`,
`db-dumps/immich-postgres.sql`). **sha256 of the restored 45 MB db-dump = the USB source, exactly**
(`d78466fcbf595b488ba8b962fcc521f68fbe6f36b8b8e57815a6124bccf4526b`). The box holds genuinely recoverable
data through the real restore endpoint.
## 6. Phase 6 — PERSISTENCE ✅
Controller restarted (bootstrap) → healthy. offbox config (enabled, host/user/repo_path), `LastStatus=ok`,
`SnapshotCount`, `LastWarning`, the per-app toggles, and the 0600 secret files (`ssh_key`, `repo_password`,
`known_hosts`) all survived — settings.json persistence intact.
## 7. Fork resolutions
1. **Topology (fork 1):** **direct — confirmed.** Residential IPv4 → box:23; no ep0 concentrator, no WG.
2. **Account model (fork 2):** **sub-account-per-customer — confirmed.** Fresh console sub-account with its
own `…-subN.your-storagebox.de` hostname, chrooted (base dir presented as `/home`), **RepoPath
convention `/home/<name>`**, host key shared with the main account (one pinned line covers both).
Password-free *intent* holds, but **the console SSH-key field is unreliable (F3)** → the repeatable
install path is `ssh-copy-id -p 23 -s -f` with the sub-account password.
3. **PBS-namespace / WG tier (fork 3):** deferred — restic-to-box is the pilot-era offsite. Not built.
4. **Custody (fork 4) — surface RECORDED, design OPEN.** Per-customer secrets, controller-managed, 0600 in
the controller data dir: **restic repo password** (`data/offbox/repo_password`, 32-byte hex,
auto-generated on first config) + **SSH private key** (`data/offbox/ssh_key`). **Neither is independently
recoverable today** — both ride DR only via the PBS whole-CT snapshot of the guest rootfs. Recovery
ordering: offsite-restic recovery depends on first recovering these from the PBS tier. An escrow/custody
design is the load-bearing open follow-up before any real reliance on this tier.
## 8. Throughput (observed, residential upload)
immich cold backup (45 MB) + repo init = 34 s; a dedup run (immich unchanged) + audiobookshelf cold = 45 s;
a full-dedup run = 44 s. The 45 MB upload itself is fast (~consistent with the spike's ~4.3 MB/s residential
upstream); **`forget --prune` + `snapshots/stats` over SFTP dominate the wall-clock on a small repo** (dedup
backups finish in ~3 s but the run is ~44 s). Fine for a daily schedule; note the first seed of a real
tens-of-GB app is upstream-bound (hours) — heads-up on onboarding.
## 9. Teardown — KEPT (Viktor's decision, 🛑)
Left standing as a demo-offsite (€4.06/mo): the fresh sub-account `u629193-sub1`, the offbox config, and the
box repo `/home/felhom-demo-repo` (immich + audiobookshelf snapshots). Config tidied — the test-only
**vaultwarden** toggle removed and a final clean run left `LastStatus=ok`, 2 snapshots, **no warning**;
toggled = {audiobookshelf, immich}. Scratch removed (the `/tmp` keypair copy — the durable key lives 0600 in
`data/offbox/ssh_key`; the 45 MB verify-restore). Untouched: the main account's other keys, the USB real
units, ep0's `felhom-offsite`/mount.
**⚠️ Standing-reliance caveat:** the scratch restic repo password is now semi-permanent and **NOT
independently recoverable** → any real dependence on this demo-offsite needs the fork-4 custody decision first.
## 10. NEXT (to fully productionize)
- **Fork-4 custody/escrow spec** — the one blocker for real reliance (repo password + SSH key recovery
independent of the PBS whole-CT).
- **Per-customer provisioning runbook** for Peti: sub-account create + key install (`ssh-copy-id -s -f`
until the console key field is fixed / F3) + known_hosts pin + RepoPath `/home/<name>` + offbox config.
- **F3** (Hetzner console sub-account SSH-key field silently not applying) — recurred; work around with
`ssh-copy-id -s -f`, or raise with Hetzner.
- **Tier-2 `TestBackupTier2Restore_DoubleClickRefused` flake** — separate backup-suite test-race debt
(pre-existing; see controller v0.104.0 REPORT Observations).