1 { config, options, pkgs, lib, ... }:
7 cfg = config.services.rspamd;
8 opt = options.services.rspamd;
9 postfixCfg = config.services.postfix;
11 bindSocketOpts = {options, config, ... }: {
15 example = "localhost:11333";
17 Socket for this worker to listen on in a format acceptable by rspamd.
23 description = "Mode to set on unix socket";
27 default = "${cfg.user}";
28 description = "Owner to set on unix socket";
32 default = "${cfg.group}";
33 description = "Group to set on unix socket";
42 optionalString options.${option}.isDefined " ${option}=${config.${option}}";
44 if (!(hasPrefix "/" config.socket)) then "${config.socket}"
45 else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}";
48 traceWarning = w: x: builtins.trace "
\e[1;31mwarning: ${w}
\e[0m" x;
50 workerOpts = { name, options, ... }: {
53 type = types.nullOr types.bool;
55 description = "Whether to run the rspamd worker.";
58 type = types.nullOr types.str;
60 description = "Name of the worker";
63 type = types.nullOr (types.enum [
64 "normal" "controller" "fuzzy" "rspamd_proxy" "lua" "proxy"
67 The type of this worker. The type `proxy` is
68 deprecated and only kept for backwards compatibility and should be
69 replaced with `rspamd_proxy`.
72 from = "services.rspamd.workers.\"${name}\".type";
73 files = options.type.files;
74 warning = "The option `${from}` defined in ${showFiles files} has enum value `proxy` which has been renamed to `rspamd_proxy`";
75 in x: if x == "proxy" then traceWarning warning "rspamd_proxy" else x;
77 bindSockets = mkOption {
78 type = types.listOf (types.either types.str (types.submodule bindSocketOpts));
81 List of sockets to listen, in format acceptable by rspamd
84 socket = "/run/rspamd.sock";
88 apply = value: map (each: if (isString each)
89 then if (isUnixSocket each)
90 then {socket = each; owner = cfg.user; group = cfg.group; mode = "0644"; rawEntry = "${each}";}
91 else {socket = each; rawEntry = "${each}";}
95 type = types.nullOr types.int;
98 Number of worker instances to run
101 includes = mkOption {
102 type = types.listOf types.str;
105 List of files to include in configuration
108 extraConfig = mkOption {
111 description = "Additional entries to put verbatim into worker section of rspamd config file.";
114 config = mkIf (name == "normal" || name == "controller" || name == "fuzzy" || name == "rspamd_proxy") {
115 type = mkDefault name;
116 includes = mkDefault [ "$CONFDIR/worker-${if name == "rspamd_proxy" then "proxy" else name}.inc" ];
121 socket = "/run/rspamd/${name}.sock";
125 in mkDefault (if name == "normal" then [(unixSocket "rspamd")]
126 else if name == "controller" then [ "localhost:11334" ]
127 else if name == "rspamd_proxy" then [ (unixSocket "proxy") ]
132 isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket);
134 mkBindSockets = enabled: socks: concatStringsSep "\n "
135 (flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks));
137 rspamdConfFile = pkgs.writeText "rspamd.conf"
139 .include "$CONFDIR/common.conf"
142 pidfile = "$RUNDIR/rspamd.pid";
143 .include "$CONFDIR/options.inc"
144 .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc"
145 .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc"
150 .include "$CONFDIR/logging.inc"
151 .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc"
152 .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc"
155 ${concatStringsSep "\n" (mapAttrsToList (name: value: let
156 includeName = if name == "rspamd_proxy" then "proxy" else name;
157 tryOverride = boolToString (value.extraConfig == "");
159 worker "${value.type}" {
160 type = "${value.type}";
161 ${optionalString (value.enable != null)
162 "enabled = ${if value.enable != false then "yes" else "no"};"}
163 ${mkBindSockets value.enable value.bindSockets}
164 ${optionalString (value.count != null) "count = ${toString value.count};"}
165 ${concatStringsSep "\n " (map (each: ".include \"${each}\"") value.includes)}
166 .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-${includeName}.inc"
167 .include(try=${tryOverride}; priority=10) "$LOCAL_CONFDIR/override.d/worker-${includeName}.inc"
171 ${optionalString (cfg.extraConfig != "") ''
172 .include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc"
176 filterFiles = files: filterAttrs (n: v: v.enable) files;
177 rspamdDir = pkgs.linkFarm "etc-rspamd-dir" (
178 (mapAttrsToList (name: file: { name = "local.d/${name}"; path = file.source; }) (filterFiles cfg.locals)) ++
179 (mapAttrsToList (name: file: { name = "override.d/${name}"; path = file.source; }) (filterFiles cfg.overrides)) ++
180 (optional (cfg.localLuaRules != null) { name = "rspamd.local.lua"; path = cfg.localLuaRules; }) ++
181 [ { name = "rspamd.conf"; path = rspamdConfFile; } ]
184 configFileModule = prefix: { name, config, ... }: {
190 Whether this file ${prefix} should be generated. This
191 option allows specific ${prefix} files to be disabled.
197 type = types.nullOr types.lines;
198 description = "Text of the file.";
203 description = "Path of the source file.";
207 source = mkIf (config.text != null) (
208 let name' = "rspamd-${prefix}-" + baseNameOf name;
209 in mkDefault (pkgs.writeText name' config.text));
214 (mapAttrs' (n: v: nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" {
215 text = v.extraConfig;
217 (filterAttrs (n: v: v.extraConfig != "") cfg.workers))
218 // (lib.optionalAttrs (cfg.extraConfig != "") {
219 "extra-config.inc".text = cfg.extraConfig;
230 enable = mkEnableOption "rspamd, the Rapid spam filtering system";
235 description = "Whether to run the rspamd daemon in debug mode.";
239 type = with types; attrsOf (submodule (configFileModule "locals"));
242 Local configuration files, written into {file}`/etc/rspamd/local.d/{name}`.
244 example = literalExpression ''
245 { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
246 "arc.conf".text = "allow_envfrom_empty = true;";
251 overrides = mkOption {
252 type = with types; attrsOf (submodule (configFileModule "overrides"));
255 Overridden configuration files, written into {file}`/etc/rspamd/override.d/{name}`.
257 example = literalExpression ''
258 { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
259 "arc.conf".text = "allow_envfrom_empty = true;";
264 localLuaRules = mkOption {
266 type = types.nullOr types.path;
268 Path of file to link to {file}`/etc/rspamd/rspamd.local.lua` for local
274 type = with types; attrsOf (submodule workerOpts);
276 Attribute set of workers to start.
282 example = literalExpression ''
285 includes = [ "$CONFDIR/worker-normal.inc" ];
287 socket = "/run/rspamd/rspamd.sock";
289 owner = "''${config.${opt.user}}";
290 group = "''${config.${opt.group}}";
294 includes = [ "$CONFDIR/worker-controller.inc" ];
295 bindSockets = [ "[::1]:11334" ];
301 extraConfig = mkOption {
305 Extra configuration to add at the end of the rspamd configuration
314 User to use when no root privileges are required.
322 Group to use when no root privileges are required.
330 description = "Add rspamd milter to postfix main.conf";
334 type = with types; attrsOf (oneOf [ bool str (listOf str) ]);
336 Addon to postfix configuration
339 smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
340 non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
348 ###### implementation
350 config = mkIf cfg.enable {
351 services.rspamd.overrides = configOverrides;
352 services.rspamd.workers = mkIf cfg.postfix.enable {
357 socket = "/run/rspamd/rspamd-milter.sock";
359 group = postfixCfg.group;
363 default = yes; # Self-scan upstreams are always default
364 self_scan = yes; # Enable self-scan
369 services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
371 systemd.services.postfix = mkIf cfg.postfix.enable {
372 serviceConfig.SupplementaryGroups = [ postfixCfg.group ];
375 # Allow users to run 'rspamc' and 'rspamadm'.
376 environment.systemPackages = [ pkgs.rspamd ];
378 users.users.${cfg.user} = {
379 description = "rspamd daemon";
380 uid = config.ids.uids.rspamd;
384 users.groups.${cfg.group} = {
385 gid = config.ids.gids.rspamd;
388 environment.etc.rspamd.source = rspamdDir;
390 systemd.services.rspamd = {
391 description = "Rspamd Service";
393 wantedBy = [ "multi-user.target" ];
394 after = [ "network.target" ];
395 restartTriggers = [ rspamdDir ];
398 ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f";
401 User = "${cfg.user}";
402 Group = "${cfg.group}";
403 SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ];
405 RuntimeDirectory = "rspamd";
406 RuntimeDirectoryMode = "0755";
407 StateDirectory = "rspamd";
408 StateDirectoryMode = "0700";
410 AmbientCapabilities = [];
411 CapabilityBoundingSet = "";
412 DevicePolicy = "closed";
413 LockPersonality = true;
414 NoNewPrivileges = true;
415 PrivateDevices = true;
416 PrivateMounts = true;
418 # we need to chown socket to rspamd-milter
419 PrivateUsers = !cfg.postfix.enable;
421 ProtectControlGroups = true;
423 ProtectHostname = true;
424 ProtectKernelLogs = true;
425 ProtectKernelModules = true;
426 ProtectKernelTunables = true;
427 ProtectSystem = "strict";
429 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
430 RestrictNamespaces = true;
431 RestrictRealtime = true;
432 RestrictSUIDSGID = true;
433 SystemCallArchitectures = "native";
434 SystemCallFilter = "@system-service";
440 (mkRemovedOptionModule [ "services" "rspamd" "socketActivation" ]
441 "Socket activation never worked correctly and could at this time not be fixed and so was removed")
442 (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ])
443 (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ])
444 (mkRemovedOptionModule [ "services" "rmilter" ] "Use services.rspamd.* instead to set up milter service")