Give the unit each managed target's roots; record host preprovisioning

ProtectSystem=full makes /etc read-only, so Idunn could not create Heimdall's
runtime bundle under /etc/gamecult/heimdall/runtime. A target whose roots are
not in ReadWritePaths is a target Idunn cannot deploy. Every migration adds
exactly three paths -- runtime root, state root, release root -- which keeps
the write set enumerated and is the honest blast radius of running as root.

Also records the per-target preprovisioning Idunn requires rather than
creates, each discovered by a failed deployment: the state root must be
root:<state_group> mode 2770 so the workload's dynamic user inherits group
access through setgid; the runner cache must be 0700 owned by the runner's
container uid, not the source identity, with a root-owned parent (/srv/build
is idunn:idunn and fails); the frozen source stage must be root-owned,
because source is frozen as idunn and then copied into a root-owned
actuation stage; and the binding's docker network must exist.

None of these are Idunn being fussy. Each is a check that a binding cannot
widen access by naming a path someone else can write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-06 19:52:56 +02:00
commit 5a12ec2af8
2 changed files with 24 additions and 0 deletions

View file

@ -380,6 +380,26 @@ consumer that hand-decodes the private-store framing and applies it to
tests will pass, because hand-built fixtures agree with the hand-built reader.
Build fixtures with the CultCache client so a test can disagree with you.
## Per-target host preprovisioning
Idunn does not create a target.s roots; it refuses to deploy into paths that are
not already shaped correctly. Each check exists so a compromised or careless
binding cannot widen access, and each one cost a failed deployment to discover:
| Path | Required shape | Check |
|---|---|---|
| `state_root` | `root:<state_group>`, mode `2770` | must be root-owned, group-owned by the state group, not world-writable, and setgid so the workload.s dynamic user inherits group access |
| `cache_root` | owned by the runner.s container uid, mode `0700` | "dedicated exact-identity 0700 directory" -- the uid is the runner.s `user`, not the source identity |
| `cache_root` parent | root-owned, not group- or world-writable | `/srv/build` is `idunn:idunn` and therefore fails; put caches under Idunn.s own root |
| frozen source stage | root-owned, non-writable | source is frozen *as* `idunn`, then copied into a root-owned actuation stage |
| runner network | must exist | the binding names a docker network; the operator provisions it |
**The Idunn unit also needs the target.s roots.** `ProtectSystem=full` makes
`/etc` read-only, so a target whose `runtime_root` is not in `ReadWritePaths`
fails at "creating runtime bundle". Every migration adds exactly three paths --
runtime root, state root, release root -- and that list is the honest blast
radius of running Idunn as root.
## Cutting over the authority
Idunn last, and not by deploying Idunn with Idunn.