Files
felhom.eu/REPORT-offsite-append-only-2026-10-03.md
T
admin 9268d9933b
gates / gates (push) Successful in 29s
R-436 measured on the provider: append-only forced key HOLDS (403 on every delete), but the sub-account password defeats it (R-820); design proposal + ep0 options
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
2026-10-03 13:40:35 +02:00

86 lines
6.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.
# 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.