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.
72 lines
3.8 KiB
Markdown
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.
|