# REPORT — off-site backup safety, step 1: the append-only lock measured on the provider (2026-10-03) A spike. No product code changed, no release. Evidence, exit test and design: `documentation/audits/offsite-append-only-2026-10-03/`. Architecture read: `07-backup-architecture.md` §8a, threat row 10, §D; `06-offsite-connectivity.md` (PBS/tunnel only — it does not describe the restic tier, so the facts went to `07` §D). Baselines (re-verified): felhom.eu `f4c5466`, controller `0945332` (v0.288.0), register 326 rows, highest id R-819. ## The Part table | Part | done / not done / changed | why | |---|---|---| | 0 — venue | **changed** — `u629488-sub4` (tester-1) instead of a new scratch customer | operator ruled "use Tester1" in-session. tester-1's box was deleted 2026-09-30; nothing writes there. Credential: the hub's stored tester-1 value, read from a copy of the hub DB on a second operator ruling (copy deleted, value never printed or written to a committed file). A new repo dir `spike-r436` only; `felhom-repo` never read or written | | A — the lock | **done**, exit test written first (`EXIT-TEST.md`) | E1–E8 and C1–C2 as stated; locks measured | | B — the attacker | **done**, one item lab-only | raw-HTTP path escape through the pinned server measured in the lab only — a live HTTP/2 bridge over the forced ssh could not be made to work in the time box | | C — design | **done** — `DESIGN.md`, STATUS decision 0 | | | D — ep0 | **done** — `PART-D-ep0-safeguard.md`, STATUS decision 0b | read only; ep0 not touched | | E — records | **done** | below | ## Claims in the brief (and the register) that turned out wrong 1. **"The box holds no sub-account password"** — it does not STORE one, but it can **obtain it at will**: declare `needs_credential` twice → the hub re-arms the stored value → the box consumes it (R-820). 2. **"A forced command cannot be bypassed by the sub-account itself"** — the pinned key cannot; the **password can** (logs in on ports 22 and 23, rewrote `authorized_keys` this session). 3. **"The hub cannot prune because of custody"** — true for *pruning*; but the hub can **delete**: it holds every sub-account password in the clear (R-821). 4. **"Both `forget` sites must change together"** — there are **four** deleting features on the box: both `forget` sites, the orphan move-aside (`mv`) and the abandonment (`rm -rf`). 5. **rclone in the image** (R-436 row: "rclone is not in the controller image today", implying it is needed) — **not needed**; restic 0.14.0 with `-o rclone.program="ssh … rclone"` is enough. 6. **R-342's first candidate, a Hetzner Volume snapshot** — does not exist. 7. **R-430's model** (a locks dir where deletion is refused) — does not describe this transport; the append-only server allows lock deletion and `unlock --remove-all` works. 8. **The vendor's cited blog** (`fluix.one`) shows the line WITHOUT `--append-only`; only Hetzner's ticket reply adds it. Copying the blog would give a deleting key. 9. Held: restic is **0.14.0** (`0.14.0-1+b5`); **restore works through the add-only key**. ## Part A — results (verbatim refusal) `blob not removed, server response: 403 Forbidden (403)` for `forget d807418c --prune`, `forget --keep-last 1` and a real `prune` (each ~45–48 s of retries, rc=1); snapshot count unchanged; control key: `1 / 1 files deleted`. Crash lock: blocks `check`, not `backup`; plain `unlock` prints success and removes nothing; `--remove-all` removes it. Files: `live/E1-E3…`, `live/E4-E6…`, `live/C2-A5…`. ## Part B — the attacker table | Route | Tried how | Result | What closes it | |---|---|---|---| | Password, port 23 | `sshpass ssh -p 23` | **logs in**; `authorized_keys` read and **rewritten** | box never receives it (hub = key registrar) | | Password, port 22 | `sshpass sftp -P 22` | **logs in** (SFTP), `.ssh` listed | same | | Box obtains the password | source read | **yes, at will** (self-heal re-arm + consume) | same — R-820 | | Pinned key: shell / `rm -rf` | `ssh … 'ls'`, `'rm -rf spike-r436'` | runs the forced rclone; repo intact | — (holds) | | Pinned key: sftp / scp / rsync | each | refused / protocol error | — (holds) | | Pinned key: port forward | `-L`, then connect | `administratively prohibited` | — (holds) | | Pinned key: other path, no flag | `rclone serve restic --stdio felhom-repo` | pinned dir served, append-only | — (holds) | | Pinned key: `../` escapes, overwrite | raw HTTP (lab) | 400 / 403 | — (holds; lab rclone) | | Pinned key: add junk / new `keys/` | raw HTTP (lab) | allowed | quota fills — R-431/quota alarms | | Pinned key: future-dated snapshots | restic (lab) | allowed → retention erases real history | poisoning guard — R-822 | | Any key on port 22 | both test keys | refused (port 22 takes no OpenSSH key) | — | | Hetzner API / panel | box code read | nothing on the box reaches either | — | | Hub DB | operator-tier | every sub-account password in clear | R-821 | **A route defeats the lock: the password (R-820).** The lock alone is not protection until it is closed. ## Records - **Closed:** R-436 (measured; the 2026-10-06 due-check is cleared — the block is now empty), R-430. - **Opened:** R-820 (P2, Security), R-821 (P2, Security), R-822 (P2, Backup). None is P1 by the scale: today the box's own key can already delete (R-95), so none adds harm *today*. - **Updated:** R-95 (the measurement, the four sites, the proposal; rank untouched), R-342 (options costed). - **Register: 326 → 327** (`register_shape_gate`). All felhom.eu gates green. - `07` §D: one `[FACT]` block. STATUS: two decisions in the operator's format. - `unproven.py --summary`: NOT WALKED 35 of 55 — unchanged. ## Teardown - **Provider:** `authorized_keys` restored — sha256 `795e7153…` before and after, identical; `spike-r436` removed; `~/.config/rclone/` (created by the provider's rclone during the test) removed; home is back to `.ssh`, `felhom-repo`. Both test keys refused afterwards. (`live/TEARDOWN.txt`) - **DooPlex:** lab container, network and image removed; test keys, the password file, the hub DB copy and hub page copies deleted from the scratchpad. - **Hub:** nothing changed (two reads). - **Left as is, on purpose:** the tester-1 sub-account password was NOT rotated — the next tester-1 install needs the stored value. R-821 covers why that is itself a risk.