Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-priv/README.md

4.3 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/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 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). egress holds four sub-commands that never touch the socket, egress-load, egress-pin, egress-lease and egress-whois, and egress_ech a fifth, egress-ech-relay, the long-running ClientHello check in front of squid's HTTPS intercept port; the hive-egress unit, udev, a timer and dnsmasq run the first three to fill the sets of the anti-spoofing table and the interception exemptions, and squid runs the last to name the agent of an intercepted flow (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.