boundary.md keeps the model, the design rule, why network isolation matters and host admin socket access; the hive-priv socket activation and the per-agent socket directory move to 4-implementation/trust-boundary. security.md keeps the agent trust model and the path-name rule; the nix builds mode with its threat model, and the hive-c0re privilege separation, move to their own pages there. Headings and text move as they were, with one level shaved off the heading depth. Links, code and nix pointers, and the docs index follow the moves.
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).
|