ed8b10330c
gates / gates (push) Successful in 2m44s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
69 lines
6.2 KiB
Markdown
69 lines
6.2 KiB
Markdown
# R-822 — fake snapshots steering the off-site retention — design proposal (burn-down night 2026-10-06, no code)
|
||
|
||
Baselines read: felhom.eu `8e2dc204`, felhom-controller `5e7522e023` (v0.301.0), hub v0.140.0. Architecture:
|
||
`07-backup-architecture.md` §246 (off-site deletion custody, decisions 68–69) and threat row 10 (§1298);
|
||
`audits/offsite-append-only-2026-10-03/DESIGN.md` §3 Option 1.
|
||
|
||
## 1. The problem
|
||
The box's off-site key can only ADD (decision 69). Someone holding that key can add snapshots with any date and make the
|
||
box's own honest pruner delete the real ones. Measured in the lab 2026-10-03 (restic 0.14.0, rclone `--append-only`):
|
||
13 empty future-dated snapshots made the policy `--keep-daily 7 --keep-weekly 4 --keep-monthly 6` select all 3 real
|
||
snapshots (`audits/offsite-append-only-2026-10-03/lab/C3-retention-poisoning.txt`). The guard (controller v0.289.0,
|
||
tuned v0.290.0 and v0.294.0) refuses that shape. **What is left (read in source tonight, not measured):** a fake dated
|
||
in the PAST, inside a week or month that already holds a real keep, and newer than that real snapshot, wins the bucket.
|
||
The real snapshot then falls out of the plan as an ordinary old removal. The guard cannot tell it from honest retention.
|
||
|
||
## 2. What the code does today (read in source)
|
||
- The guard runs on the box before any `forget`, inside a hub window (`felhom-controller/controller/internal/backup/offbox_window.go:21-38`, `offsiteGuard` `:131-158`). It refuses: a future date (`:133-135`); a date after the window opened (`:136-138`); a plan that removes a snapshot younger than 7 calendar days and not superseded the same day (`:141-149`); a plan above `MaxRemove` (`:152-154`).
|
||
- A YOUNG real snapshot superseded the same day by a newer one is only EXCLUDED, then removed in a later window once old (`:142-145`, R-824). So a same-day fake, planted after the night run, also wins the daily bucket — later.
|
||
- `MaxRemove` = half of the count, at least 5 (`felhom.eu/hub/internal/offsitekeys/service.go:240-246`). So the old real history goes in about two windows.
|
||
- The hub's after-window check compares counts the BOX reports: `countBefore` comes with the box's request (`service.go:284`), `CountAfter` with its close report (`service.go:343`). Fakes added between windows keep the count level, so the drop alarm (`EventWindowDrop`, `:350`) does not fire.
|
||
- **The same exposure already exists without any fake:** during a window the box's own key may delete (`service.go:303`, the deleting line). A box that is broken into at window time can delete everything and report any counts. Decision 68 accepted that, with the count check as the backstop.
|
||
|
||
**Result, inferred from the above:** an attacker who held the box's key once can, over about two weekly windows, shrink
|
||
the real off-site history to the last 7 days. Nobody is alarmed. It needs the attacker to plant fakes; it does not need
|
||
the attacker to still be there.
|
||
|
||
## 3. Options
|
||
**A. Accept the residual and close the row.** Record it in `07` §246 as a stated limit of decision 68.
|
||
- Costs: nothing to build.
|
||
- Can go wrong: the slow, silent loss above. It is the same class of loss decision 68 already accepts for a broken-into
|
||
box in a window (box-reported counts), so A adds no NEW weakness — it names one.
|
||
|
||
**B. The hub keeps its own snapshot list (no repository password needed).** The hub already logs in to each sub-account
|
||
daily (the `authorized_keys` audit). It also lists `<repo>/snapshots/`: file names (= snapshot ids) and their upload
|
||
times on the provider. The hub then: (1) counts before and after each window ITSELF, not from the box; (2) alarms when
|
||
snapshot files appear that no box run explains (a run reports its time; a file uploaded outside a run, or more files
|
||
than runs, is the injection signature); (3) refuses to open a window while such files exist.
|
||
- Costs: hub only, about one day. One more SFTP listing per customer per day. Custody unchanged (07 §8a): the hub sees
|
||
file names and sizes, never contents.
|
||
- Can go wrong: a manual run, a catch-up run or a crash retry must be counted as "a run", or false alarms. Not measured:
|
||
whether the provider's restricted shell gives upload times (`ls -l` on port 23) — measure on the scratch sub-account.
|
||
- It also closes the box-trusted count in decision 68's backstop — worth having on its own.
|
||
|
||
**C. A stricter box guard (refuse when a keep is empty or tiny).** Rejected: a fake can point at a real snapshot's tree
|
||
with another date, at no cost. The guard runs on the box that the attacker held.
|
||
|
||
## 4. The pick — PROPOSAL for the operator, not a decision
|
||
**A now, B as the next build.** Close R-822 as a stated, bounded limit of decision 68 (the loss is the last-7-days
|
||
floor, at ≤ half the snapshots per weekly window), and file B as its own row: "the hub trusts the box's own count before
|
||
and after a window". B is the real defence for both the fake-snapshot case and the broken-into-box-in-a-window case. It
|
||
needs a measurement first, so it is not tonight's work.
|
||
|
||
## 5. First slice and its proof (for B)
|
||
- Measure first, on the scratch sub-account: `ls -l <repo>/snapshots/` through the password login on port 23 — does it
|
||
list names and upload times? (Read only.)
|
||
- Build: hub `offsitekeys` gets `ListSnapshots(ctx, target, pw) ([]SnapshotFile, error)`; `OpenWindowFor` and
|
||
`CloseWindowFor` take the counts from it, not from the box (the box's numbers are logged beside).
|
||
- Red test first (must FAIL today): a fake registrar whose listing holds 17 files before and 3 after; the box reports
|
||
17 → 15. Assert `EventWindowDrop` fires. Today it does not (the hub uses the box's 15).
|
||
- Live proof on scratch 9202's sub-account: open a one-shot window; the hub's own before/after counts appear in its
|
||
log and match `restic snapshots` run on the box. Control from a different channel: the provider's listing read by
|
||
hand over SFTP.
|
||
|
||
## 6. Open questions for the operator
|
||
1. Accept the residual (an attacker who once held a box's key can shrink its off-site history to 7 days over about two
|
||
weeks, silently) and close R-822? If you do nothing: the row stays open; nothing changes on the boxes.
|
||
2. Build B (the hub counts the snapshots itself, about a day of work, after one read-only measurement)? If you do
|
||
nothing: the window's alarm keeps trusting the numbers the box sends.
|