| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .. | ||
| src | ||
| Cargo.toml | ||
| README.md | ||
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/2-handbook/hives/boundary.md and docs/4-implementation/trust-boundary/hive-c0re-privilege.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). egress holds two sub-commands that never
touch the socket, egress-pin and egress-lease; the hive-egress unit, udev
and dnsmasq run them to fill the sets of the anti-spoofing table (see
docs/4-implementation/networking/network.md). 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), pulled beforehand by
hive-c0re through the nix daemon. hive-priv first imports the cache's signature for the path with
nix store copy-sigs (from /run/hyperhive-nix/cache.conf; a failure logs a
warning), then applies it only when
nix store verify --no-contents --sigs-needed 1, with the key in root-owned
/run/hyperhive-priv/attic/public-key, finds the swarm cache's signature on it.
It refuses a path that is absent, unsigned, signed by another key or not a store
path, and logs an error naming the agent and the path. hive-priv keeps no record
of what was pulled, so a restart between the pull and the apply loses nothing.
RefreshCacheFacts starts the root unit that refreshes the cache facts.
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.