Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/docs/4-implementation/trust-boundary/boundary-mechanism.md
iris 0ae34b1cd0 docs: persistence and config changes become one operator page
The operator parts of the persistence and config-changes pages merge into
agents/persistence.md. The sqlite tables, state dirs, mounts and migrations move
to 4-implementation/agent-state/persistence.md, and the deploy path, meta flake,
deploy markers, startup migrations, root container, system messages and
autoupdate move to 4-implementation/agent-state/config-changes.md. Links, code
comments and nix comments that named the old sections point at the new pages.
2026-11-11 01:50:35 +01:00

3.8 KiB

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.

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.

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/4-implementation/agent-state/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.

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 for how PrivRequest::ReloadGatewayNginx's containment works.

⚠️ This dir has exactly one writer, so it needs no world write at all.