Agent 0.159.1, the catalog allow-list (controller 0.328.0), break-glass rotation: report, register, design
gates / gates (push) Successful in 8m49s
gates / gates (push) Successful in 8m49s
Closed R-971, R-954, R-1124, R-1012 (174 -> 175 rows); opened R-1139..R-1143; R-895 READY with the design and hub slice 1; R-972 waits on the vouch. [DESIGN] "The catalog allow-list" in 02; the catalog trust line in 01; FELHOM_THINCHECK and unit-forget in 03. Evidence audits/agent-allowlist-rotation-2026-10-17/. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -727,6 +727,46 @@ Cloudflare edge served 0.315.0's first stylesheet under the address its third bu
|
||||
|
||||
---
|
||||
|
||||
## [DESIGN] The catalog allow-list — the container rights the box gives (controller v0.328.0, R-971, operator decision D2 = A, 2026-10-17)
|
||||
|
||||
**What.** Before ANY app compose is written — the install, a settings change, the 15-minute correction, a guarded update
|
||||
or ladder step, a unit or off-site restore — the controller renders the compose with the app's own values (exactly as
|
||||
`compose up` will see them) and asks `internal/privpolicy.Admit`. A setting in one of these classes is refused unless
|
||||
this app's service has EXACTLY that setting on the allow-list: `privileged`, `cap_add`, `devices`,
|
||||
`device_cgroup_rules`, `gpus`, deploy device reservations, `network_mode`/`pid`/`ipc`/`uts`/`userns_mode`/`cgroup`/
|
||||
`cgroup_parent`/`runtime`/`isolation`, `security_opt` other than `no-new-privileges`, `sysctls`, `volumes_from`, `build`,
|
||||
`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
|
||||
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).
|
||||
|
||||
**Where, and why in code.** The list is `internal/privpolicy/allowlist.json`, compiled in with `go:embed`: a change in
|
||||
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
|
||||
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,
|
||||
running; the controller raises the operator-only hub event `app_definition_refused` once per refused definition (hub ≥
|
||||
v0.157.0) and the app page carries one line (`app_info.definition_refused`). A refused install, settings change or
|
||||
restore is refused with a plain message („Semmi nem változott"); a restore asks BEFORE it stops the app. A check
|
||||
that cannot read the compose refuses. The sync never copies a catalog template named like a controller infra stack.
|
||||
|
||||
**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.
|
||||
|
||||
**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
|
||||
first), `sync.TestR971_*` (a refused correction writes neither file and is not reported updated).
|
||||
|
||||
## The app-definition seam — what happens to a deployed app when its compose file is rewritten
|
||||
|
||||
> **STATUS: MEASURED BEHAVIOUR, NOT A RECORDED DESIGN.** This section describes what the system was
|
||||
|
||||
Reference in New Issue
Block a user