Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/docs/4-implementation/trust-boundary/boundary-mechanism.md
iris 30a8f71bb5 docs: the trust-boundary pages keep the operator guidance, the mechanism moves to implementation
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.
2026-11-06 19:45:11 +01:00

72 lines
3.8 KiB
Markdown

# Socket mechanics behind the operator/agent boundary
How systemd activates `hive-priv` and how the per-agent socket directory
gets its owner and mode, the two pieces of the boundary described in
[the operator/agent boundary](../../2-handbook/trust-boundary/boundary.md).
## 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.
⚠️ Intentionally, no self-bind fallback exists: if `hive-priv` bound
the socket itself, it would create the file owned by root's primary
group rather than `hive-core`, and a `hive-core` client couldn't
connect the way the socket unit's `SocketGroup` grant intends.
Requiring socket activation everywhere keeps dev and prod on the
exact same path, so the group grant always holds.
## the per-agent socket dir
**Three principals that share no group** reach `/run/hive-agent/<name>/`,
which is why its mode is what it's:
| principal | reaches | needs |
| ------------------- | ---------------------------------------- | ------------ |
| the agent's harness | binds + unlinks `agent.sock`, `web.sock` | owner, `rwx` |
| `hive-c0re` | dials `agent.sock` (todo wakes) | traverse |
| the gateway's nginx | dials `web.sock` | traverse |
The last two land in "other," so the dir is **`0751`, owned by the
agent's container uid/gid** — `o=--x` is traverse without listing, and
both sockets are `0666`, which is all a dialer needs.
<!-- vale write-good.Passive = NO -->
**One mechanism creates it, one sets its owner.** Before every start,
hive-priv creates the dir when missing (`EnsureAgentSocketDir`, `0751
root:root`, no symlink followed) and leaves an existing one alone. The
container's own activation (`hive-agent-user-migrate`) then chowns it to
the agent user and sets `0751`. Without a user namespace
that uid is the host inode's owner. (A namespaced agent, see "User namespace" in
`docs/2-handbook/agent-lifecycle/persistence.md`, gets the dir from `EnsureAgentSocketDir`
owned by its data owner instead, because the container can't chown through
the id map.) Don't add another host-side chown or chmod:
two owners of one path revert each other. Until the container activates,
the dir is `0751 root`: nothing but root can plant a socket in it.
<!-- vale write-good.Passive = YES -->
The mode is load-bearing, not cosmetic. Write permission on a
_directory_ is what confers the right to unlink its entries, whoever owns
them, and the sticky bit is the only thing that would restrain that (it
isn't set here). A world-writable socket dir therefore lets anything
able to reach the path delete an agent's socket and bind its own. On the
host, any non-root process whose `/run` is writable can do that, such as
a login session or dnsmasq. nginx only dials: `ProtectSystem=strict` makes its view
of `/run` read-only. Dropping `o=w` removes that permission rather than
qualifying it.
⚠️ **The gateway's nginx and dnsmasq are host services, next to
`hive-c0re`** (see `docs/4-implementation/networking/gateway.md`) — there is no namespace between
them and the rest of the host. That costs no network isolation: nginx
binds the host's `:80`/`:443` and reaches `localhost` upstreams, which
would require opening a netns anyway.
🔑 It does mean nothing _implicitly_ scopes the privileged reload verb —
see [`docs/4-implementation/trust-boundary/hive-c0re-privilege.md`](hive-c0re-privilege.md) for
how `PrivRequest::ReloadGatewayNginx`'s containment works.
⚠️ This dir has exactly one writer, so it needs no world write at all.