# 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 reaches the socket through a # bind-mount, and a socket that is never bound to an address cannot be # reached from off-host by mistake. { lib, config, ... }: let cfg = config.services.hyperhive.swarm.controller; 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. ''; }; 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. ''; }; 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 one socket and is bind-mounted into one container. Moving this path under a directory that carries anything else — `/run/hyperhive`, which holds the host admin socket, above all — hands whatever else lives there to every consumer that mounts it. Changing this therefore means re-checking the gateway bind-mount, 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 = { }; 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 bind-mount source never vanishes # from under a running gateway container. 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; }; }; }