From 30a2a2e9de95614276d87756ee6671d0c522fe9b Mon Sep 17 00:00:00 2001 From: atlas Date: Wed, 22 Jul 2026 22:47:20 +0200 Subject: [PATCH] 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. --- docs/boundary.md | 19 +++++++++++++++ docs/tools/hivectl.md | 7 +++++- nix/host-modules/hive-c0re/default.nix | 32 +++++++++++++++++++++----- nix/host-modules/hive-c0re/options.nix | 17 ++++++++++++++ 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/docs/boundary.md b/docs/boundary.md index 0de2ab99..868a051e 100644 --- a/docs/boundary.md +++ b/docs/boundary.md @@ -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. diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 55117dea..487a771b 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -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), diff --git a/nix/host-modules/hive-c0re/default.nix b/nix/host-modules/hive-c0re/default.nix index d2cc0b74..0e9ceed9 100644 --- a/nix/host-modules/hive-c0re/default.nix +++ b/nix/host-modules/hive-c0re/default.nix @@ -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"; }; }; }; diff --git a/nix/host-modules/hive-c0re/options.nix b/nix/host-modules/hive-c0re/options.nix index 2700dfa5..9822e2c6 100644 --- a/nix/host-modules/hive-c0re/options.nix +++ b/nix/host-modules/hive-c0re/options.nix @@ -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. + ''; + }; }; }