1{
2 config,
3 options,
4 pkgs,
5 lib,
6 ...
7}:
8
9with lib;
10
11let
12
13 cfg = config.services.rspamd;
14 opt = options.services.rspamd;
15 postfixCfg = config.services.postfix;
16
17 bindSocketOpts =
18 { options, config, ... }:
19 {
20 options = {
21 socket = mkOption {
22 type = types.str;
23 example = "localhost:11333";
24 description = ''
25 Socket for this worker to listen on in a format acceptable by rspamd.
26 '';
27 };
28 mode = mkOption {
29 type = types.str;
30 default = "0644";
31 description = "Mode to set on unix socket";
32 };
33 owner = mkOption {
34 type = types.str;
35 default = "${cfg.user}";
36 description = "Owner to set on unix socket";
37 };
38 group = mkOption {
39 type = types.str;
40 default = "${cfg.group}";
41 description = "Group to set on unix socket";
42 };
43 rawEntry = mkOption {
44 type = types.str;
45 internal = true;
46 };
47 };
48 config.rawEntry =
49 let
50 maybeOption = option: optionalString options.${option}.isDefined " ${option}=${config.${option}}";
51 in
52 if (!(hasPrefix "/" config.socket)) then
53 "${config.socket}"
54 else
55 "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}";
56 };
57
58 traceWarning = w: x: builtins.trace "[1;31mwarning: ${w}[0m" x;
59
60 workerOpts =
61 { name, options, ... }:
62 {
63 options = {
64 enable = mkOption {
65 type = types.nullOr types.bool;
66 default = null;
67 description = "Whether to run the rspamd worker.";
68 };
69 name = mkOption {
70 type = types.nullOr types.str;
71 default = name;
72 description = "Name of the worker";
73 };
74 type = mkOption {
75 type = types.nullOr (
76 types.enum [
77 "normal"
78 "controller"
79 "fuzzy"
80 "rspamd_proxy"
81 "lua"
82 "proxy"
83 ]
84 );
85 description = ''
86 The type of this worker. The type `proxy` is
87 deprecated and only kept for backwards compatibility and should be
88 replaced with `rspamd_proxy`.
89 '';
90 apply =
91 let
92 from = "services.rspamd.workers.\"${name}\".type";
93 files = options.type.files;
94 warning = "The option `${from}` defined in ${showFiles files} has enum value `proxy` which has been renamed to `rspamd_proxy`";
95 in
96 x: if x == "proxy" then traceWarning warning "rspamd_proxy" else x;
97 };
98 bindSockets = mkOption {
99 type = types.listOf (types.either types.str (types.submodule bindSocketOpts));
100 default = [ ];
101 description = ''
102 List of sockets to listen, in format acceptable by rspamd
103 '';
104 example = [
105 {
106 socket = "/run/rspamd.sock";
107 mode = "0666";
108 owner = "rspamd";
109 }
110 "*:11333"
111 ];
112 apply =
113 value:
114 map (
115 each:
116 if (isString each) then
117 if (isUnixSocket each) then
118 {
119 socket = each;
120 owner = cfg.user;
121 group = cfg.group;
122 mode = "0644";
123 rawEntry = "${each}";
124 }
125 else
126 {
127 socket = each;
128 rawEntry = "${each}";
129 }
130 else
131 each
132 ) value;
133 };
134 count = mkOption {
135 type = types.nullOr types.int;
136 default = null;
137 description = ''
138 Number of worker instances to run
139 '';
140 };
141 includes = mkOption {
142 type = types.listOf types.str;
143 default = [ ];
144 description = ''
145 List of files to include in configuration
146 '';
147 };
148 extraConfig = mkOption {
149 type = types.lines;
150 default = "";
151 description = "Additional entries to put verbatim into worker section of rspamd config file.";
152 };
153 };
154 config =
155 mkIf (name == "normal" || name == "controller" || name == "fuzzy" || name == "rspamd_proxy")
156 {
157 type = mkDefault name;
158 includes = mkDefault [ "$CONFDIR/worker-${if name == "rspamd_proxy" then "proxy" else name}.inc" ];
159 bindSockets =
160 let
161 unixSocket = name: {
162 mode = "0660";
163 socket = "/run/rspamd/${name}.sock";
164 owner = cfg.user;
165 group = cfg.group;
166 };
167 in
168 mkDefault (
169 if name == "normal" then
170 [ (unixSocket "rspamd") ]
171 else if name == "controller" then
172 [ "localhost:11334" ]
173 else if name == "rspamd_proxy" then
174 [ (unixSocket "proxy") ]
175 else
176 [ ]
177 );
178 };
179 };
180
181 isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket);
182
183 mkBindSockets =
184 enabled: socks:
185 concatStringsSep "\n " (flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks));
186
187 rspamdConfFile = pkgs.writeText "rspamd.conf" ''
188 .include "$CONFDIR/common.conf"
189
190 options {
191 pidfile = "$RUNDIR/rspamd.pid";
192 .include "$CONFDIR/options.inc"
193 .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc"
194 .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc"
195 }
196
197 logging {
198 type = "syslog";
199 .include "$CONFDIR/logging.inc"
200 .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc"
201 .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc"
202 }
203
204 ${concatStringsSep "\n" (
205 mapAttrsToList (
206 name: value:
207 let
208 includeName = if name == "rspamd_proxy" then "proxy" else name;
209 tryOverride = boolToString (value.extraConfig == "");
210 in
211 ''
212 worker "${value.type}" {
213 type = "${value.type}";
214 ${optionalString (value.enable != null)
215 "enabled = ${if value.enable != false then "yes" else "no"};"
216 }
217 ${mkBindSockets value.enable value.bindSockets}
218 ${optionalString (value.count != null) "count = ${toString value.count};"}
219 ${concatStringsSep "\n " (map (each: ".include \"${each}\"") value.includes)}
220 .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-${includeName}.inc"
221 .include(try=${tryOverride}; priority=10) "$LOCAL_CONFDIR/override.d/worker-${includeName}.inc"
222 }
223 ''
224 ) cfg.workers
225 )}
226
227 ${optionalString (cfg.extraConfig != "") ''
228 .include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc"
229 ''}
230 '';
231
232 filterFiles = files: filterAttrs (n: v: v.enable) files;
233 rspamdDir = pkgs.linkFarm "etc-rspamd-dir" (
234 (mapAttrsToList (name: file: {
235 name = "local.d/${name}";
236 path = file.source;
237 }) (filterFiles cfg.locals))
238 ++ (mapAttrsToList (name: file: {
239 name = "override.d/${name}";
240 path = file.source;
241 }) (filterFiles cfg.overrides))
242 ++ (optional (cfg.localLuaRules != null) {
243 name = "rspamd.local.lua";
244 path = cfg.localLuaRules;
245 })
246 ++ [
247 {
248 name = "rspamd.conf";
249 path = rspamdConfFile;
250 }
251 ]
252 );
253
254 configFileModule =
255 prefix:
256 { name, config, ... }:
257 {
258 options = {
259 enable = mkOption {
260 type = types.bool;
261 default = true;
262 description = ''
263 Whether this file ${prefix} should be generated. This
264 option allows specific ${prefix} files to be disabled.
265 '';
266 };
267
268 text = mkOption {
269 default = null;
270 type = types.nullOr types.lines;
271 description = "Text of the file.";
272 };
273
274 source = mkOption {
275 type = types.path;
276 description = "Path of the source file.";
277 };
278 };
279 config = {
280 source = mkIf (config.text != null) (
281 let
282 name' = "rspamd-${prefix}-" + baseNameOf name;
283 in
284 mkDefault (pkgs.writeText name' config.text)
285 );
286 };
287 };
288
289 configOverrides =
290 (mapAttrs' (
291 n: v:
292 nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" {
293 text = v.extraConfig;
294 }
295 ) (filterAttrs (n: v: v.extraConfig != "") cfg.workers))
296 // (lib.optionalAttrs (cfg.extraConfig != "") {
297 "extra-config.inc".text = cfg.extraConfig;
298 });
299in
300
301{
302 ###### interface
303
304 options = {
305
306 services.rspamd = {
307
308 enable = mkEnableOption "rspamd, the Rapid spam filtering system";
309
310 debug = mkOption {
311 type = types.bool;
312 default = false;
313 description = "Whether to run the rspamd daemon in debug mode.";
314 };
315
316 locals = mkOption {
317 type = with types; attrsOf (submodule (configFileModule "locals"));
318 default = { };
319 description = ''
320 Local configuration files, written into {file}`/etc/rspamd/local.d/{name}`.
321 '';
322 example = literalExpression ''
323 { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
324 "arc.conf".text = "allow_envfrom_empty = true;";
325 }
326 '';
327 };
328
329 overrides = mkOption {
330 type = with types; attrsOf (submodule (configFileModule "overrides"));
331 default = { };
332 description = ''
333 Overridden configuration files, written into {file}`/etc/rspamd/override.d/{name}`.
334 '';
335 example = literalExpression ''
336 { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
337 "arc.conf".text = "allow_envfrom_empty = true;";
338 }
339 '';
340 };
341
342 localLuaRules = mkOption {
343 default = null;
344 type = types.nullOr types.path;
345 description = ''
346 Path of file to link to {file}`/etc/rspamd/rspamd.local.lua` for local
347 rules written in Lua
348 '';
349 };
350
351 workers = mkOption {
352 type = with types; attrsOf (submodule workerOpts);
353 description = ''
354 Attribute set of workers to start.
355 '';
356 default = {
357 normal = { };
358 controller = { };
359 };
360 example = literalExpression ''
361 {
362 normal = {
363 includes = [ "$CONFDIR/worker-normal.inc" ];
364 bindSockets = [{
365 socket = "/run/rspamd/rspamd.sock";
366 mode = "0660";
367 owner = "''${config.${opt.user}}";
368 group = "''${config.${opt.group}}";
369 }];
370 };
371 controller = {
372 includes = [ "$CONFDIR/worker-controller.inc" ];
373 bindSockets = [ "[::1]:11334" ];
374 };
375 }
376 '';
377 };
378
379 extraConfig = mkOption {
380 type = types.lines;
381 default = "";
382 description = ''
383 Extra configuration to add at the end of the rspamd configuration
384 file.
385 '';
386 };
387
388 user = mkOption {
389 type = types.str;
390 default = "rspamd";
391 description = ''
392 User to use when no root privileges are required.
393 '';
394 };
395
396 group = mkOption {
397 type = types.str;
398 default = "rspamd";
399 description = ''
400 Group to use when no root privileges are required.
401 '';
402 };
403
404 postfix = {
405 enable = mkOption {
406 type = types.bool;
407 default = false;
408 description = "Add rspamd milter to postfix main.conf";
409 };
410
411 config = mkOption {
412 type =
413 with types;
414 attrsOf (oneOf [
415 bool
416 str
417 (listOf str)
418 ]);
419 description = ''
420 Addon to postfix configuration
421 '';
422 default = {
423 smtpd_milters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
424 non_smtpd_milters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
425 };
426 };
427 };
428 };
429 };
430
431 ###### implementation
432
433 config = mkIf cfg.enable {
434 services.rspamd.overrides = configOverrides;
435 services.rspamd.workers = mkIf cfg.postfix.enable {
436 controller = { };
437 rspamd_proxy = {
438 bindSockets = [
439 {
440 mode = "0660";
441 socket = "/run/rspamd/rspamd-milter.sock";
442 owner = cfg.user;
443 group = postfixCfg.group;
444 }
445 ];
446 extraConfig = ''
447 upstream "local" {
448 default = yes; # Self-scan upstreams are always default
449 self_scan = yes; # Enable self-scan
450 }
451 '';
452 };
453 };
454 services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
455
456 systemd.services.postfix = mkIf cfg.postfix.enable {
457 serviceConfig.SupplementaryGroups = [ postfixCfg.group ];
458 };
459
460 # Allow users to run 'rspamc' and 'rspamadm'.
461 environment.systemPackages = [ pkgs.rspamd ];
462
463 users.users.${cfg.user} = {
464 description = "rspamd daemon";
465 uid = config.ids.uids.rspamd;
466 group = cfg.group;
467 };
468
469 users.groups.${cfg.group} = {
470 gid = config.ids.gids.rspamd;
471 };
472
473 environment.etc.rspamd.source = rspamdDir;
474
475 systemd.services.rspamd = {
476 description = "Rspamd Service";
477
478 wantedBy = [ "multi-user.target" ];
479 after = [ "network.target" ];
480 restartTriggers = [ rspamdDir ];
481
482 serviceConfig = {
483 ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f";
484 Restart = "always";
485
486 User = "${cfg.user}";
487 Group = "${cfg.group}";
488 SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ];
489
490 RuntimeDirectory = "rspamd";
491 RuntimeDirectoryMode = "0755";
492 StateDirectory = "rspamd";
493 StateDirectoryMode = "0700";
494
495 AmbientCapabilities = [ ];
496 CapabilityBoundingSet = "";
497 DevicePolicy = "closed";
498 LockPersonality = true;
499 NoNewPrivileges = true;
500 PrivateDevices = true;
501 PrivateMounts = true;
502 PrivateTmp = true;
503 # we need to chown socket to rspamd-milter
504 PrivateUsers = !cfg.postfix.enable;
505 ProtectClock = true;
506 ProtectControlGroups = true;
507 ProtectHome = true;
508 ProtectHostname = true;
509 ProtectKernelLogs = true;
510 ProtectKernelModules = true;
511 ProtectKernelTunables = true;
512 ProtectSystem = "strict";
513 RemoveIPC = true;
514 RestrictAddressFamilies = [
515 "AF_INET"
516 "AF_INET6"
517 "AF_UNIX"
518 ];
519 RestrictNamespaces = true;
520 RestrictRealtime = true;
521 RestrictSUIDSGID = true;
522 SystemCallArchitectures = "native";
523 SystemCallFilter = "@system-service";
524 UMask = "0077";
525 };
526 };
527 };
528 imports = [
529 (mkRemovedOptionModule [
530 "services"
531 "rspamd"
532 "socketActivation"
533 ] "Socket activation never worked correctly and could at this time not be fixed and so was removed")
534 (mkRenamedOptionModule
535 [ "services" "rspamd" "bindSocket" ]
536 [ "services" "rspamd" "workers" "normal" "bindSockets" ]
537 )
538 (mkRenamedOptionModule
539 [ "services" "rspamd" "bindUISocket" ]
540 [ "services" "rspamd" "workers" "controller" "bindSockets" ]
541 )
542 (mkRemovedOptionModule [
543 "services"
544 "rmilter"
545 ] "Use services.rspamd.* instead to set up milter service")
546 ];
547}