9268d9933b
gates / gates (push) Successful in 29s
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
86 lines
6.3 KiB
Markdown
86 lines
6.3 KiB
Markdown
# 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.
|