A rebuild or create downloads the closure the swarm built and applies it; with none to deploy it fails and changes nothing. hive-priv takes a store path instead of an internal/meta commit and builds nothing.
63 lines
3.2 KiB
Markdown
63 lines
3.2 KiB
Markdown
# 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`, and hive-priv accepts it only in the shape of a store
|
|
path.
|
|
|
|
`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`.
|