From 6c9593377596abd680c56c72d355428871e45c6c Mon Sep 17 00:00:00 2001 From: sock Date: Fri, 10 Jul 2026 17:04:31 +0200 Subject: [PATCH] docs(security): ci netns, knowledge .git tmpfs, matrix id/secret split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/boundary.md | 13 +++++++++++++ docs/ci.md | 4 ++-- docs/knowledge.md | 5 +++++ docs/security.md | 19 +++++++++++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/boundary.md b/docs/boundary.md index 1fc5ed3b..0de2ab99 100644 --- a/docs/boundary.md +++ b/docs/boundary.md @@ -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:` +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 diff --git a/docs/ci.md b/docs/ci.md index f6b5bad6..be9189dc 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -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:` (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://` (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:` 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://`, 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. diff --git a/docs/knowledge.md b/docs/knowledge.md index 447430c1..73aa1e8f 100644 --- a/docs/knowledge.md +++ b/docs/knowledge.md @@ -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 diff --git a/docs/security.md b/docs/security.md index 435a066a..f0fb0efa 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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:`, 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//.claude/` — mode `0700`, owned by the per-agent