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:
1 parent
d968f8490e
commit
5a5486104a
36 files changed
+816
-16
No files matched your search
@@ -169,6 +169,12 @@ second layer. **How it is checked:** `scripts/https_only_check.py` lists every p
|
||||
routes and demands 301/308 to the same https URL (negative control: a stub answering plain 200 must be convicted).
|
||||
Evidence: `audits/https-only-2026-10-14/`.
|
||||
|
||||
**What the catalog may ask of the box (controller v0.328.0, R-971, decision D2 = A).** The catalog repository is trusted
|
||||
for WHICH apps exist and how they are configured — not for container RIGHTS. A template that asks for a right not on the
|
||||
allow-list compiled into the controller (privileged mode, capabilities, devices, host namespaces, the Docker socket,
|
||||
binds outside the box's data roots, …) is refused on the box before its compose is written, and the operator is told
|
||||
(`02-controller-module-map.md` „The catalog allow-list"). Signed catalog releases remain the later step R-971 names.
|
||||
|
||||
**Who may reach an app, and through what (recorded 2026-09-29 — no document said it before; spike finding F1).**
|
||||
Every app is reached only through the box's traefik (no catalog app publishes a host port except crafty-controller's
|
||||
game ports; none uses host networking — read from the catalog 2026-09-29). traefik routes by host name: the tunnel's
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -113,6 +113,8 @@ 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_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 | — |
|
||||
| `FELHOM_SSHD` | the out-of-band operator sshd | config via `felhom-priv-apply sshd-config` (the ONE template, only the Port varies, never 22); the felhom-op key via `sshd-key` (one plain key, no `command=`/`from=` options) | felhom-op's key itself is hub-delivered, not signed: a compromised agent can install its own key for **felhom-op** — whose sudo is scoped (`felhom-op.sudoers`: fixed repair verbs and `pct start` / `stop` / `unlock` of ONE numeric vmid, anchored since R-861 (b) B2), not root — accepted (`09` §3 decision 165, B3) |
|
||||
|
||||
Reference in new issue
Block a user