hyperhive/docs/boundary.md
atlas 480b35aac4 docs(boundary): update to reflect always-on network isolation
All three area:ops pillars are now complete: network isolation is
always on (private netns, no shared-netns mode), the gateway is live,
and hive-c0re runs as the unprivileged hive-core user.

- Rewrite the intro to say the boundary is enforced, not aspirational
- Update 'Why network isolation is the load-bearing step' section to
  use past tense for the shared-netns world and note completion
- Mark network isolation as Complete in the sequencing list
2026-07-04 20:58:55 +02:00

82 lines
3.8 KiB
Markdown

# The operator/agent boundary
Design rationale for hyperhive's two-principal trust model. The
_implementation_ work — container network isolation, the unifying
gateway, core-daemon privsep — is tracked as `area:ops` issues on
the forge.
The operator/agent boundary is now technically enforced, not just a
convention. Containers run in private netns (network isolation is
always on), the gateway proxies all operator-facing traffic, and
`hive-c0re` runs as the unprivileged `hive-core` user. All three
`area:ops` pillars — network isolation, the gateway, and privsep —
are complete and active.
## Two principals, two paths
- **Operator** — reaches every UI (the dashboard + every
per-agent page) through the gateway, on one origin.
Operator-authority actions (approve / deny, answer-as-operator,
lifecycle POSTs) are served by the core daemon and only
reachable via the gateway.
- **Agent** — speaks only for itself, only over its per-agent
unix socket. The socket's identity _is_ the agent (see
`docs/conventions.md`, "identity = socket"). An agent must not
be able to reach the core daemon's HTTP surface, another
agent's socket, or another agent's web UI.
## Design rule
**Operator-authority actions never get a per-agent-socket entry
point.** They live on the core backend.
Worked example — answering an operator-targeted question is a
`POST /answer-question/{id}` on the core dashboard, _never_ an
`AgentRequest` variant. If it were a per-agent-socket request, an
agent could `curl` its own socket and spoof an operator answer.
The per-agent web UI POSTs cross-origin to the core for these
(see the inline-answer feature — the loose-ends section on each
agent page).
## Why network isolation is the load-bearing step
Without network isolation, containers share the host network namespace
and can reach `localhost:<core-port>`, the dashboard, and every other
agent's web port — the operator/agent split is on the honour system and
every boundary claim above is aspirational. Network isolation is what
makes the boundary _real_; the gateway and privsep are ergonomics and
defence-in-depth layered on top.
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`.
The `area:ops` issues followed this sequencing:
1. **Gateway** — pure ergonomics win, unblocks same-origin (lets the
cross-origin CORS shim on `/answer-question/{id}` go away), no
behavioural risk. An nginx nixos-container now sits in front of all
surfaces; per-agent UIs are proxied under `/agent/<name>/`.
2. **Network isolation** — the load-bearing step that turns the
honour-system split into an enforced boundary. **Complete**
always-on, unconditional; the shared-netns mode was removed.
3. **Privsep** — defence in depth on the core process; `hive-c0re`
runs as the unprivileged `hive-core` user and delegates root
operations to `hive-priv`, a narrow socket-activated helper. See
[`docs/security.md`](security.md) for the privilege boundary table.
### hive-priv socket activation
`hive-priv` is **always** socket-activated by the `hive-priv.socket`
systemd unit. The unit binds `/run/hive/priv.sock` with
`SocketGroup=hive-core` and mode `0660` and passes the ready listener
to the helper as fd 3 (`LISTEN_FDS`). The helper requires this and
bails if it isn't socket-activated — there is intentionally no
self-bind fallback.
Dropping the old fallback removed a dev/prod divergence: when
`hive-priv` bound the socket itself it created the file owned by
root's primary group rather than `hive-core`, so a `hive-core` client
couldn't connect the way the socket unit's `SocketGroup` grant
intends. Requiring socket activation everywhere means dev and prod
take the exact same path and the group grant always holds.