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
intends. Requiring socket activation everywhere means dev and prod
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` 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.
Unlike the `hive-c0re` daemon subcommands (which go through the broker),

View file

@ -126,6 +126,15 @@ in
};
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
# alongside hyperhive), so the per-agent web-port range stays closed on
# the host firewall. See `docs/gateway.md::Firewall posture (host-level)`.
@ -189,7 +198,12 @@ in
Group = "hive-core";
SupplementaryGroups = [ "systemd-journal" ];
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";
StateDirectory = "hyperhive";
StateDirectoryMode = "0750";
@ -251,12 +265,18 @@ in
socketConfig = {
# Must match the `--socket` arg passed to `hive-c0re serve`.
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";
# Parent dir inherits the RuntimeDirectory mode (0750) set on the
# service unit; DirectoryMode is only consulted when the dir is
# absent at socket-unit activation.
DirectoryMode = "0750";
SocketGroup = "hive-admin";
# `0751` (traverse-only, no listing) so hive-admin can reach the socket
# path — the socket's own `0660 hive-admin` gates the connection, and
# 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.
'';
};
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.
'';
};
};
}