Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-priv/README.md
atlas 6b800600e2 bind a container apply to the closure realised for the agent, and record the deployed pins on drift
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.
2026-10-29 22:36:58 +01:00

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 PrivRequest variant 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.