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

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:
2026-10-17 23:31:34 +02:00
parent 5a5486104a
commit 7d64038f02
27 changed files with 1813 additions and 77 deletions
@@ -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