Spike, no product change. Venue u629488-sub4 (tester-1, operator ruling); scratch repo removed, authorized_keys restored byte-identical. Closed R-436 (due-check cleared), R-430. Opened R-820, R-821, R-822. R-95 and R-342 updated. 07 §D [FACT] block. STATUS: two operator decisions. Register 326 -> 327. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
6.3 KiB
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
- "The box holds no sub-account password" — it does not STORE one, but it can obtain it at will:
declare
needs_credentialtwice → the hub re-arms the stored value → the box consumes it (R-820). - "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_keysthis session). - "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).
- "Both
forgetsites must change together" — there are four deleting features on the box: bothforgetsites, the orphan move-aside (mv) and the abandonment (rm -rf). - 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. - R-342's first candidate, a Hetzner Volume snapshot — does not exist.
- 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-allworks. - 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. - 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_keysrestored — sha256795e7153…before and after, identical;spike-r436removed;~/.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.