docs(security): ci netns, knowledge .git tmpfs, matrix id/secret split

Security fixes landed without updating the security-model docs; bring them
current as present-tense invariants:

- security.md: matrix identities are public / cross-agent-readable via
  GetAgentMeta while the token stays private; name validation extends to
  GetAgentMeta; the nix-build blast radius has a network dimension (hive-ci
  netns) alongside the 0600 token policy.
- boundary.md: the core dashboard /api has no app-layer auth — protection is
  the gateway plus network isolation; stated as a standing invariant.
- ci.md: hive-ci runs in its own private netns behind the bridge, reaches the
  forge via the gateway, and cannot reach host-loopback (dashboard/forge port).
- knowledge.md: /knowledge excludes .git via tmpfs so the core token in
  .git/config isn't readable by agents.

Refs #2308, #2310, #2313.
This commit is contained in:
sock 2026-07-10 17:04:31 +02:00 committed by mara
commit 6c95933775
4 changed files with 39 additions and 2 deletions

View file

@ -51,6 +51,19 @@ Network isolation is now complete and always on: every agent container
runs in a private netns behind the hive bridge. The shared-netns mode
was removed. See `docs/network.md`.
Concretely, the core daemon's dashboard `/api` carries **no
application-layer authentication** — operator-authority routes are served
unauthenticated at the HTTP layer. Their protection is entirely (a) the
gateway, which fronts all operator traffic and is where operator auth lives,
and (b) network isolation, which keeps agents — and `hive-ci`'s untrusted PR
builds — off host-loopback so nothing can reach `127.0.0.1:<dashboard_port>`
directly. This is deliberate given the load-bearing role of network isolation
above, but it is a standing invariant: the `/api` must never be bound to a
non-loopback address or exposed outside the gateway, and every new
operator-authority route inherits that assumption. `hive-ci` is treated like an
agent for this purpose — it runs untrusted PR code and is netns-isolated for
the same reason.
The `area:ops` issues followed this sequencing:
1. **Gateway** — pure ergonomics win, unblocks same-origin (lets the

View file

@ -53,7 +53,7 @@ Set `services.hyperhive.forge.ci.enable = true` in the host NixOS config. That's
## Container design
- **Shared host netns**: container reaches hive-forge at `http://127.0.0.1:<httpPort>` (same as hive-gateway).
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It cannot reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `forge.behindGateway = true`.
- **Non-ephemeral**: runner credentials persist across restarts (written to container's stateDir on first registration, reused thereafter).
- **Sandbox fallback**: nspawn containers can't create user-namespaces, so nix's sandboxing would always fail. Module sets `nix.settings.sandbox-fallback = true` in the container — nix builds run unsandboxed (safe because the container is already isolated). See `docs/gotchas.md`.
- **Credential isolation**: the forge admin token (`forge-core-token`) never enters the container. A host-side oneshot service (`hive-ci-prefetch.service`) performs all forge API calls and writes only the runner registration token into the container via a read-only bind-mount at `/run/hive-ci/runner-token`.
@ -125,7 +125,7 @@ nspawn containers cannot create user-namespaces, so `nix.settings.sandbox-fallba
A malicious `default.nix` or build script in a PR can therefore:
- **Make arbitrary network requests** to any address reachable from the container. The container shares host netns, so `http://127.0.0.1:<forgePort>` is reachable with the forge API — without admin credentials, but public/read endpoints are accessible.
- **Make arbitrary network requests** to any address reachable from the container. The container runs in its own netns behind the hive bridge, so it reaches the forge only through the gateway (`http://<forge.domain>`, public/read endpoints — no admin credentials) and **cannot** reach host-loopback services: the unauthenticated core dashboard at `127.0.0.1:7000` and the raw forge port are off-limits (bridge→127.0.0.0/8 is dropped).
- **Write to the container filesystem**, including corrupting the runner's state dir or `.runner` credentials.
The core admin token (`forge-core-token`) is **not** bind-mounted into the container. It is accessed only by the host-side `hive-ci-prefetch.service` before the container starts. A build process can still reach forge over the network, but cannot use the admin token to issue privileged API calls.

View file

@ -64,6 +64,11 @@ before returning.
from the host clone on every container start. Gone when container
is stopped; reappears on next start with the current clone state.
The mount deliberately **excludes `.git`**: the host clone embeds the `core`
token in `.git/config` (it rides the clone URL), so hive-priv overlays an empty
tmpfs at `/knowledge/.git` — agents see the documents, not the repo metadata or
token.
## Contributing
Agents are read-only collaborators on `internal/knowledge`, so they

View file

@ -39,6 +39,13 @@ So a compromised/confused agent's reach on the forge or matrix is bounded by
_its own_ account's scope, not the swarm's. This is the main thing standing
between "one agent does something dumb" and "the whole hive is affected".
**Identity vs. secret (matrix).** The scoping is on the _secret_, not the
_identity_: an agent's matrix **token** is private to its own account, but its
matrix **identities** — the public handles (`name`, `user_id` `@user:server`,
`homeserver`) — are intentionally readable by any agent via `GetAgentMeta`, so
peers can find and address one another on a shared matrix instance. Only the
public handle crosses that boundary; the token never does.
### Threat model: prompt injection → confused deputy
The realistic adversary **never needs to breach the container**. They supply
@ -127,6 +134,11 @@ Defense-in-depth layers (in order):
dashboard renders anchors only for tokens that passed the same checks the
read endpoint enforces.
The same invariant holds wherever an agent-supplied name reaches a filesystem
path: the agent socket's `GetAgentMeta` validates `name` with
`validate_agent_name` before building `agent_notes_dir(name)`, so a `..`
component can't traverse.
## Nix builds and credential isolation
### Background
@ -143,6 +155,13 @@ Unsandboxed nix builds run as `nixbld` users (non-root, typically UIDs 30001-300
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. hive-ci runs its unsandboxed
builds of untrusted PR code in its own private netns behind the hive bridge: a
build reaches the forge only through the gateway and cannot 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/ci.md`](ci.md)). The `0600`
token policy bounds file reads; network isolation bounds network reach.
**What is NOT exposed**:
- `/home/<name>/.claude/` — mode `0700`, owned by the per-agent