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.

View file

@ -414,6 +414,24 @@ let
&& lib.hasInfix socket l."/webhook/forge/".proxyPass
&& !(lib.hasInfix "auth_request" l."/webhook/forge/".extraConfig);
}
{
# Only nginx (through the clients group) and root reach the controller
# socket: the runtime dir is 0750 with that group set. The last two
# clauses are the presence controls — the group exists and is not the
# daemon's own, so a misspelled name cannot satisfy the rest.
name = "the controller socket's directory is 0750 and group-owned by the group nginx joins";
ok =
let
sc = allLocal.systemd.services.swarm-controller.serviceConfig;
group = "swarm-controller-clients";
in
sc.RuntimeDirectoryMode == "0750"
&& lib.hasPrefix "+" sc.ExecStartPre
&& lib.hasInfix "chgrp ${group} /run/swarm-controller" sc.ExecStartPre
&& lib.elem group allLocal.systemd.services.nginx.serviceConfig.SupplementaryGroups
&& allLocal.users.groups ? ${group}
&& sc.Group != group;
}
{
# nginx serves a path from its longest matching prefix location, so that
# one must be the gated `/api/`, and no exact location may take a route