troutbot/nix/modules/nixos.nix
NotAShelf 70443c83ce
nix: harden Systemd service in NixOS module
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I086cdbfcd0a0d1d87173a2cf97f5b5416a6a6964
2026-02-01 19:07:34 +03:00

173 lines
4.4 KiB
Nix

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;
};
};
}