boundary.md keeps the model, the design rule, why network isolation matters and host admin socket access; the hive-priv socket activation and the per-agent socket directory move to 4-implementation/trust-boundary. security.md keeps the agent trust model and the path-name rule; the nix builds mode with its threat model, and the hive-c0re privilege separation, move to their own pages there. Headings and text move as they were, with one level shaved off the heading depth. Links, code and nix pointers, and the docs index follow the moves.
5.9 KiB
Nix builds and credential isolation
The mechanism and threat model for nix build inside an agent container, from
the security model.
Background
nixos-containers' start script bind-mounts the host's nix-daemon socket
directory (/nix/var/nix/daemon-socket) into every container, hyperhive's
agents included; hyperhive adds no bind for it. The host daemon may
have sandbox-fallback = false (strict NixOS defaults), which causes nix build
inside nspawn containers to fail — containers lack kernel user namespaces, so nix
can't set up its build sandbox. the agent modules set sandbox-fallback = true
so that builds fall back to unsandboxed execution rather than failing outright.
Per-agent nix builds mode
services.hyperhive.agent.nixBuilds picks where an agent's nix builds run. It
has two values, and defaults to host:
host: builds go to the host daemon through the socket above. The daemon forks them in its own cgroup, so they run outside the agent container's CPU and memory caps, which sit oncontainer-<name>.service.none: the container has no route to the host daemon. hive-priv adds--tmpfs=/nix/var/nix/daemon-socketto the container's nspawn flags. nspawn mounts its custom mounts sorted by destination, then by kind (a bind before a tmpfs), so the empty tmpfs lands on top of the start script's bind whatever order the flags are in, and the container finds nosocketin the directory.NIX_REMOTE=daemonstays, so a build fails withcannot connect to socket at '/nix/var/nix/daemon-socket/socket', which names the cause. The agent's/nix/storeand its read-only nix database stay bound, so store paths already there stay readable; only a realisation of a new path, or anynixcommand that needs the daemon, fails. The agent's own NixOS config turns itsnix-daemonsocket and service off in this mode: the tmpfs is writable, so the unit would otherwise listen on it. Underhostthe directory is the host's read-only bind, which the socket unit'sConditionPathIsReadWriterejects, so that unit never starts.
The mode reaches the container as privateUsers does: the swarm builder reads
the option from the evaluated config into the agent's closure record
(nix_builds), hive-c0re reads the record when it writes the container's
flags and sends the mode to hive-priv, and the flags apply at the container's
next start.
- An agent whose module doesn't declare the option reads
host. When the builder can't read the option (its eval fails or times out), the record carries no mode, and so does a record or builder from before the field. hive-c0re then sends no mode, and hive-priv keeps what the container's last write chose. - A mode this build doesn't know (a record written by a newer swarm) reads as
none, neverhost: an agent that asked for a mode its hive can't give gets no builds, not builds outside its limits. - A record hive-c0re can't read, or none, sends no mode either, and hive-priv keeps what the container's last write chose.
- A hive-c0re or hive-priv from before the option ignores it, so the agent keeps host builds until you upgrade both.
none cuts the daemon only. It leaves the agent's egress alone.
Threat model
Unsandboxed nix builds run as nixbld users (non-root; nrBuildUsers is at least 32, so UIDs 30001 and up).
Without sandbox isolation, a build derivation's builder script has read access to
any file in the container that the nixbld user can read.
The blast radius also has a network dimension. swarm-ci-runner runs its
unsandboxed builds of untrusted PR code in its own private netns behind the
bridge: a build reaches the forge by name and can't reach host-loopback
services — including the core dashboard at 127.0.0.1:<dashboard_port>, which
has no application-layer auth of its own (see docs/4-implementation/scheduler/ci.md). The 0600
token policy bounds file reads; network isolation bounds network reach.
What's NOT exposed:
/home/<name>/.claude/—.credentials.json,history.jsonlandsettings.jsonare0600;projects/andsessions/are0700. All owned by the per-agent user<name>, so nixbld users can't read any of them. ⚠️ The directory itself is0755, on purpose:hive-coreis a different user and needs read+execute to list it soclaude_has_sessioncan detect a valid session (ensure_claude_dir,hive-c0re/src/lifecycle/setup.rs). The per-file mode is therefore the whole protection here — anything added to this directory at a default mode is world-readable, which isn't hypothetical:plugins/and.last-cleanupalready are.$HYPERHIVE_STATE_DIR/forge-token(=/agents/<name>/state/forge-token) — the token hive-c0re wrote at mode0600, chowned to the per-agent uid:gid, before swarm-controller took over minting. Nothing writes it any more; it stays as the fallback for an agent without a store identity. nixbld users can't read it. The swarm-minted token lives under/run/hive-agent-forge-token/(0700directory,0400file), outside the state dir./run/hive-agent-proxy-credential/proxy.env— the agent's egress-proxy credential inside its proxy URL (0700directory,0600file, tmpfs). Units load it as an environment file, so the credential is also in the environment of every process they start. It authenticates this agent to the hive's squid and nothing else.
Policy: all credential files written to agent state directories MUST be mode
0600 or stricter. Don't create world-readable secret files in agent state dirs.
Long-term fix
The proper fix is to enable user namespaces inside nspawn containers
(--private-users=inherit in EXTRA_NSPAWN_FLAGS) so nix can set up its real
sandbox and sandbox-fallback becomes a true last resort. This requires verifying
bind-mount compatibility with user namespace UID mapping first.