self: { config, pkgs, lib, ... }: let inherit (lib.modules) mkIf; inherit (lib.options) mkOption mkEnableOption literalExpression; inherit (lib.types) nullOr str port package; defaultPackage = self.packages.${pkgs.stdenv.hostPlatform.system}.troutbot; cfg = config.services.troutbot; in { options.services.troutbot = { enable = mkEnableOption "troutbot"; package = mkOption { type = nullOr package; default = defaultPackage; defaultText = literalExpression "inputs.troutbot.packages.${pkgs.stdenv.hostPlatform.system}.troutbot"; description = '' The Troutbot package to use. By default, this option will use the `packages.default` as exposed by this flake. ''; }; environmentFile = mkOption { type = nullOr str; default = null; description = '' Environment file for specifying additional settings such as secrets. ''; }; configPath = mkOption { type = nullOr str; default = null; description = '' File path containing the Troutbot Typescript config. ''; }; settings = { port = mkOption { type = port; default = 3000; }; user = mkOption { type = str; default = "troutbot"; description = "User to run Troutbot under"; }; group = mkOption { type = str; default = "troutbot"; description = "Group to run Troutbot under"; }; }; }; config = mkIf cfg.enable { users = { groups.${cfg.settings.group} = {}; users.${cfg.settings.user} = { isSystemUser = true; inherit (cfg.settings) group; }; }; systemd.services.troutbot = { description = "Troutbot - GitHub PR/Issue Analysis Bot"; documentation = ["https://github.com/notashelf/troutbot"]; after = ["network-online.target"]; wants = ["network-online.target"]; wantedBy = ["multi-user.target"]; environment = { NODE_ENV = "production"; CONFIG_PATH = cfg.configPath; PORT = toString cfg.port; }; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; ExecStart = "${lib.getExe cfg.package}"; # Restart policy with rate limiting Restart = "on-failure"; RestartSec = "5s"; StartLimitInterval = "60s"; StartLimitBurst = 3; # Timeouts TimeoutStartSec = "30s"; TimeoutStopSec = "30s"; # Working directory and state WorkingDirectory = "/var/lib/troutbot"; StateDirectory = "troutbot"; StateDirectoryMode = "0750"; # Environment file for secrets EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile; # Security hardening # Filesystem restrictions ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; PrivateDevices = true; # Process restrictions NoNewPrivileges = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; ProtectClock = true; # Memory and execution restrictions MemoryDenyWriteExecute = true; RestrictRealtime = true; RestrictSUIDSGID = true; LockPersonality = true; # IPC cleanup RemoveIPC = true; # Capabilities CapabilityBoundingSet = [""]; # no capabilities needed AmbientCapabilities = []; # no ambient capabilities # System call filtering SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; SystemCallErrorNumber = "EPERM"; # Address families, only IPv4/IPv6 needed RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" # for DNS resolution ]; UMask = "0027"; DeviceAllow = []; }; }; serviceConfig = { Type = "simple"; User = cfg.settings.user; Group = cfg.settings.group; ExecStart = "${lib.getExe cfg.package}"; Restart = "on-failure"; EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; WorkingDirectory = "/var/lib/troutbot"; StateDirectory = "troutbot"; ProtectSystem = "strict"; ProtectHome = true; PrivateTmp = true; NoNewPrivileges = true; }; }; }