diff options
Diffstat (limited to 'nixos/modules/services/mail/rspamd.nix')
-rw-r--r-- | nixos/modules/services/mail/rspamd.nix | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/nixos/modules/services/mail/rspamd.nix b/nixos/modules/services/mail/rspamd.nix new file mode 100644 index 00000000000..a570e137a55 --- /dev/null +++ b/nixos/modules/services/mail/rspamd.nix @@ -0,0 +1,446 @@ +{ config, options, pkgs, lib, ... }: + +with lib; + +let + + cfg = config.services.rspamd; + opt = options.services.rspamd; + postfixCfg = config.services.postfix; + + bindSocketOpts = {options, config, ... }: { + options = { + socket = mkOption { + type = types.str; + example = "localhost:11333"; + description = '' + Socket for this worker to listen on in a format acceptable by rspamd. + ''; + }; + mode = mkOption { + type = types.str; + default = "0644"; + description = "Mode to set on unix socket"; + }; + owner = mkOption { + type = types.str; + default = "${cfg.user}"; + description = "Owner to set on unix socket"; + }; + group = mkOption { + type = types.str; + default = "${cfg.group}"; + description = "Group to set on unix socket"; + }; + rawEntry = mkOption { + type = types.str; + internal = true; + }; + }; + config.rawEntry = let + maybeOption = option: + optionalString options.${option}.isDefined " ${option}=${config.${option}}"; + in + if (!(hasPrefix "/" config.socket)) then "${config.socket}" + else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}"; + }; + + traceWarning = w: x: builtins.trace "[1;31mwarning: ${w}[0m" x; + + workerOpts = { name, options, ... }: { + options = { + enable = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether to run the rspamd worker."; + }; + name = mkOption { + type = types.nullOr types.str; + default = name; + description = "Name of the worker"; + }; + type = mkOption { + type = types.nullOr (types.enum [ + "normal" "controller" "fuzzy" "rspamd_proxy" "lua" "proxy" + ]); + description = '' + The type of this worker. The type <literal>proxy</literal> is + deprecated and only kept for backwards compatibility and should be + replaced with <literal>rspamd_proxy</literal>. + ''; + apply = let + from = "services.rspamd.workers.\"${name}\".type"; + files = options.type.files; + warning = "The option `${from}` defined in ${showFiles files} has enum value `proxy` which has been renamed to `rspamd_proxy`"; + in x: if x == "proxy" then traceWarning warning "rspamd_proxy" else x; + }; + bindSockets = mkOption { + type = types.listOf (types.either types.str (types.submodule bindSocketOpts)); + default = []; + description = '' + List of sockets to listen, in format acceptable by rspamd + ''; + example = [{ + socket = "/run/rspamd.sock"; + mode = "0666"; + owner = "rspamd"; + } "*:11333"]; + apply = value: map (each: if (isString each) + then if (isUnixSocket each) + then {socket = each; owner = cfg.user; group = cfg.group; mode = "0644"; rawEntry = "${each}";} + else {socket = each; rawEntry = "${each}";} + else each) value; + }; + count = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Number of worker instances to run + ''; + }; + includes = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of files to include in configuration + ''; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Additional entries to put verbatim into worker section of rspamd config file."; + }; + }; + config = mkIf (name == "normal" || name == "controller" || name == "fuzzy" || name == "rspamd_proxy") { + type = mkDefault name; + includes = mkDefault [ "$CONFDIR/worker-${if name == "rspamd_proxy" then "proxy" else name}.inc" ]; + bindSockets = + let + unixSocket = name: { + mode = "0660"; + socket = "/run/rspamd/${name}.sock"; + owner = cfg.user; + group = cfg.group; + }; + in mkDefault (if name == "normal" then [(unixSocket "rspamd")] + else if name == "controller" then [ "localhost:11334" ] + else if name == "rspamd_proxy" then [ (unixSocket "proxy") ] + else [] ); + }; + }; + + isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket); + + mkBindSockets = enabled: socks: concatStringsSep "\n " + (flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks)); + + rspamdConfFile = pkgs.writeText "rspamd.conf" + '' + .include "$CONFDIR/common.conf" + + options { + pidfile = "$RUNDIR/rspamd.pid"; + .include "$CONFDIR/options.inc" + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc" + } + + logging { + type = "syslog"; + .include "$CONFDIR/logging.inc" + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc" + } + + ${concatStringsSep "\n" (mapAttrsToList (name: value: let + includeName = if name == "rspamd_proxy" then "proxy" else name; + tryOverride = boolToString (value.extraConfig == ""); + in '' + worker "${value.type}" { + type = "${value.type}"; + ${optionalString (value.enable != null) + "enabled = ${if value.enable != false then "yes" else "no"};"} + ${mkBindSockets value.enable value.bindSockets} + ${optionalString (value.count != null) "count = ${toString value.count};"} + ${concatStringsSep "\n " (map (each: ".include \"${each}\"") value.includes)} + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-${includeName}.inc" + .include(try=${tryOverride}; priority=10) "$LOCAL_CONFDIR/override.d/worker-${includeName}.inc" + } + '') cfg.workers)} + + ${optionalString (cfg.extraConfig != "") '' + .include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc" + ''} + ''; + + filterFiles = files: filterAttrs (n: v: v.enable) files; + rspamdDir = pkgs.linkFarm "etc-rspamd-dir" ( + (mapAttrsToList (name: file: { name = "local.d/${name}"; path = file.source; }) (filterFiles cfg.locals)) ++ + (mapAttrsToList (name: file: { name = "override.d/${name}"; path = file.source; }) (filterFiles cfg.overrides)) ++ + (optional (cfg.localLuaRules != null) { name = "rspamd.local.lua"; path = cfg.localLuaRules; }) ++ + [ { name = "rspamd.conf"; path = rspamdConfFile; } ] + ); + + configFileModule = prefix: { name, config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether this file ${prefix} should be generated. This + option allows specific ${prefix} files to be disabled. + ''; + }; + + text = mkOption { + default = null; + type = types.nullOr types.lines; + description = "Text of the file."; + }; + + source = mkOption { + type = types.path; + description = "Path of the source file."; + }; + }; + config = { + source = mkIf (config.text != null) ( + let name' = "rspamd-${prefix}-" + baseNameOf name; + in mkDefault (pkgs.writeText name' config.text)); + }; + }; + + configOverrides = + (mapAttrs' (n: v: nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" { + text = v.extraConfig; + }) + (filterAttrs (n: v: v.extraConfig != "") cfg.workers)) + // (if cfg.extraConfig == "" then {} else { + "extra-config.inc".text = cfg.extraConfig; + }); +in + +{ + ###### interface + + options = { + + services.rspamd = { + + enable = mkEnableOption "rspamd, the Rapid spam filtering system"; + + debug = mkOption { + type = types.bool; + default = false; + description = "Whether to run the rspamd daemon in debug mode."; + }; + + locals = mkOption { + type = with types; attrsOf (submodule (configFileModule "locals")); + default = {}; + description = '' + Local configuration files, written into <filename>/etc/rspamd/local.d/{name}</filename>. + ''; + example = literalExpression '' + { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf"; + "arc.conf".text = "allow_envfrom_empty = true;"; + } + ''; + }; + + overrides = mkOption { + type = with types; attrsOf (submodule (configFileModule "overrides")); + default = {}; + description = '' + Overridden configuration files, written into <filename>/etc/rspamd/override.d/{name}</filename>. + ''; + example = literalExpression '' + { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf"; + "arc.conf".text = "allow_envfrom_empty = true;"; + } + ''; + }; + + localLuaRules = mkOption { + default = null; + type = types.nullOr types.path; + description = '' + Path of file to link to <filename>/etc/rspamd/rspamd.local.lua</filename> for local + rules written in Lua + ''; + }; + + workers = mkOption { + type = with types; attrsOf (submodule workerOpts); + description = '' + Attribute set of workers to start. + ''; + default = { + normal = {}; + controller = {}; + }; + example = literalExpression '' + { + normal = { + includes = [ "$CONFDIR/worker-normal.inc" ]; + bindSockets = [{ + socket = "/run/rspamd/rspamd.sock"; + mode = "0660"; + owner = "''${config.${opt.user}}"; + group = "''${config.${opt.group}}"; + }]; + }; + controller = { + includes = [ "$CONFDIR/worker-controller.inc" ]; + bindSockets = [ "[::1]:11334" ]; + }; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration to add at the end of the rspamd configuration + file. + ''; + }; + + user = mkOption { + type = types.str; + default = "rspamd"; + description = '' + User to use when no root privileges are required. + ''; + }; + + group = mkOption { + type = types.str; + default = "rspamd"; + description = '' + Group to use when no root privileges are required. + ''; + }; + + postfix = { + enable = mkOption { + type = types.bool; + default = false; + description = "Add rspamd milter to postfix main.conf"; + }; + + config = mkOption { + type = with types; attrsOf (oneOf [ bool str (listOf str) ]); + description = '' + Addon to postfix configuration + ''; + default = { + smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"]; + non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"]; + }; + }; + }; + }; + }; + + + ###### implementation + + config = mkIf cfg.enable { + services.rspamd.overrides = configOverrides; + services.rspamd.workers = mkIf cfg.postfix.enable { + controller = {}; + rspamd_proxy = { + bindSockets = [ { + mode = "0660"; + socket = "/run/rspamd/rspamd-milter.sock"; + owner = cfg.user; + group = postfixCfg.group; + } ]; + extraConfig = '' + upstream "local" { + default = yes; # Self-scan upstreams are always default + self_scan = yes; # Enable self-scan + } + ''; + }; + }; + services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config; + + systemd.services.postfix = mkIf cfg.postfix.enable { + serviceConfig.SupplementaryGroups = [ postfixCfg.group ]; + }; + + # Allow users to run 'rspamc' and 'rspamadm'. + environment.systemPackages = [ pkgs.rspamd ]; + + users.users.${cfg.user} = { + description = "rspamd daemon"; + uid = config.ids.uids.rspamd; + group = cfg.group; + }; + + users.groups.${cfg.group} = { + gid = config.ids.gids.rspamd; + }; + + environment.etc.rspamd.source = rspamdDir; + + systemd.services.rspamd = { + description = "Rspamd Service"; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + restartTriggers = [ rspamdDir ]; + + serviceConfig = { + ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f"; + Restart = "always"; + + User = "${cfg.user}"; + Group = "${cfg.group}"; + SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ]; + + RuntimeDirectory = "rspamd"; + RuntimeDirectoryMode = "0755"; + StateDirectory = "rspamd"; + StateDirectoryMode = "0700"; + + AmbientCapabilities = []; + CapabilityBoundingSet = ""; + DevicePolicy = "closed"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + # we need to chown socket to rspamd-milter + PrivateUsers = !cfg.postfix.enable; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = "@system-service"; + UMask = "0077"; + }; + }; + }; + imports = [ + (mkRemovedOptionModule [ "services" "rspamd" "socketActivation" ] + "Socket activation never worked correctly and could at this time not be fixed and so was removed") + (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ]) + (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ]) + (mkRemovedOptionModule [ "services" "rmilter" ] "Use services.rspamd.* instead to set up milter service") + ]; +} |