diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index a0d17fc9..c60897df 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -456,6 +456,43 @@ journal link to the host takes part; nspawn skips `--link-journal=try-guest` in ephemeral mode without failing. The journal vanishes at stop, and with it the lines written after the last push. +### User namespace (opt-in, per agent) + +An agent named in `services.hyperhive.userns.agents` runs in its own user +namespace. Every other agent keeps the host's uids. The operator sets the list +in the host config; hive-priv reads it from its unit's environment, so an agent +can't switch it on or off. + +- **On disk the data belongs to one host uid:gid**, `services.hyperhive.userns.dataOwner` + (default `60000:60000`). Inside the container that owner shows as the agent + user. hive-priv mounts the agent's `state/`, `harness/`, `work/` and + `claude/` dirs and its socket dir with `owneridmap`, and every other bind too + (config, `/knowledge`, the runtime dir), whose owner shows as root inside. +- **The container starts with `--private-users=pick --private-users-ownership=map`.** + nspawn idmaps the root at mount time, so a different range for `pick` never + walks or chowns it. +- **The move happens once.** The first `WriteNspawnFlags` that finds a listed + agent's container stopped chowns those four dirs to `dataOwner` (ownership + only; it removes or copies nothing), writes + `/var/lib/hive-priv/userns/`, and creates the mount points in the root + template owned by the agent user. A later request with the marker present + chowns nothing. A running container waits for its next stop. The socket dir + is on a tmpfs, so `EnsureAgentSocketDir` hands it to `dataOwner` at every + start. +- **The agent's gid must equal its uid.** `owneridmap` maps the gid of a bind + with the uid map, so a group other than the uid's has no mapping and writes + fail. Pin `services.hyperhive.agent.user.uid` and `.gid` to one number in the + agent's config before listing it. +- **Leaving the list is manual.** hive-priv refuses to start an agent whose + marker exists but who is no longer on the list, and one whose `dataOwner` + changed. Chown the four dirs back, then delete the marker. + +The pause marker needs nothing extra: hive-priv gives it the owner of the +`harness/` dir. hive-c0re still creates agent dirs and new subvolumes as +`hive-core`, and only the first namespaced start chowns them. A dir created +after the move, such as a recreated subvolume, stays `hive-core`'s and the +agent can't write to it. + ### Cross-agent access to state An agent holding the `manage_root_agent` permission gets every other diff --git a/docs/trust-boundary/boundary.md b/docs/trust-boundary/boundary.md index 71c8a776..810aa484 100644 --- a/docs/trust-boundary/boundary.md +++ b/docs/trust-boundary/boundary.md @@ -119,8 +119,11 @@ both sockets are `0666`, which is all a dialer needs. 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`. The container has no user namespace, so -that uid is the host inode's owner. Don't add a host-side chown or chmod: +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/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. diff --git a/docs/trust-boundary/security.md b/docs/trust-boundary/security.md index fa131ec3..98265c89 100644 --- a/docs/trust-boundary/security.md +++ b/docs/trust-boundary/security.md @@ -261,8 +261,8 @@ known operations; there is no arbitrary command pass-through: | `DestroyContainer` | `nixos-container destroy ` | | `ListContainers` | `nixos-container list` | | `ReloadGatewayNginx` | `systemctl reload/start/reset-failed nginx` (host unit; the unit name is hard-coded, not a parameter) | -| `WriteNspawnFlags` | write `/etc/nixos-containers/.conf` (bind-mount list, network isolation vars, and `--ephemeral` after the root's salvage). hive-priv first copies a stopped container's root to the agent's `work/`, then renames it aside (it deletes nothing), following no link the container could have planted | -| `EnsureAgentSocketDir` | `mkdirat` `/run/hive-agent/` `0751 root:root` under an `O_NOFOLLOW` parent fd; leaves an existing directory alone, refuses anything else | +| `WriteNspawnFlags` | write `/etc/nixos-containers/.conf` (bind-mount list, network isolation vars, and `--ephemeral` after the root's salvage). hive-priv first copies a stopped container's root to the agent's `work/`, then renames it aside (it deletes nothing), following no link the container could have planted; for an agent in `services.hyperhive.userns.agents` it also adds `--private-users=pick`, mounts every bind `owneridmap`, and once chowns the agent's `state`, `harness`, `work` and `claude` dirs (never `config`, following no link) to the data owner | +| `EnsureAgentSocketDir` | `mkdirat` `/run/hive-agent/` `0751 root:root` under an `O_NOFOLLOW` parent fd; leaves an existing directory alone, refuses anything else; for an agent in `services.hyperhive.userns.agents` it then chowns the dir to the data owner | | `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=`/`CPUWeight=`/`IOWeight=` systemd drop-in for agent container, with an `ExecStartPre=` that runs `hive-priv check-rootfs ` (no-follow check of the root paths the start script touches; see persistence.md), and a GC root on the build it names | | `RemoveServiceDropin` | remove `container@.service.d/` drop-in on destroy, and the GC root of the hive-priv build it named | | `DaemonReload` | `systemctl daemon-reload` | diff --git a/nix/host-modules/hive-priv.nix b/nix/host-modules/hive-priv.nix index 235eb978..b3bef1a7 100644 --- a/nix/host-modules/hive-priv.nix +++ b/nix/host-modules/hive-priv.nix @@ -40,6 +40,11 @@ in the container stopped. Take the agent off the list only after chowning those dirs back and removing `/var/lib/hive-priv/userns/`; until then it will not start. + + The agent's own config must pin `services.hyperhive.agent.user.uid` + and `.gid` to one number: nspawn maps a bind's gids with its uid map. + Keep an agent that holds `ManageRootAgent` off the list, since the + other agents' dirs it mounts are not owned by its data owner. ''; };