Prose-only sweep of every remaining claim that nginx or dnsmasq lives in a container: the port comment (root in a container -> root on the host), upstreamHost's netns rationale, the ACME state dir, the store path reachability note, the vhost tree header, dnsmasq's resolv.conf paragraph (there is no copy and no path unit watching it any more), the two hive-network bridge comments, and swarm-controller's socket access-control note, which described a bind-mount that no longer exists. No behaviour change; all of it was describing a mechanism that was deleted.
177 lines
7.1 KiB
Nix
177 lines
7.1 KiB
Nix
# The swarm-level controller daemon. Per-host opt-in: a swarm has one
|
|
# controller, so most hives leave this off and point at the hive that
|
|
# runs it. Distinct from hive-c0re, which every hive runs — c0re owns
|
|
# the agents on one host, this owns what is true across hives.
|
|
#
|
|
# Serves HTTP over a unix socket rather than a TCP port: the gateway's
|
|
# nginx is the only intended client, it runs on this same host and so
|
|
# reaches the socket by path, and a socket that is never bound to an
|
|
# address cannot be reached from off-host by mistake.
|
|
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.hyperhive.swarm.controller;
|
|
autheliaCfg = config.services.hyperhive.swarm.authelia;
|
|
|
|
# What `swarmctl` needs in order to act on authelia from the host.
|
|
#
|
|
# Only set when authelia actually runs **here**: the controller can be
|
|
# enabled on a host that is not the swarm's SSO host, and in that case
|
|
# the right behaviour is for `swarmctl user add` to fail saying the
|
|
# value is unset. A guessed path would resolve cleanly and write a file
|
|
# nothing reads, which is the failure mode that costs an afternoon.
|
|
autheliaEnv = lib.optionalAttrs autheliaCfg.enable {
|
|
# The CONFIGURED authelia, not whatever is on PATH: the argon2
|
|
# parameters baked into a hash have to match the verifier's.
|
|
SWARMCTL_AUTHELIA_BIN = "${autheliaCfg.package}/bin/authelia";
|
|
SWARMCTL_AUTHELIA_USERS_FILE = autheliaCfg.hostUsersFile;
|
|
SWARMCTL_AUTHELIA_MACHINE = autheliaCfg.machine;
|
|
SWARMCTL_AUTHELIA_UNIT = autheliaCfg.unit;
|
|
};
|
|
|
|
# Wrapped rather than documented: every one of these values is derived
|
|
# from an option this deployment already set, so making the operator
|
|
# re-supply them on the command line would be asking them to repeat the
|
|
# config back to it — and to get it wrong the day one of them changes.
|
|
swarmctlConfigured = pkgs.symlinkJoin {
|
|
name = "swarmctl-configured";
|
|
paths = [ cfg.swarmctlPackage ];
|
|
nativeBuildInputs = [ pkgs.makeWrapper ];
|
|
postBuild = ''
|
|
wrapProgram $out/bin/swarmctl ${
|
|
lib.concatStringsSep " " (
|
|
lib.mapAttrsToList (name: value: "--set ${name} ${lib.escapeShellArg value}") autheliaEnv
|
|
)
|
|
}
|
|
'';
|
|
};
|
|
in
|
|
{
|
|
options.services.hyperhive.swarm.controller = {
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Run the swarm-controller daemon on this host. Off by default and
|
|
deliberately not derived from `services.hyperhive.enable`: a swarm
|
|
has one controller, so enabling it per hive is a decision about
|
|
swarm topology, not about whether hyperhive is installed.
|
|
|
|
`services.hyperhive.enableAllLocalDefaults` does assert it, and
|
|
that is not an exception to the rule above — it is the rule
|
|
applied. That mode says "this box is the whole deployment", which
|
|
answers the topology question outright, where
|
|
`services.hyperhive.enable` alone never can.
|
|
'';
|
|
};
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarm-controller";
|
|
description = ''
|
|
swarm-controller package. Wired by default from this flake's own
|
|
package set (see `flake.nix`); override to run a different build.
|
|
'';
|
|
};
|
|
|
|
swarmctlPackage = lib.mkOption {
|
|
type = lib.types.package;
|
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.swarmctl";
|
|
description = ''
|
|
`swarmctl` package — the swarm operator's CLI, installed on this
|
|
host alongside the daemon and wrapped with the paths it needs.
|
|
|
|
A separate option from `package` rather than a second binary in
|
|
the same derivation: the CLI runs as root and acts directly,
|
|
the daemon runs unprivileged and serves a socket, and pinning one
|
|
without the other is a thing an operator may legitimately want.
|
|
'';
|
|
};
|
|
|
|
socketPath = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/run/swarm-controller/controller.sock";
|
|
description = ''
|
|
Unix socket the daemon serves on, and the path the gateway's nginx
|
|
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.
|
|
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.
|
|
|
|
Changing this therefore means re-checking what else lives in the
|
|
new directory, not just the daemon.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf (config.services.hyperhive.enable && cfg.enable) {
|
|
users.users.swarm-controller = {
|
|
isSystemUser = true;
|
|
group = "swarm-controller";
|
|
description = "hyperhive swarm-controller daemon";
|
|
};
|
|
users.groups.swarm-controller = { };
|
|
|
|
# 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
|
|
# exactly what it must not inherit.
|
|
environment.systemPackages = [ swarmctlConfigured ];
|
|
|
|
systemd.services.swarm-controller = {
|
|
description = "hyperhive swarm-level controller daemon";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" ];
|
|
|
|
serviceConfig = {
|
|
ExecStart = "${cfg.package}/bin/swarm-controller";
|
|
User = "swarm-controller";
|
|
Group = "swarm-controller";
|
|
Restart = "on-failure";
|
|
RestartSec = "5s";
|
|
|
|
# `/run/swarm-controller` — its own directory, holding only the
|
|
# 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";
|
|
# 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.
|
|
RuntimeDirectoryPreserve = "yes";
|
|
|
|
StateDirectory = "swarm-controller";
|
|
StateDirectoryMode = "0750";
|
|
|
|
# Nothing here needs a writable filesystem, real privileges, or a
|
|
# view of the rest of the machine; the daemon reads its socket path
|
|
# from config and serves.
|
|
PrivateTmp = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
NoNewPrivileges = true;
|
|
PrivateDevices = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
RestrictAddressFamilies = [
|
|
"AF_UNIX"
|
|
];
|
|
};
|
|
|
|
environment.SWARM_CONTROLLER_SOCKET = cfg.socketPath;
|
|
};
|
|
};
|
|
}
|