# R-895 — the hub counts a box's off-site snapshots itself (option B, updated with the measurement) — 2026-10-17 Baselines: felhom.eu `8f97b800` (hub v0.157.0 code), design origin `audits/night-burndown-2026-10-06/design-R-822.md` option B. Architecture: `07-backup-architecture.md` (the off-site tier; decision 68 — the box-trusted count; decision 69 — the hub writes the pinned key line), `06-offsite-connectivity.md`. ## 1. The question The clean-up window's drop alarm (`EventWindowDrop`) compares counts the BOX reports before and after a window. A box that was broken into, or fakes planted with its add-only key, keep the count level while the real history shrinks. Option B: the hub counts the files in the box's restic `snapshots/` directory itself, over a login of its own. ## 2. The measurement (read-only, demo-hp's repository folder only) **What login would the hub need?** None new. The hub already logs in to every provisioned sub-account once a day with the sub-account PASSWORD (sealed at rest, R-821) for the `authorized_keys` audit (`offsitekeys.Registrar.Audit`, port 23, host key pinned). That login can list `snapshots/`. **It is NOT read-only** — the same password can `rm`, `mv` and rewrite `authorized_keys` (measured 2026-10-03, `audits/offsite-append-only-2026-10-03/live/`). The listing itself only reads (`ls -l`), and adds no capability the hub does not already hold. **Does the provider show upload times?** Yes, by prior measurement on this very sub-account: on 2026-09-01 the password login on demo-hp's sub-account (`u629488-sub3`) listed entries with their modification time to the minute over SFTP (`ls -la` → `Sep 1 12:12`), and the port-23 shell offers `ls|ll`, `stat` (`audits/evidence-drill-r95-recovery-2026-09-01/phase1-find-the-snapshot/01-probe-tree.txt`, `03-shell-capabilities.txt`). A restic snapshot file is written once and never changed, so its mtime is its upload time. **Today's re-measurement from DooPlex: NOT done — and why, with what was tried.** Since decision 69 no box holds the sub-account password; the hub alone holds it, sealed. Tried: (a) CC's secret store (`sec list`) — no Storage Box credential; (b) the box's own key — pinned to `rclone serve restic --append-only`, it reaches only restic's REST API (names and sizes, no times, and it is the box's key, which is exactly what the hub must not rely on); (c) the hub — it has no command or endpoint that lists. Reading the sealed password out of the hub DB to measure by hand was not done: it would put a delete-capable credential of a customer sub-account on DooPlex for a measurement the hub can make itself. **So the measurement ships as code:** hub v0.157.0's daily audit lists `snapshots/` read-only and LOGS the count, the newest and the oldest upload time (`internal/offsitekeys/snapshots.go`). The first daily run after the deploy IS the measurement; compare its count with the box's own (`offsite.snapshot_count` in the host report) — a control from a different channel. ## 3. The design (the alarm slice, after the measurement) 1. **Before/after a window, the hub's own count** (`OpenWindowFor` / `CloseWindowFor` take the listing, the box's numbers logged beside). `EventWindowDrop` fires on the hub's numbers. Red test first (design-R-822 §5): a fake listing 17 → 3 while the box reports 17 → 15 must fire; today it does not. 2. **Files no box run explains:** each box run reports its time (manual runs, catch-ups and crash retries included — they are runs). A snapshot file whose upload time falls outside every reported run (± a slack), or more files than runs, is the injection signature → an operator-only event; the next window is refused while such files exist. 3. **Snapshots that vanished outside a window:** today's list minus yesterday's, when no window was open → an operator-only event. The hub keeps only names and times (custody unchanged, `07` §8a: never contents). ## 4. Costs | | cost | |---|---| | credential | none new; the existing sealed password (write-capable — a fact today, not a new exposure) | | per box | one more SSH login per day (the listing), and the stored list (≈ 70 names × 100 bytes) | | build | slice 1 (this release, ~1 h: list + log); slice 2 (§3.1–3.3, ~1 day with the red tests); a live proof on scratch 9202's sub-account | | what can go wrong | `ls -l` output differing on the provider (slice 1 logs it before anything alarms); a run that does not report its time (false alarm — every run path must report) | **A read-only login would be better, and costs a per-box sub-account:** a second sub-account with `readonly=true` on the same directory (Hetzner API) would let the hub list without holding delete rights for this purpose. It adds one provisioning step and one sealed secret per box. Not built; named as the alternative. ## 5. State R-895 stays OPEN (READY) with this design. Slice 1 (the listing, log-only) is in hub v0.157.0 — deployed only on the operator's yes. Slice 2 (the alarms) needs: the first logged listings on real boxes (the measurement), then about a day.