hive-priv applies only the path its last RealiseClosure returned for that agent. On drift the swap and create record the pins of the commit the closure was built from.
3.6 KiB
hive-priv
The minimal root privileged-helper for hive-c0re. It runs as root and
exposes a narrow unix socket at /run/hive/priv.sock that accepts PrivRequest
JSON lines and performs only the handful of operations that genuinely require
root — bind-mount edits, nsenter into a container, btrfs subvolume ops. All
coordination logic (broker, HTTP, scheduling) stays in the unprivileged
hive-c0re process, which delegates here.
Why it exists
Privsep. hive-c0re runs as the unprivileged hive-core user so a bug or a
prompt-injection in the large daemon can't directly wield root. The few root
operations it needs are funnelled through this small, auditable helper instead.
See docs/trust-boundary/boundary.md and docs/trust-boundary/security.md for the privilege boundary.
Security model
- Strict allowlist. hive-priv checks every request against a container-name
allowlist before any filesystem or process operation, and accepts only names
matching the hive convention (
h-*, the manager container included). - No pass-through. Every
PrivRequestvariant maps to a single known operation; there is no arbitrary-command escape hatch. - Socket-activated, always. systemd binds
/run/hive/priv.sock(SocketGroup=hive-core,0660) and passes the listener as fd 3 (LISTEN_FDS); the helper requires this and has no self-bind fallback, so dev and prod take the identical path and the group grant always holds.
The wire contract (PrivRequest / response types) lives in the separate
hive-priv-sock crate so this root binary depends on just the protocol shapes,
not the whole daemon-shared crate.
Layout
src/main.rs holds the socket loop and the dispatch table, one arm per
PrivRequest variant. Each op family, with its validation, has its own module:
container (lifecycle), container_config (nspawn flags, systemd drop-ins),
storage (btrfs subvolumes, quotas, snapshots, disk measurement), files
(pause marker, socket dir, legacy tmpfiles) and infra (gateway nginx, forge
admin, infra containers). Helpers more than one family calls live in shared.
Each module's //! header lists its ops.
Implementation notes
Container toplevels (create/update)
container_toplevel_action (in src/container.rs) applies a closure that is
already in the store; hive-priv builds nothing. hive-c0re names the closure
(toplevel on CreateContainer / UpdateContainer), downloaded beforehand
with RealiseClosure. hive-priv remembers, per agent, the path its last
successful RealiseClosure returned (in memory), and applies only that path to
that agent. It refuses a path never realised for the agent, another agent's
and the host's own, and logs an error naming the agent and both paths. A
restart between the download and the apply empties the record, so the apply
fails and the job retries. hive-priv does not re-check the signature at apply
time: RealiseClosure enforces it, and the binding limits an apply to what that
call stored.
create passes the path to nixos-container create --system-path, so
nixos-container's own buildFlake() never runs. Without the flag, it builds
to a hardcoded relative .tmp out-link: $systemPath stays undef and
"$systemPath.tmp" interpolates to the bare string .tmp in the caller's cwd.
With services.hyperhive.deploy.hive-controller.buildSlots > 1, two concurrent
calls could share that one .tmp, and one agent's container got another
agent's closure.
update points the container's profile at the path with nix-env --set and
reloads the unit if it runs, because nixos-container update dies on its
version probe before it honors --system-path.