gates / gates (push) Successful in 8m49s
Closed R-971, R-954, R-1124, R-1012 (174 -> 175 rows); opened R-1139..R-1143; R-895 READY with the design and hub slice 1; R-972 waits on the vouch. [DESIGN] "The catalog allow-list" in 02; the catalog trust line in 01; FELHOM_THINCHECK and unit-forget in 03. Evidence audits/agent-allowlist-rotation-2026-10-17/. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
66 lines
5.0 KiB
Markdown
66 lines
5.0 KiB
Markdown
# 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.
|