Watch
0
0
Fork
You've already forked hyperhive
0

add the agent option privateUsers, with the uid = gid assertion

This commit is contained in:
atlas 2026-11-01 16:05:36 +01:00 • committed by mara
commit 1fd591d28b
2 changed files with 76 additions and 0 deletions

View file

@ -90,8 +90,47 @@ in
'';
};
options.services.hyperhive.agent.user.privateUsers = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Run this agent's container in its own user namespace. Off by default.
**Approving the config PR that sets this authorises a host-level
change.** The swarm builder reads the option from the evaluated config
into the agent's closure record, and the hive then starts the container
with `--private-users=pick`, every hive bind mounted `owneridmap`, and
its root idmapped. The first such start has `hive-priv`, as root, chown
the agent's `state`, `harness`, `work` and `claude` dirs once to the
host's data owner
(`services.hyperhive.deploy.hive-controller.userns.dataOwner`). Nothing is
deleted. Turning it off afterwards needs the dirs chowned back by hand.
Needs `user.uid` and `user.gid` set to the same number: nspawn maps a
bind's gids with its uid map, so a group of another number has no
mapping and writes fail. Start the agent once with the pin before
turning this on; `hive-priv` refuses to move a `state` dir whose gid
differs from its uid. An agent that holds `ManageRootAgent` should not
set it, since the other agents' dirs it mounts are not remapped.
'';
};
config = {
assertions = [
{
assertion =
!config.services.hyperhive.agent.user.privateUsers
|| (
config.services.hyperhive.agent.user.uid != null
&& config.services.hyperhive.agent.user.uid == config.services.hyperhive.agent.user.gid
);
message = ''
services.hyperhive.agent.user.privateUsers needs services.hyperhive.agent.user.uid
and services.hyperhive.agent.user.gid set to the same number: a user namespace
maps a bind's gids with its uid map, so a group of another number has no mapping.
'';
}
{
assertion =
config.services.hyperhive.agent.user.uid == null

View file

@ -27,7 +27,44 @@ let
.system.activationScripts.hive-agent-user-migrate.text;
# The tmpfiles rules of the same named agent.
rules = (agentWith { services.hyperhive.agent.user.name = "iris"; }).systemd.tmpfiles.rules;
# `privateUsers` with the given pins; the agent name is the one the cases
# above use, so only the pins differ.
withUsers =
user:
agentWith {
services.hyperhive.agent.user = {
name = "iris";
}
// user;
};
# Whether the module refuses the config over the user-namespace pin.
refusesPins =
cfg: lib.any (a: !a.assertion && lib.hasInfix "privateUsers" a.message) cfg.assertions;
cases = [
{
# The control is the matching pair: it passes, so each refusal is the
# assertion's own and the default (off, no pins) passes too.
name = "privateUsers needs a uid and a gid that are the same number";
ok =
!refusesPins (withUsers { })
&& !refusesPins (withUsers {
privateUsers = true;
uid = 1000;
gid = 1000;
})
&& refusesPins (withUsers {
privateUsers = true;
})
&& refusesPins (withUsers {
privateUsers = true;
uid = 1000;
})
&& refusesPins (withUsers {
privateUsers = true;
uid = 1000;
gid = 1001;
});
}
{
# hive-c0re creates the host work dir as hive-core; this activation is
# the only thing that lets the agent write to it.