feat(#2641): sudoless hivectl via a hive-admin group on the host socket

The host admin socket `/run/hyperhive/host.sock` was `0660 root:root` (no
SocketGroup), so hivectl needed sudo. Group-own it by a new `hive-admin`
group and add a `services.hyperhive.c0re.adminUsers` allowlist: listed users
join `hive-admin` and drive hivectl without root.

- `SocketGroup = "hive-admin"`, `SocketMode = "0660"` on the hive-c0re.socket
  unit.
- `/run/hyperhive` -> `0751` (traverse-only, no listing) so the group can reach
  the socket path; the socket's own `0660 hive-admin` mode gates the
  connection, and the per-agent subdirs keep their own restrictive perms.
- Empty `adminUsers` (the default) leaves `hive-admin` memberless -> root-only,
  as before.

The admin socket is full hive control (spawn/kill/destroy/deploy), so
`adminUsers` is an explicit, opt-in trust grant. Documented in
docs/boundary.md (host admin socket access) + docs/tools/hivectl.md.
This commit is contained in:
atlas 2026-07-22 22:47:20 +02:00 committed by mara
commit 30a2a2e9de
4 changed files with 68 additions and 7 deletions

View file

@ -93,3 +93,22 @@ root's primary group rather than `hive-core`, so a `hive-core` client
couldn't connect the way the socket unit's `SocketGroup` grant couldn't connect the way the socket unit's `SocketGroup` grant
intends. Requiring socket activation everywhere means dev and prod intends. Requiring socket activation everywhere means dev and prod
take the exact same path and the group grant always holds. take the exact same path and the group grant always holds.
### host admin socket access (`hivectl`)
`hivectl` drives the whole hive — spawn / kill / destroy / rebuild /
deploy — over the **host admin socket** `/run/hyperhive/host.sock`,
socket-activated by the `hive-c0re.socket` unit. That socket *is* the
full-control surface, so who can connect to it is a real trust
boundary.
By default the socket is `0660` group-owned by **`hive-admin`**, an
empty group — so it is effectively **root-only** until an operator is
explicitly granted access. Grant sudoless `hivectl` by listing login
users in `services.hyperhive.c0re.adminUsers`; each is added to
`hive-admin`, and members connect without `sudo`. The runtime dir
`/run/hyperhive` is `0751` (traverse-only, no listing) so the group can
reach the socket path; the socket's own `0660 hive-admin` mode gates
the connection, and the per-agent subdirs under it keep their own
restrictive perms. Keep `adminUsers` to trusted operators — membership
is equivalent to root over the hive.

View file

@ -1,7 +1,12 @@
# hivectl # hivectl
`hivectl` is the operator-facing host CLI for hyperhive. It lives on the `hivectl` is the operator-facing host CLI for hyperhive. It lives on the
host (not inside any container) and requires root for most operations. host (not inside any container). It talks to `hive-c0re` over the host
admin socket `/run/hyperhive/host.sock`, which is root-only by default —
so it needs `sudo` unless you grant sudoless access by listing your
login user in `services.hyperhive.c0re.adminUsers` (adds you to the
`hive-admin` group that owns the socket; see
[`docs/boundary.md`](../boundary.md#host-admin-socket-access-hivectl)).
Available via the `hive-c0re` package in the host NixOS config. Available via the `hive-c0re` package in the host NixOS config.
Unlike the `hive-c0re` daemon subcommands (which go through the broker), Unlike the `hive-c0re` daemon subcommands (which go through the broker),

View file

@ -126,6 +126,15 @@ in
}; };
users.groups.hive-core = { }; users.groups.hive-core = { };
# Operators granted sudoless `hivectl`. Members of `hive-admin` can
# connect to the host admin socket (group-owned by hive-admin via the
# socket unit's `SocketGroup` below) without root. That socket is *full*
# hive control (spawn/kill/destroy/deploy, docs/boundary.md), so this is
# an explicit opt-in allowlist — empty by default (root-only).
users.groups.hive-admin = {
members = cfg.adminUsers;
};
# The gateway nginx is always the sole external entry point (it runs # The gateway nginx is always the sole external entry point (it runs
# alongside hyperhive), so the per-agent web-port range stays closed on # alongside hyperhive), so the per-agent web-port range stays closed on
# the host firewall. See `docs/gateway.md::Firewall posture (host-level)`. # the host firewall. See `docs/gateway.md::Firewall posture (host-level)`.
@ -189,7 +198,12 @@ in
Group = "hive-core"; Group = "hive-core";
SupplementaryGroups = [ "systemd-journal" ]; SupplementaryGroups = [ "systemd-journal" ];
RuntimeDirectory = "hyperhive"; RuntimeDirectory = "hyperhive";
RuntimeDirectoryMode = "0750"; # 0751 (traverse-only, no listing) so `hive-admin` operators can reach
# the host admin socket (`SocketGroup = "hive-admin"`, 0660) without
# root. Others can traverse but not list; the socket + per-agent
# subdirs gate access by their own perms. Matches the socket unit's
# DirectoryMode.
RuntimeDirectoryMode = "0751";
RuntimeDirectoryPreserve = "yes"; RuntimeDirectoryPreserve = "yes";
StateDirectory = "hyperhive"; StateDirectory = "hyperhive";
StateDirectoryMode = "0750"; StateDirectoryMode = "0750";
@ -251,12 +265,18 @@ in
socketConfig = { socketConfig = {
# Must match the `--socket` arg passed to `hive-c0re serve`. # Must match the `--socket` arg passed to `hive-c0re serve`.
ListenStream = "/run/hyperhive/host.sock"; ListenStream = "/run/hyperhive/host.sock";
# 0660 root:root — `hivectl` is a host-only tool run as root. # `0660 root:hive-admin` — group-owned by `hive-admin` so operators in
# that group (services.hyperhive.c0re.adminUsers) drive `hivectl`
# without root; an empty adminUsers list leaves the group memberless,
# so it stays effectively root-only. See docs/boundary.md.
SocketMode = "0660"; SocketMode = "0660";
# Parent dir inherits the RuntimeDirectory mode (0750) set on the SocketGroup = "hive-admin";
# service unit; DirectoryMode is only consulted when the dir is # `0751` (traverse-only, no listing) so hive-admin can reach the socket
# absent at socket-unit activation. # path — the socket's own `0660 hive-admin` gates the connection, and
DirectoryMode = "0750"; # the per-agent subdirs under here keep their own restrictive perms.
# Must match the service unit's RuntimeDirectoryMode. DirectoryMode is
# only consulted when the dir is absent at socket-unit activation.
DirectoryMode = "0751";
}; };
}; };
}; };

View file

@ -315,5 +315,22 @@
serialized by its lifecycle lease regardless. serialized by its lifecycle lease regardless.
''; '';
}; };
adminUsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [ "alice" ];
description = ''
Login users granted **sudoless `hivectl`**. Each is added to the
`hive-admin` group, which group-owns the host admin socket
(`/run/hyperhive/host.sock`, mode `0660`) so listed users drive
`hivectl` (and thus the whole hive) without `sudo`.
This is a real privilege grant: the admin socket is *full* hive
control spawn / kill / destroy / deploy see `docs/boundary.md`.
Keep the list to trusted operators. Empty (the default) keeps the
socket root-only, as before.
'';
};
}; };
} }