Watch
0
0
Fork
You've already forked hyperhive
0

swarm-controller: restrict its socket directory to nginx's access group

This commit is contained in:
atlas 2026-10-17 13:54:14 +02:00
commit 51cc093465
4 changed files with 63 additions and 16 deletions

View file

@ -20,6 +20,12 @@ let
natsCfg = config.services.hyperhive.swarm.nats;
swarmCfg = config.services.hyperhive.swarm;
# Who may connect to the controller's socket: nginx (the gateway's `/api/`
# and `/webhook/forge/` locations) and root (`swarmctl`). A group of its own
# rather than `swarm-controller`: that group also owns the state directory,
# and nginx has no business reading it.
clientsGroup = "swarm-controller-clients";
# Quick links to swarm services, built from `swarm.*` options alone: every
# host evaluates the same entries, whichever host runs each service. One
# entry per service; reading a `deploy.*` option here would tie a link to
@ -469,10 +475,13 @@ in
proxies to.
The **directory** is the access control here, not the socket mode:
the socket itself is `0666` (nginx runs as another user, and
`connect(2)` needs write), exactly as hive-c0re publishes the
per-agent sockets. What keeps that safe is that the directory holds
exactly one socket and is traverse-only (`0751`) for everyone else.
the socket itself is `0666`, exactly as hive-c0re publishes the
per-agent sockets, and the directory is `0750` owned by the
`${clientsGroup}` group. Members of that group (the gateway's
nginx, for `/api/` and `/webhook/forge/`) and root (`swarmctl`,
which bypasses mode bits) can reach the socket; every other local
user gets `EACCES` at the directory. What keeps `0666` safe is that
the directory holds exactly one socket.
Moving this path under a directory that carries anything else —
`/run/hyperhive`, which holds the host admin socket, above all —
exposes whatever else lives there to the same reachability.
@ -621,6 +630,14 @@ in
description = "hyperhive swarm-controller daemon";
};
users.groups.swarm-controller = { };
users.groups.${clientsGroup} = { };
# Through the unit, not `users.users.nginx.extraGroups`: systemd resolves
# a unit's groups at start, and only a changed unit file makes the switch
# restart nginx (it does not reload) onto the new group.
systemd.services.nginx = lib.mkIf config.services.nginx.enable {
serviceConfig.SupplementaryGroups = [ clientsGroup ];
};
# Installed host-wide, not into the daemon's unit: `swarmctl` is run
# by a human on this box and acts as root, so the daemon's sandbox is
@ -707,10 +724,14 @@ in
# socket. See `socketPath`'s description for why that is a security
# property and not tidiness.
RuntimeDirectory = "swarm-controller";
# 0751: traverse-only for others, so the gateway's nginx can reach
# the socket path without being able to list the directory. Same
# shape (and same reason) as hive-c0re's runtime dir.
RuntimeDirectoryMode = "0751";
# 0750 with the group set to `clientsGroup` below: only nginx and
# root can reach the socket inside.
RuntimeDirectoryMode = "0750";
# systemd chowns the runtime directory to `User:Group` on every
# start, and the daemon is not a member of `clientsGroup`, so it
# cannot chgrp the directory itself. `+` runs this as root, after
# that chown.
ExecStartPre = "+${pkgs.coreutils}/bin/chgrp ${clientsGroup} ${builtins.dirOf deployCfg.swarm-controller.socketPath}";
# Preserved across restarts so the path never vanishes from under a
# running nginx. The daemon unlinks a stale socket on start, which
# is what makes preservation safe.