docs: R-200 Part 0 shipped; R-203 recorded (mandatory userdata dir missing from the offsite snapshot while the run says ok)
gates / gates (push) Successful in 9s
gates / gates (push) Successful in 9s
This commit is contained in:
+27
@@ -1672,6 +1672,33 @@ Last updated: 2026-06-13 (v0.60.0 backlog-Medium cleanup)
|
||||
|
||||
---
|
||||
|
||||
## R-203 (2026-08-04) — a MANDATORY userdata directory can be absent from the off-site snapshot while the run says `ok`
|
||||
|
||||
Found live on demo-hp while staging the R-201 drill, and it **halted that drill**.
|
||||
|
||||
`NamespaceRoot(drivePath, inGuestDrive)` (`appbackup/paths.go:28-33`) appends the `felhom-data` segment
|
||||
**when the drive IS the system data path** — `m.namespaceRoot` = `NamespaceRoot(drivePath, drivePath !=
|
||||
m.systemDataPath)` (`backup/backup.go:331`). The deploy-time bind does not: `${USERDATA_PATH}` is
|
||||
`<HDD_PATH>/userdata` (`stacks/classify_binds.go:14`).
|
||||
|
||||
With `system_data_path: /mnt/sys_drive` and `calibre-web` deployed at `HDD_PATH=/mnt/sys_drive`:
|
||||
|
||||
live bind (files land here): /mnt/sys_drive/userdata/media/books ← exists
|
||||
capture set looked for: /mnt/sys_drive/felhom-data/userdata/media/books ← does not
|
||||
|
||||
**The same compose used BOTH roots** — `${IMPORT_PATH}` resolved *with* the segment,
|
||||
`${USERDATA_PATH}` *without*. The run logged one `[WARN] mandatory data path missing on disk, skipped
|
||||
from offsite`, then `0 mandatory path(s)` and **`backup OK: 3 app(s), 3 snapshot(s)`**, with
|
||||
`last_status: ok`. Nothing customer-visible or hub-visible said the directory was dropped.
|
||||
|
||||
**Not established:** whether `HDD_PATH == system_data_path` is a supported deploy. It was accepted
|
||||
(HTTP 202) one call after the NAS path was correctly refused (R-108). **Either branch is a defect** —
|
||||
broken resolution, or a missing refusal.
|
||||
|
||||
**Two things a fix must do:** make the two roots one function, and make a skipped **MANDATORY** path a
|
||||
customer/hub-visible signal rather than a container-log WARN. `opengist`/`privatebin` declare no
|
||||
mandatory userdata paths and are unaffected.
|
||||
|
||||
## R-200 (v0.195.0) — the offsite key recovery diagnostic
|
||||
|
||||
`--recover-offsite-check` is a `docker exec` escape hatch (the `--print-reset-code` shape), NOT a page
|
||||
|
||||
@@ -1,60 +1,42 @@
|
||||
# REPORT — controller v0.195.0: prove the offsite key comes back (2026-08-04, R-200 plumbing half)
|
||||
# REPORT — controller v0.196.0 (R-200 Part 0) + the R-203 finding (2026-08-04)
|
||||
|
||||
**Deployed:** `gitea.dooplex.hu/admin/felhom-controller:0.195.0` on guest 9201 (demo-felhom), `Up (healthy)`.
|
||||
**Requires agent >= v0.125.0** (declared MinAgent) and hub >= v0.94.0.
|
||||
**Deployed:** `felhom-controller:0.196.0` on demo-hp guest 9201 and demo-felhom guest 9201. Requires
|
||||
agent >= v0.125.0.
|
||||
|
||||
## What shipped
|
||||
## Part 0 — `--recover-offsite-install`
|
||||
|
||||
`--recover-offsite-check` — a `docker exec` diagnostic in the shape of `--print-reset-code`. It reads
|
||||
the customer's recovery code from **STDIN**, asks the agent to fetch this host's hub-held sealed bundle
|
||||
and open it, and reports whether the recovered offsite repository password matches the one on disk
|
||||
**by sha256**. Two hashes and a verdict; never a password, never the recovery code, never a blob.
|
||||
The sibling of `--recover-offsite-check`: same fetch → unseal → extract path through the agent, same
|
||||
STDIN discipline for R, but it **places** the recovered repository password via `InjectOffboxPassword`
|
||||
so a rebuilt box reopens the off-site history it inherited.
|
||||
|
||||
- `internal/backup/offbox_recovery_check.go` — `CheckOffsiteKeyRecoverable`, hashes only.
|
||||
- `internal/backup/offbox_recovery_cli.go` — the entry point, R from stdin.
|
||||
- `internal/agentapi/escrow.go` — `RecoverOffsiteRepoPassword` (the only place R crosses outbound).
|
||||
- `cmd/controller/main.go` — the flag, alongside `--print-reset-code`.
|
||||
- **The confirmation is a second invocation.** Without `--confirm-install` both hashes print and
|
||||
nothing is written; the operator sees the comparison before a write is possible.
|
||||
- **Three outcomes, named distinctly:** *installed* (no local password — the rebuilt-box shape),
|
||||
*unchanged* (identical key present, nothing written), *refused* (a DIFFERENT key present — exit 2,
|
||||
no force offered, because which history to keep is not this command's decision).
|
||||
- It re-reads the file after writing rather than trusting the call's return.
|
||||
|
||||
**R from stdin and not a flag** because a flag value is visible in `ps`, in shell history, in a
|
||||
container's command line and in any session transcript. **It compares and never installs** (§8.5):
|
||||
the recovered password is not written to `offbox/repo_password`, and link 9 — placing it so the
|
||||
existing repository opens — is next session's work, with the drill around it.
|
||||
`go build && go vet && go test ./...` rc=0; `controller_gates.py --fast` OK.
|
||||
**Red-proof observed:** removing the confirmation gate makes the dry run write the password.
|
||||
The R-persistence test carries a **positive control** (a planted copy found, then removed and not
|
||||
found) because an absence check is worth only what its sensitivity is.
|
||||
|
||||
## THE RESULT
|
||||
**NOT exercised against a live recovery** — the R-201 drill halted before step 9. Unit-proven only.
|
||||
|
||||
On demo-felhom, 2026-08-04 13:49 CEST:
|
||||
## The R-203 finding — see CONTEXT.md
|
||||
|
||||
```
|
||||
on-disk sha256: c60c8bc737a6b7c6647c7849283f52087f650a885babedb4ef5fdf9a5c9543cb
|
||||
recovered sha256: c60c8bc737a6b7c6647c7849283f52087f650a885babedb4ef5fdf9a5c9543cb
|
||||
[MATCH] the offsite repository password IS recoverable from the sealed escrow.
|
||||
```
|
||||
A customer-declared **mandatory** data directory was silently absent from the off-site snapshot while
|
||||
the run reported `ok`, because the deploy-time `${USERDATA_PATH}` root and the backup-time
|
||||
`NamespaceRoot` disagree for an app on the system drive. It halted the drill: the sentinel file was not
|
||||
in the snapshot, so a wipe would have destroyed it and proven nothing.
|
||||
|
||||
Both equal the hash the hub independently stores as `restic_pw_sha256` — three sources agree.
|
||||
Confirmed non-write: `offbox/repo_password` mtime is still `2026-08-03 07:18:02` afterwards.
|
||||
Full record: `felhom.eu/documentation/audits/DRILL-r201-offsite-recovery-2026-08-04.md`.
|
||||
|
||||
Five minutes earlier the same path with a deliberately wrong code returned
|
||||
`[FAIL] … the recovery code did not open the sealed bundle … nothing was written`, exit 1.
|
||||
## Observations
|
||||
|
||||
## Tests
|
||||
|
||||
`go build ./... && go vet ./... && go test ./...` — rc=0; `controller_gates.py --fast` — all OK.
|
||||
New: `offbox_recovery_check_test.go` — match/mismatch verdicts, the no-local-password shape, R passed
|
||||
through verbatim, agent-failure-is-not-a-mismatch, and the four exit codes with a check that no
|
||||
printed stream carries a secret.
|
||||
|
||||
**Red-proof observed:** adding `InjectOffboxPassword` to the check fails
|
||||
`TestCheckOffsiteKeyRecoverable_WritesNothing` with *"the check INSTALLED the recovered password"*.
|
||||
|
||||
## Not in this release
|
||||
|
||||
No card, no form, no preview, no customer-facing text — building an interface on a chain nobody had
|
||||
walked is how the preceding three weeks went wrong. The customer-facing shape the operator ruled on
|
||||
(yell → recovery-code form → preview → proceed) is R-200's remaining half, now priced against a chain
|
||||
that exists. **R-201 stays open:** no recovered password has been installed, no repository reopened,
|
||||
no file restored.
|
||||
|
||||
## Observation
|
||||
|
||||
The controller binary is at `/usr/local/bin/felhom-controller`, not `/app/…` — a guessed path cost the
|
||||
first diagnostic invocation. Worth a line in the runbook when one is written.
|
||||
- **The CSRF form field is `_csrf`, not `csrf_token`** — the login page uses one name and the protected
|
||||
forms another. Cost real time; worth a line in the headless-access memory.
|
||||
- **`/api/stacks/<name>/deploy` accepted `HDD_PATH=/mnt/sys_drive`** one call after refusing a NAS path
|
||||
with a clear error. If the system drive is not a valid app namespace, that refusal is missing.
|
||||
- **demo-hp has no registered drive usable as an app namespace** (its only drive is a NAS), which is
|
||||
why the drill's file-leg app had to go on the system drive at all.
|
||||
|
||||
Reference in New Issue
Block a user