From 894349459aab35e1865c156617be91bff658e4ae Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 1 Nov 2026 16:21:17 +0100 Subject: [PATCH] document where the privateUsers flag lives and who it authorises --- docs/agent-lifecycle/persistence.md | 36 ++++++++++++++++++++--------- docs/trust-boundary/security.md | 4 ++-- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index 07426830..46ebcd28 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -458,12 +458,23 @@ 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. +An agent whose config sets `services.hyperhive.agent.user.privateUsers = true` +runs in its own user namespace. Every other agent keeps the host's uids. -- **On disk the data belongs to one host uid:gid**, `services.hyperhive.userns.dataOwner` +**Approving the config PR that sets it authorises a host-level change:** the +first start afterwards has hive-priv, as root, chown the agent's `state`, +`harness`, `work` and `claude` dirs once to the host's data owner. It deletes nothing. + +**How the hive learns the flag.** It never evaluates the agent's nix. The swarm +builder reads `config.services.hyperhive.agent.user.privateUsers` from the +config it evaluates in its sandbox and puts it in the agent's closure record +(`private_users`). hive-c0re reads the record when it writes the container's +flags and sends the value to hive-priv with each start. A record or builder +from before the field reads false; a record hive-c0re can't read, or none, +leaves the agent as it is. + +- **On disk the data belongs to one host uid:gid**, + `services.hyperhive.deploy.hive-controller.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 @@ -471,7 +482,7 @@ can't switch it on or off. - **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 +- **The move happens once.** The first `WriteNspawnFlags` that finds the 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 @@ -482,12 +493,15 @@ can't switch it on or off. or manager that can write the dirs can't redirect it to a host path. - **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, and start it once, before listing it. The move refuses an - agent whose `state` dir has another gid than uid, naming the agent. -- **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` + fail. The agent module asserts that `user.uid` and `user.gid` hold the + same number when `privateUsers` is on. Start the agent once with the pin + before turning it on: the move refuses an agent whose `state` dir has another + gid than uid, naming the agent. +- **Turning it off is manual.** hive-priv refuses to start an agent whose + marker exists while its record says false, and one whose `dataOwner` changed. Chown the four dirs back, then delete the marker. +- **Keep a `ManageRootAgent` holder off it.** The other agents' dirs it mounts + aren't remapped. 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 diff --git a/docs/trust-boundary/security.md b/docs/trust-boundary/security.md index 98265c89..706da122 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; 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 | +| `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 whose closure record sets `private_users` 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 whose closure record sets `private_users` 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` |