Hub v0.158.0 code (built, NOT deployed): agent-only vouch, R-895 snapshot alarms, the hub's own 90-day access log (R-1140 A); installer --rotate-recovery-only (R-1141); 02/03/05/09 + break-glass
gates / gates (push) Successful in 6m45s
gates / gates (push) Successful in 6m45s
Hub: the R-120 golden guard judges only a CHANGED golden selection; offsite_snapshots_vanished / _unexplained (operator-only, hub-origin) on the 07:10 listing; internal/accesslog writes path-only lines to /data/access-log, 90 days. Installer: --rotate-recovery-only runs step 4b alone (rides the next installer tag). 03: the orphan check runs after a failed backup (agent 6545ccc). 02: the allow-list is empty, appdata_folders (controller v0.329.0). All red-proofed.
This commit is contained in:
1 parent
5a5486104a
commit
7d64038f02
27 files changed
+1813
-77
No files matched your search
@@ -738,7 +738,8 @@ this app's service has EXACTLY that setting on the allow-list: `privileged`, `ca
|
||||
`include`, ANY `extends`, a `provider` service, `post_start`/`pre_stop` hooks, an `env_file`/`configs`/`secrets` file
|
||||
outside the stack dir, a named volume with `driver`/`driver_opts`/`external`/`name`, a `${VAR}` in any of those keys,
|
||||
and binds: the Docker socket anywhere, mount propagation, a `${VAR}` in a bind's options, and any source outside the
|
||||
data roots the box itself gives — `${HDD_PATH}/appdata/`, `${USERDATA_PATH}`, `${IMPORT_PATH}` (a bind source may use
|
||||
data roots the box itself gives — `${HDD_PATH}/appdata/<the app's own folder>/` (v0.329.0, R-1142), `${USERDATA_PATH}`,
|
||||
`${IMPORT_PATH}` (a bind source may use
|
||||
only those variables, as its leading token; each root must lie under `/mnt/felhom-drives/` or the system data path; on
|
||||
the box a source is judged where it really points, through its nearest existing ancestor, so a symlink cannot carry it
|
||||
out).
|
||||
@@ -747,9 +748,17 @@ out).
|
||||
Gitea — the threat R-971 names (whoever can push to the catalog can take every guest) — cannot add to it; a new entry
|
||||
needs a controller release. An entry is app + service + exact setting, and holds only while that service runs the
|
||||
entry's image REPOSITORY (a literal reference), with the image's own entrypoint and command, and the exact
|
||||
`healthcheck.test` the entry names — so a push cannot keep the allowed right and change what runs with it. Today: one
|
||||
entry, homepage's read-only Docker socket (the survey of all 123 catalog files found nothing else,
|
||||
`audits/agent-allowlist-rotation-2026-10-17/P2/survey.md`). The catalog's CI gate `check-privileges.py` mirrors the
|
||||
`healthcheck.test` the entry names — so a push cannot keep the allowed right and change what runs with it. **Since
|
||||
v0.329.0 the list has NO entry** (R-981): homepage's read-only Docker socket — the one entry of v0.328.0 — served no
|
||||
Felhom feature (its rendered `docker.yaml` is empty, nothing carries `homepage.*` labels; measured on demo-hp,
|
||||
`audits/night-2026-10-18/P1/`), so the template dropped it and so did the list. The entry-binding rules stay pinned on a
|
||||
test list, so a future entry is held to them. **The app's own folder (R-1142):** the list's `appdata_folders` map names
|
||||
an app's folder under `${HDD_PATH}/appdata/` when it is not the app's name — one entry, `paperless-ngx → paperless` (the
|
||||
survey of every template, the drill catalog and the three test boxes found no other). It is compiled in with the list,
|
||||
because the catalog is the threat and must not name the folder; an app NAMED like a folder the map gives to another app
|
||||
gets no `HDD_PATH` root, and one folder for two apps refuses to load. `USERDATA_PATH` is always the controller's own
|
||||
derivation from `HDD_PATH`, never the app's env — a deploy field a catalog push adds would otherwise name any folder
|
||||
(both found by the security review of the pushed commit, 2026-10-18). The catalog's CI gate `check-privileges.py` mirrors the
|
||||
check with a byte-identical copy of the list (pinned from both sides); the BOX is the authority.
|
||||
|
||||
**What a refusal does.** Nothing is written. A refused correction or update leaves the app on its last good compose,
|
||||
@@ -760,8 +769,8 @@ that cannot read the compose refuses. The sync never copies a catalog template n
|
||||
|
||||
**Out of scope, stated.** The controller's own infra stacks (traefik, cloudflared, filebrowser, samba) and its override
|
||||
files (setup gate, family gate, install hold, media library) are rendered by the controller, not the catalog. Published
|
||||
host ports are not a class (crafty-controller's game ports). Known limit: under `${HDD_PATH}/appdata/` one app can
|
||||
name another app's folder (a per-app rule needs a name map — paperless-ngx keeps `appdata/paperless`), R-1142.
|
||||
host ports are not a class (crafty-controller's game ports). `${USERDATA_PATH}` and `${IMPORT_PATH}` are shared by design:
|
||||
they are the household's own folders, which every media app reads.
|
||||
|
||||
**Pinned by** `internal/privpolicy/privpolicy_test.go` (each class refused, with positive controls),
|
||||
`stacks.TestAllowList_IsWiredIntoEveryComposeWriter` (AST: every compose writer and every env decision point asks
|
||||
|
||||
@@ -113,7 +113,7 @@ fixed file, delivered by the signed config bundle):
|
||||
| `FELHOM_CONTROLLERSWAP` | the managed controller update | exact vmid; image ref pinned to `gitea.dooplex.hu/admin/felhom-controller:X.Y.Z` for the image check; the inspect template stays free text; **the write goes through `felhom-priv-apply controller-image <vmid>`** (the ref on stdin, re-checked as root against the same pattern — R-861 (a) A1, `09` §3 decision 165, agent after v0.150.0) | guest-scoped: a compromised agent can restart the guest's bootstrap and choose an OLDER version of OUR controller image (a downgrade); it can no longer hand the guest any other image. Until the A1 bundle reached a box, the agent's in-guest `tee` let it write any ref (sudo cannot see stdin — the 2026-10-06 night correction, `audits/night-burndown-2026-10-06/design-R-861.md`) |
|
||||
| `FELHOM_STALELOCK` / `FELHOM_SCRATCH_TEARDOWN` | stale-lock clear; failed restore-test scratch | exact vmid; the scratch band `99000[0-9]` was already exact | — |
|
||||
| `FELHOM_VZPARTIAL` | remove a dead half-written vzdump (R-1006, agent v0.156.1) | ONE line: `felhom-vzdump-partial-clean <storage> <vmid> <half-written name of that vmid>`; the root helper (a config-bundle file) re-checks: a `dir` storage from `storage.cfg`, the name of that vmid (`.tar.dat`, `.vma.dat`, `.tmp`), not a symlink, directly in `<path>/dump`, no vzdump on the host, nothing holds it open, dead (before this boot or quiet 120 s) | a compromised agent can delete a dead partial — never a finished archive, never a live one |
|
||||
| `FELHOM_THINCHECK` | name an ORPHAN thin device around a whole-guest backup (R-1012, agent v0.159.0) | ONE line: `felhom-thin-check <vg> <pool>`; the root helper (a config-bundle file) lists LVM's thin ids before and after and the pool's ids through a metadata snapshot it reserves and releases; it refuses (exit 3) when someone else holds a metadata snapshot and sends no other pool message. The agent reports an id in the pool and in neither LVM list (two reads) in the backup failure's text; the repair (`runbooks/whole-guest-backup-orphan-thin.md`) stays human | a compromised agent can READ the pool's device ids — it cannot delete, create or change a thin device |
|
||||
| `FELHOM_THINCHECK` | name an ORPHAN thin device when a whole-guest backup has failed (R-1012, agent v0.159.0) | ONE line: `felhom-thin-check <vg> <pool>`; the root helper (a config-bundle file) lists LVM's thin ids before and after and the pool's ids through a metadata snapshot it reserves and releases; it refuses (exit 3) when someone else holds a metadata snapshot and sends no other pool message. The agent runs it ONLY when a snapshot-mode vzdump has failed (start or task) — the moment the answer is needed; a successful backup never takes the pool's metadata snapshot (v0.159.0/v0.159.1 also ran it before every snapshot backup; the next agent release after v0.159.1 drops that pre-run check). It reports an id in the pool and in neither LVM list (two reads) in the backup failure's text; the repair (`runbooks/whole-guest-backup-orphan-thin.md`) stays human | a compromised agent can READ the pool's device ids — it cannot delete, create or change a thin device |
|
||||
| `FELHOM_MOUNT` `unit-forget` | forget a decommissioned drive that is gone (R-1124, agent v0.159.0) | `felhom-priv-apply unit-forget mnt-<…>.mount` removes ONE unit file, only a felhom by-UUID drive unit (not a network unit) that is disabled, not mounted, and whose `/dev/disk/by-uuid` link is absent; the agent calls it at start and on the 20-s tick for drives whose intent is `decommissioned` | a compromised agent can delete the unit of an ABSENT, unmounted drive — the same effect as unplugging it; a present drive keeps its unit |
|
||||
| `FELHOM_WG` | the off-site tunnel | conf via `felhom-priv-apply wg` (only the keys `renderConf` writes; no `PostUp`/`PreUp`/`DNS`/`Table`; `/32` only) | — |
|
||||
| `FELHOM_SELFUPDATE` | commit / rollback of the A/B flip | **`apply` removed**: the flip runs only inside `felhom-os-apply agent_update`, after the operator signature, host, window and nonce are checked as root and the staged bytes are hashed ONCE and copied to a root-owned dir (`/var/lib/felhom-os-apply/agent-update/`); the wrapper accepts only that dir | — |
|
||||
|
||||
@@ -132,7 +132,9 @@ touches WAF.
|
||||
the floor carries it to every box (MinAgent declared), raised after the release ran on the three test boxes and the box
|
||||
walk is green; the golden is baked weekly and vouched by the operator; a new box installs the vouched golden and updates
|
||||
itself to the floor (R-1052: not yet proven end to end). Setting the global floor today needs the operator page with the
|
||||
operator's password — there is no scoped route for CC (R-1051).
|
||||
operator's password — there is no scoped route for CC (R-1051). **An agent can be vouched mid-week; the golden stays
|
||||
weekly** (hub v0.158.0): the R-120 golden guard judges only a CHANGED golden selection, so a Day-0 save that keeps the
|
||||
vouched golden (same version and sha) saves a new agent even while that golden is behind the fleet.
|
||||
|
||||
**The controller floor and its agent requirement (hub v0.112.0, R-472).** The managed controller floor
|
||||
rides the report ACK. `store.ResolveManagedFloor` decides per box whether to serve it, from three
|
||||
@@ -505,6 +507,21 @@ namespace (`runbooks/RUNBOOK-hub-db-offsite-backup.md`). Pinned by `internal/dbs
|
||||
`cmd/hub/r173_wiring_test.go`.
|
||||
|
||||
|
||||
### 16.4 The hub's own access log [DESIGN — hub v0.158.0, operator decision A on R-1140, 2026-10-17 22:26]
|
||||
|
||||
**Why:** R-954 asked „did anyone use the old hub key before 2026-10-12?" and nobody could answer — the ingress
|
||||
controller's container log covers about half a day (k3s keeps 10 MiB × 5) and the hub logged no requests. **What is
|
||||
kept:** one line per request the hub serves — time (UTC), method, the PATH (the query string is dropped; the self-bind
|
||||
capability in `/bind/<token>` and any path segment of 32+ characters are written as `[redacted]`), status, duration,
|
||||
the socket peer, the X-Forwarded-For hops that parse as IP addresses (recorded, never trusted), the KIND of credential
|
||||
presented and the identity kind the hub's auth established (customer, host, global, session, basic, token) — never a
|
||||
key, token, password, cookie or passphrase value (`TestAccessLog_NeverWritesASecret`). kube-probe's own `/healthz`
|
||||
is not logged. **Where and how long:** `<data_dir>/access-log/access-YYYY-MM-DD.log` on the hub's own volume (`/data`,
|
||||
the `hub-data` PVC; no new env var, no manifest change), one file per UTC day, mode 0600; files older than **90 days**
|
||||
are deleted at start-up and at each new day (`TestAccessLog_Keeps90DaysThenDeletes`). Measured 2026-10-17 from the
|
||||
ingress log: ≈ 1,900 hub requests a day at ≈ 110 bytes a line → ≈ 0.2 MB a day, ≈ 19 MB for 90 days. A logging
|
||||
failure never fails or delays a request: lines go through a non-blocking buffer to one writer (`internal/accesslog`).
|
||||
|
||||
## 17. Operator access tokens [DESIGN — hub v0.152.0, operator ruling 2026-10-15 11:38 (`09` §3 decision 251)]
|
||||
|
||||
**The problem.** Scripts — Claude Code's floor step first — drove the hub with the operator's own password (HTTP Basic,
|
||||
|
||||
@@ -969,9 +969,13 @@ its length, and both fixes cost something the household would notice — operato
|
||||
*Route:* the floor save on the operator page, with CC's scoped token (decision 251, `05` §17); until the token
|
||||
works live, the operator password for the floor only (R-1051).
|
||||
3. **Agent releases (and their bundles) still stop for the operator's vouch;** a floor whose MinAgent is above the
|
||||
vouched agent is never set.
|
||||
vouched agent is never set. **An agent can be vouched mid-week:** a Day-0 save that leaves the golden as vouched
|
||||
(same version and sha) is not refused by the R-120 golden guard even while the golden is behind the fleet (hub
|
||||
v0.158.0, `TestVouchAgentOnlyWithStaleGoldenIsSaved`).
|
||||
4. **The golden is baked weekly** (Sunday night, `runbooks/nightly-rotation.md`); the operator vouches it. A new box
|
||||
installs the vouched golden and updates itself to the floor (not yet proven end to end — R-1052).
|
||||
installs the vouched golden and updates itself to the floor (not yet proven end to end — R-1052). **The golden
|
||||
stays weekly:** a CHANGED golden selection is still refused when it is older than the fleet's newest controller
|
||||
(`TestVouchGoldenChangedToOlderIsStillRefused`).
|
||||
5. The push gate judges the golden by its AGE (older than 7 days while the floor is above it), not by the newest
|
||||
CHANGELOG heading (`scripts/golden_currency_gate.py`).
|
||||
251. **Scripts use scoped hub tokens, not the operator password; CC's floor step uses its own.** *Operator ruling
|
||||
|
||||
@@ -86,16 +86,25 @@ Fix the cause; the three layers are the safety net, not the cure.
|
||||
- **Keep the build-server PVE token fresh.** In the spike incident the build-server-cached PVE token
|
||||
(`kisfenyo@192.168.0.180:~/felhom-agent-demo.json`) was **stale (401)** and left no non-SSH lever
|
||||
before the web-console path was used. A working break-glass path independent of sshd must always exist.
|
||||
- **Rotate the console credential** with `felhom-host-install … --rotate-recovery` (regenerates +
|
||||
re-vaults; a plain re-run does NOT rotate, to avoid stranding your saved copy). Full auto-rotation is
|
||||
a future item.
|
||||
- **Rotate the console credential ALONE** with `sudo bash felhom-host-install.sh --rotate-recovery-only`
|
||||
(R-1141; try it first with `--dry-run`). It runs step 4b and nothing else: a new random `root@pam`
|
||||
password, set with `chpasswd` and re-vaulted to the hub with the host key from the agent's own config
|
||||
(`/etc/felhom-agent/agent.json`). It does NOT reinstall or change the agent, its config or the root
|
||||
files, needs no passphrase or customer id, checks that the hub answers (`/healthz` 200) before the
|
||||
password changes, and refuses a byo host (also an agent config whose `deployment_mode` is not
|
||||
`appliance`). Proof after a rotation: the new vaulted value gets a PVE ticket (200), the old one
|
||||
gets 401. The flag ships with the first `installer-v…` tag after installer 1.33.0 — until then the
|
||||
served installer does not know it. The older `--rotate-recovery` flag only works inside a FULL
|
||||
installer re-run, which also reinstalls the vouched agent and the root files (on a box ahead of the
|
||||
vouched agent it moves the agent back) — do not use it for a password change. A plain re-run does NOT
|
||||
rotate, to avoid stranding your saved copy. Full auto-rotation is a future item.
|
||||
- **Operator-set FIXED credential (e.g. demo-felhom-01, pinned 2026-07-12).** A host's `root@pam` may
|
||||
be deliberately set to a *known operator-chosen* password instead of the random day-0 one. Do it the
|
||||
same way step 4b does, just with your password: set it on the box (`printf 'root:<pw>\n' | chpasswd`),
|
||||
then re-vault with `PUT /api/v1/hosts/<host_id>/recovery-credential` (host `api_key` from
|
||||
`/etc/felhom-agent/agent.json`; expect HTTP 200), and ensure `break_glass` is in
|
||||
`/var/lib/felhom-install/state.json`'s `completed[]`. This **survives plain installer re-runs**
|
||||
(step 4b skips on the marker). Two caveats: (a) **never `--rotate-recovery` such a host** — it
|
||||
(step 4b skips on the marker). Two caveats: (a) **never `--rotate-recovery` or `--rotate-recovery-only` such a host** — it
|
||||
replaces your known copy with a random one; (b) a **full from-scratch reinstall** (uninstall→install)
|
||||
wipes `state.json`, so step 4b re-randomizes — re-run the set-and-vault afterward. Background: on each
|
||||
full reinstall this is exactly why `root@pam` "kept changing" until it was pinned.
|
||||
|
||||
Reference in new issue
Block a user