v0.328.0: the catalog allow-list — the box refuses container rights a template asks for and the box does not give (R-971, D2 = A)
gates / gates (push) Successful in 45s

internal/privpolicy checks the RENDERED compose (privileged, cap_add, devices, host namespaces, security_opt,
sysctls, volumes_from, build/include/extends, env_file/config files outside the stack dir, aliasing named volumes,
binds outside the box's data roots, the Docker socket, propagation, ${VAR} in a privileged key) against an
allow-list compiled into the controller (one entry: homepage's read-only socket). Asked before every compose
write: the 15-minute sync, the guarded update/ladder step, the install, both settings changes, both restores
(before the stop). A refusal writes nothing, keeps the app on its last good compose, raises the operator-only
event app_definition_refused once per definition and puts one line on the app page. The sync never copies a
template named like a controller infra stack. Structural guard TestAllowList_IsWiredIntoEveryComposeWriter.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
admin committed 2026-10-17 16:34:12 +02:00
1 parent 71fbef6bbe
commit 6d90374f63
26 files changed
+2408 -9

No files matched your search

+1
View File
@@ -136,6 +136,7 @@
| Symbol | File | Short signature | Use for | Gotchas |
|---|---|---|---|---|
| `stacks.Manager.admitCompose` / `privpolicy.Admit` (v0.328.0, R-971) | controller/internal/stacks/allowlist.go, controller/internal/privpolicy/privpolicy.go | `admitCompose(name, data, env) error` | THE question before ANY write of an app compose (and before a settings change or restore commits an env): the box's catalog allow-list on the RENDERED compose | A new compose writer must call it first — `TestAllowList_IsWiredIntoEveryComposeWriter` fails otherwise; a refusal records the page line + one hub event per definition; the allow-list is `privpolicy/allowlist.json` (exact app+service+setting) and the catalog's `scripts/privilege-allowlist.json` must stay byte-identical |
| `Manager.DeployStack` | controller/internal/stacks/deploy.go | `(req DeployRequest) (string, error)` | Full deploy flow | Sets in-memory `Deployed` BEFORE compose up (slow-pull race), reverts on failure |
| `DeclaredVolumeNames` (R-658, v0.268.0) | controller/internal/stacks/undo.go | `(composePath) (own, external []string, err)` | THE app's named volumes as Docker names them: `name:` else `<project>_<key>`, project = top-level `name:` else the stack dir | **Never select an app's volumes by the `com.docker.compose.project` label** — a restore before v0.268.0 made volumes without it, and the undo then copied nothing (R-658). The label is a cross-check only |
| `renderDigests` / `RenderWithLadderDigests` / `CarryDigests` / `StripDigest` (v0.269.0/.1) | controller/internal/stacks/digest.go | compose bytes → the same with `ref@sha256` from the ladder entry for its refs | writing a compose that RUNS (update; the sync for an app NOT yet installed) — an INSTALLED app's sync uses `CarryDigests` (v0.269.1), which keeps the digest the app runs | Pins and records are digest-free by construction (`parseComposeImagesBytes` and the installed record strip) — never compare a raw `.Config.Image` against a pin |