# Security model ## State-file endpoint security model `GET /api/state-file?path=

` serves files from agent state dirs and the shared space to authenticated dashboard users (browser, operator). Two allow-listed root prefixes are accepted; all other paths are rejected before touching the filesystem: - `/var/lib/hyperhive/agents//state/` — per-agent durable notes (canonical host form or the in-container view `/agents//state/`) - `/var/lib/hyperhive/shared/` — shared docs (`/shared/` in-container) `/state/...` without an agent prefix is explicitly *not* accepted — it is ambiguous from the host's perspective. Defense-in-depth layers (in order): 1. **Allow-list prefix check** — rejects without touching the filesystem if the path doesn't match either root. 2. **No symlinks below the matched root** — each path component is checked with `symlink_metadata` before canonicalize. A sub-agent that plants `ln -s /other/secret /agents/me/state/peek` can't proxy another agent's file through this endpoint (canonicalize would happily resolve the symlink to a still-within-allow-list path). 3. **Canonicalize as belt-and-braces** — resolves `..`/`.` traversal and rejects if the result escapes the roots. 4. **`state/` subdir constraint** — under `AGENTS_ROOT`, the second path component must be `state/`. Applied, proposed git repos and config dirs are off-limits. 5. **World-readable check** — file must have `mode & 0o004` set. A `0600` file inside `state/` would otherwise be accessible to any operator with dashboard access. `scan_validated_paths` (broker-message ingest, linkifier) uses the same `resolve_state_path` helper so security rules stay in sync — the dashboard renders anchors only for tokens that passed the same checks the read endpoint enforces. ## Nix builds and credential isolation ### Background Agent containers bind-mount the host's `nix-daemon` socket. 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 cannot set up its build sandbox. `harness-base.nix` sets `sandbox-fallback = true` so that builds fall back to unsandboxed execution rather than failing outright. ### Threat model Unsandboxed nix builds run as `nixbld` users (non-root, typically UIDs 30001-30010). Without sandbox isolation, a build derivation's builder script has read access to any file in the container that the nixbld user can read. **What is NOT exposed**: - `/home//.claude/` — mode `0700`, owned by the per-agent user ``. nixbld users cannot read it. - `$HYPERHIVE_STATE_DIR/forge-token` (= `/agents//state/forge-token`) — written at mode `0600` by `hive-c0re/src/forge.rs` and chowned to the per-agent uid:gid by `lifecycle::chown_to_agent`. nixbld users cannot read it. **Policy**: all credential files written to agent state directories MUST be mode `0600` or stricter. Do not 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 and is tracked as a TODO. ## hive-c0re privilege separation ### Background `hive-c0re` runs as the unprivileged system user `hive-core` (`/var/lib/hyperhive` owned by `hive-core:hive-core`). It cannot directly invoke `nixos-container`, `journalctl -M`, or `systemctl -M hive-gateway` — those require root. `hive-priv` fills this gap. ### hive-priv `hive-priv` is a minimal privileged helper that runs as root, socket-activated at `/run/hive/priv.sock` (mode `0660`, group `hive-core` — only the `hive-core` user can connect). `hive-c0re` calls it via `priv_client` for every operation that genuinely requires root. **Narrow interface** — `PrivRequest` variants map 1:1 to specific known operations; there is no arbitrary command pass-through: | Operation | What it runs | |-----------|-------------| | `StartContainer` / `StopContainer` / `KillContainer` | `nixos-container start/stop/kill ` | | `CreateContainer` / `UpdateContainer` | `nixos-container create/update --flake ` | | `DestroyContainer` | `nixos-container destroy ` | | `ListContainers` | `nixos-container list` | | `ReadContainerJournal` | `journalctl -M -n [filters...]` | | `ReloadGatewayNginx` | `systemctl -M hive-gateway reload/start/reset-failed nginx` | | `WriteNspawnFlags` | write `/etc/nixos-containers/.conf` (bind-mount list + network isolation vars) | | `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=` systemd drop-in for agent container | | `RemoveServiceDropin` | remove `container@.service.d/` drop-in on destroy | | `DaemonReload` | `systemctl daemon-reload` | | `ChownSocketDir` / `ChmodSocketDir` | chown/chmod `/run/hive-agent//` socket directory | | `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin ` | | `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir | | `RestartMatrixDaemon` | `systemctl --machine=h- restart hive-matrix-daemon.service` | **Container allowlist** — every request is validated against an allowlist before any operation: only names matching the agent-name convention (char-validated) or the known sibling service containers (`hive-gateway`, `hive-forge`, `hive-matrix`, `hive-ci`) are accepted. Arbitrary container names are rejected. **Socket-activated** — systemd starts `hive-priv` on the first incoming connection (`LISTEN_FDS=1`); it is not running between calls. The `ProtectSystem=strict` + `ReadWritePaths` sandbox limits filesystem writes to only the paths `hive-priv` legitimately needs. ### Privilege boundary summary | Component | Runs as | Privilege needed for | |-----------|---------|----------------------| | `hive-c0re` | `hive-core` | broker, HTTP dashboard, scheduling, approvals | | `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes | | `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving |