swarm-controller: restrict its socket directory to nginx's access group
This commit is contained in:
parent
5bb9d7c7e5
commit
51cc093465
4 changed files with 63 additions and 16 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue