Watch
0
0
Fork
You've already forked hyperhive
0

document the user namespace opt-in

This commit is contained in:
atlas 2026-11-01 13:53:41 +01:00 • committed by mara
commit da098b1a50
4 changed files with 49 additions and 4 deletions

View file

@ -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 in ephemeral mode without failing. The journal vanishes at stop, and with it
the lines written after the last push. 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/<name>`, 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 ### Cross-agent access to state
An agent holding the `manage_root_agent` permission gets every other An agent holding the `manage_root_agent` permission gets every other

View file

@ -119,8 +119,11 @@ both sockets are `0666`, which is all a dialer needs.
hive-priv creates the dir when missing (`EnsureAgentSocketDir`, `0751 hive-priv creates the dir when missing (`EnsureAgentSocketDir`, `0751
root:root`, no symlink followed) and leaves an existing one alone. The root:root`, no symlink followed) and leaves an existing one alone. The
container's own activation (`hive-agent-user-migrate`) then chowns it to 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 the agent user and sets `0751`. Without a user namespace
that uid is the host inode's owner. Don't add a host-side chown or chmod: 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, 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 dir is `0751 root`: nothing but root can plant a socket in it.

View file

@ -261,8 +261,8 @@ known operations; there is no arbitrary command pass-through:
| `DestroyContainer` | `nixos-container destroy <name>` | | `DestroyContainer` | `nixos-container destroy <name>` |
| `ListContainers` | `nixos-container list` | | `ListContainers` | `nixos-container list` |
| `ReloadGatewayNginx` | `systemctl reload/start/reset-failed nginx` (host unit; the unit name is hard-coded, not a parameter) | | `ReloadGatewayNginx` | `systemctl reload/start/reset-failed nginx` (host unit; the unit name is hard-coded, not a parameter) |
| `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.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 | | `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.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/<name>` `0751 root:root` under an `O_NOFOLLOW` parent fd; leaves an existing directory alone, refuses anything else | | `EnsureAgentSocketDir` | `mkdirat` `/run/hive-agent/<name>` `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 <container>` (no-follow check of the root paths the start script touches; see persistence.md), and a GC root on the build it names | | `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=`/`CPUWeight=`/`IOWeight=` systemd drop-in for agent container, with an `ExecStartPre=` that runs `hive-priv check-rootfs <container>` (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@<name>.service.d/` drop-in on destroy, and the GC root of the hive-priv build it named | | `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy, and the GC root of the hive-priv build it named |
| `DaemonReload` | `systemctl daemon-reload` | | `DaemonReload` | `systemctl daemon-reload` |

View file

@ -40,6 +40,11 @@ in
the container stopped. Take the agent off the list only after chowning the container stopped. Take the agent off the list only after chowning
those dirs back and removing `/var/lib/hive-priv/userns/<agent>`; those dirs back and removing `/var/lib/hive-priv/userns/<agent>`;
until then it will not start. 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.
''; '';
}; };