The handbook's sections were agent-lifecycle, integrations, swarm, tools, trust-boundary and web-ui, which do not follow how the system is built, so a reader could not tell which layer a page is about. Its pages now sit in swarm/, hives/ and agents/, with the accounts an agent can hold outside the system in external-accounts/. - swarm/: forge, matrix and the knowledge repo join the swarm pages, and so does the swarmctl reference. - hives/: the dashboard, hivectl and its reference, and the operator/agent boundary, with a README that takes over the old web-ui and tools pages. - agents/: the roster, persistence, config changes, the agent's own web page (was web-ui/agent) and the security model, with a README. - external-accounts/: GitHub and GitLab. Pages are moved whole; none is split yet. Every reference follows: relative links in the docs, `docs/2-handbook/...` paths in code comments, the nix checks that regenerate the CLI references and the ignore list, and docs/README.md, whose handbook section is now by layer.
109 lines
5.4 KiB
Markdown
109 lines
5.4 KiB
Markdown
# The operator/agent boundary
|
|
|
|
<!-- vale write-good.Passive = NO -->
|
|
|
|
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.
|
|
|
|
<!-- vale write-good.Passive = YES -->
|
|
|
|
The operator/agent boundary is 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.
|
|
|
|
## Two principals, two paths
|
|
|
|
- **Operator** — reaches every UI (the dashboard + every
|
|
per-agent page) through the gateway, on one origin.
|
|
The core daemon serves operator-authority actions
|
|
(answer-as-operator, lifecycle POSTs), reachable only via the
|
|
gateway.
|
|
- **Agent** — speaks only for itself, only over its per-agent
|
|
unix socket. The socket's identity _is_ the agent (see
|
|
`docs/5-project/process/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 in hive-c0re.
|
|
|
|
Worked example — destroying or rebuilding a container is a
|
|
`POST /api/{destroy,rebuild}/{name}` on the core dashboard, _never_ a
|
|
per-agent-socket `Request` variant. If it were a per-agent-socket
|
|
request, a compromised agent could `curl` its own socket and destroy
|
|
or rebuild itself (or, if the variant took an arbitrary target, another
|
|
agent) without ever touching the core's own authenticated surface.
|
|
|
|
## 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 complete and always on: every agent container
|
|
runs in a private netns behind the hive bridge, and there is no
|
|
shared-netns mode. See `docs/4-implementation/networking/network.md`.
|
|
|
|
Concretely, the core daemon's dashboard `/api` carries **no
|
|
application-layer authentication** — the core daemon serves operator-authority
|
|
routes 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 the CI runner'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's a standing invariant: the `/api` must never bind to a
|
|
non-loopback address or get exposed outside the gateway, and every new
|
|
operator-authority route inherits that assumption. hyperhive treats
|
|
`swarm-ci-runner` like an agent for this purpose — it runs untrusted PR code and is
|
|
netns-isolated for the same reason.
|
|
|
|
The boundary rests on three layers:
|
|
|
|
1. **Gateway** — fronts all surfaces (dashboard + every per-agent UI)
|
|
on one origin. An nginx nixos-container proxies per-agent UIs under
|
|
`/agent/<name>/`, which is what lets each agent page's inbox panel
|
|
POST `mark-all-read` to the core dashboard's
|
|
`/api/agent/{name}/mark-all-read` go same-origin instead of needing
|
|
a cross-origin CORS shim. Pure ergonomics — no behavioural risk on
|
|
its own.
|
|
2. **Network isolation** — the load-bearing layer: every agent
|
|
container runs in a private netns behind the hive bridge, always
|
|
on and unconditional. This is what turns the operator/agent split
|
|
from an honour-system convention into an enforced boundary.
|
|
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/4-implementation/trust-boundary/hive-c0re-privilege.md`](../../4-implementation/trust-boundary/hive-c0re-privilege.md) for the privilege boundary table.
|
|
|
|
### host admin socket access (`hivectl`)
|
|
|
|
`hivectl` drives the whole hive — spawn / kill / destroy / rebuild /
|
|
deploy — over the **host admin socket** `/run/hyperhive/host.sock`,
|
|
socket-activated by the `hive-c0re.socket` unit. That socket _is_ the
|
|
full-control surface, so who can connect to it's a real trust
|
|
boundary.
|
|
|
|
By default the socket is `0660` group-owned by **`hive-admin`**, an
|
|
empty group — so it's effectively **root-only** until an operator is
|
|
explicitly granted access. Grant sudoless `hivectl` by listing login
|
|
users in `services.hyperhive.deploy.hive-controller.adminUsers`; the option adds each to
|
|
`hive-admin`, and members connect without `sudo`. The runtime dir
|
|
`/run/hyperhive` is `0751` (traverse-only, no listing) so the group can
|
|
reach the socket path; the socket's own `0660 hive-admin` mode gates
|
|
the connection, and the per-agent subdirs under it keep their own
|
|
restrictive perms. Keep `adminUsers` to trusted operators — membership
|
|
is equivalent to root over the hive.
|
|
|
|
## Implementation detail
|
|
|
|
How `hive-priv` is socket-activated and how the per-agent socket directory
|
|
gets its owner and mode:
|
|
[socket mechanics](../../4-implementation/trust-boundary/boundary-mechanism.md).
|