grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / mail / rspamd.nix
blobc61ddcac954ad568d762285b56fe094cbb0409d0
1 { config, options, pkgs, lib, ... }:
3 with lib;
5 let
7   cfg = config.services.rspamd;
8   opt = options.services.rspamd;
9   postfixCfg = config.services.postfix;
11   bindSocketOpts = {options, config, ... }: {
12     options = {
13       socket = mkOption {
14         type = types.str;
15         example = "localhost:11333";
16         description = ''
17           Socket for this worker to listen on in a format acceptable by rspamd.
18         '';
19       };
20       mode = mkOption {
21         type = types.str;
22         default = "0644";
23         description = "Mode to set on unix socket";
24       };
25       owner = mkOption {
26         type = types.str;
27         default = "${cfg.user}";
28         description = "Owner to set on unix socket";
29       };
30       group = mkOption {
31         type = types.str;
32         default = "${cfg.group}";
33         description = "Group to set on unix socket";
34       };
35       rawEntry = mkOption {
36         type = types.str;
37         internal = true;
38       };
39     };
40     config.rawEntry = let
41       maybeOption = option:
42         optionalString options.${option}.isDefined " ${option}=${config.${option}}";
43     in
44       if (!(hasPrefix "/" config.socket)) then "${config.socket}"
45       else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}";
46   };
48   traceWarning = w: x: builtins.trace "\e[1;31mwarning: ${w}\e[0m" x;
50   workerOpts = { name, options, ... }: {
51     options = {
52       enable = mkOption {
53         type = types.nullOr types.bool;
54         default = null;
55         description = "Whether to run the rspamd worker.";
56       };
57       name = mkOption {
58         type = types.nullOr types.str;
59         default = name;
60         description = "Name of the worker";
61       };
62       type = mkOption {
63         type = types.nullOr (types.enum [
64           "normal" "controller" "fuzzy" "rspamd_proxy" "lua" "proxy"
65         ]);
66         description = ''
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`.
70         '';
71         apply = let
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;
76       };
77       bindSockets = mkOption {
78         type = types.listOf (types.either types.str (types.submodule bindSocketOpts));
79         default = [];
80         description = ''
81           List of sockets to listen, in format acceptable by rspamd
82         '';
83         example = [{
84           socket = "/run/rspamd.sock";
85           mode = "0666";
86           owner = "rspamd";
87         } "*:11333"];
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}";}
92           else each) value;
93       };
94       count = mkOption {
95         type = types.nullOr types.int;
96         default = null;
97         description = ''
98           Number of worker instances to run
99         '';
100       };
101       includes = mkOption {
102         type = types.listOf types.str;
103         default = [];
104         description = ''
105           List of files to include in configuration
106         '';
107       };
108       extraConfig = mkOption {
109         type = types.lines;
110         default = "";
111         description = "Additional entries to put verbatim into worker section of rspamd config file.";
112       };
113     };
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" ];
117       bindSockets =
118         let
119           unixSocket = name: {
120             mode = "0660";
121             socket = "/run/rspamd/${name}.sock";
122             owner = cfg.user;
123             group = cfg.group;
124           };
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") ]
128           else [] );
129     };
130   };
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"
138     ''
139       .include "$CONFDIR/common.conf"
141       options {
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"
146       }
148       logging {
149         type = "syslog";
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"
153       }
155       ${concatStringsSep "\n" (mapAttrsToList (name: value: let
156           includeName = if name == "rspamd_proxy" then "proxy" else name;
157           tryOverride = boolToString (value.extraConfig == "");
158         in ''
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"
168         }
169       '') cfg.workers)}
171       ${optionalString (cfg.extraConfig != "") ''
172         .include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc"
173       ''}
174    '';
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; } ]
182   );
184   configFileModule = prefix: { name, config, ... }: {
185     options = {
186       enable = mkOption {
187         type = types.bool;
188         default = true;
189         description = ''
190           Whether this file ${prefix} should be generated.  This
191           option allows specific ${prefix} files to be disabled.
192         '';
193       };
195       text = mkOption {
196         default = null;
197         type = types.nullOr types.lines;
198         description = "Text of the file.";
199       };
201       source = mkOption {
202         type = types.path;
203         description = "Path of the source file.";
204       };
205     };
206     config = {
207       source = mkIf (config.text != null) (
208         let name' = "rspamd-${prefix}-" + baseNameOf name;
209         in mkDefault (pkgs.writeText name' config.text));
210     };
211   };
213   configOverrides =
214     (mapAttrs' (n: v: nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" {
215       text = v.extraConfig;
216     })
217     (filterAttrs (n: v: v.extraConfig != "") cfg.workers))
218     // (lib.optionalAttrs (cfg.extraConfig != "") {
219       "extra-config.inc".text = cfg.extraConfig;
220     });
224   ###### interface
226   options = {
228     services.rspamd = {
230       enable = mkEnableOption "rspamd, the Rapid spam filtering system";
232       debug = mkOption {
233         type = types.bool;
234         default = false;
235         description = "Whether to run the rspamd daemon in debug mode.";
236       };
238       locals = mkOption {
239         type = with types; attrsOf (submodule (configFileModule "locals"));
240         default = {};
241         description = ''
242           Local configuration files, written into {file}`/etc/rspamd/local.d/{name}`.
243         '';
244         example = literalExpression ''
245           { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
246             "arc.conf".text = "allow_envfrom_empty = true;";
247           }
248         '';
249       };
251       overrides = mkOption {
252         type = with types; attrsOf (submodule (configFileModule "overrides"));
253         default = {};
254         description = ''
255           Overridden configuration files, written into {file}`/etc/rspamd/override.d/{name}`.
256         '';
257         example = literalExpression ''
258           { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
259             "arc.conf".text = "allow_envfrom_empty = true;";
260           }
261         '';
262       };
264       localLuaRules = mkOption {
265         default = null;
266         type = types.nullOr types.path;
267         description = ''
268           Path of file to link to {file}`/etc/rspamd/rspamd.local.lua` for local
269           rules written in Lua
270         '';
271       };
273       workers = mkOption {
274         type = with types; attrsOf (submodule workerOpts);
275         description = ''
276           Attribute set of workers to start.
277         '';
278         default = {
279           normal = {};
280           controller = {};
281         };
282         example = literalExpression ''
283           {
284             normal = {
285               includes = [ "$CONFDIR/worker-normal.inc" ];
286               bindSockets = [{
287                 socket = "/run/rspamd/rspamd.sock";
288                 mode = "0660";
289                 owner = "''${config.${opt.user}}";
290                 group = "''${config.${opt.group}}";
291               }];
292             };
293             controller = {
294               includes = [ "$CONFDIR/worker-controller.inc" ];
295               bindSockets = [ "[::1]:11334" ];
296             };
297           }
298         '';
299       };
301       extraConfig = mkOption {
302         type = types.lines;
303         default = "";
304         description = ''
305           Extra configuration to add at the end of the rspamd configuration
306           file.
307         '';
308       };
310       user = mkOption {
311         type = types.str;
312         default = "rspamd";
313         description = ''
314           User to use when no root privileges are required.
315         '';
316       };
318       group = mkOption {
319         type = types.str;
320         default = "rspamd";
321         description = ''
322           Group to use when no root privileges are required.
323         '';
324       };
326       postfix = {
327         enable = mkOption {
328           type = types.bool;
329           default = false;
330           description = "Add rspamd milter to postfix main.conf";
331         };
333         config = mkOption {
334           type = with types; attrsOf (oneOf [ bool str (listOf str) ]);
335           description = ''
336             Addon to postfix configuration
337           '';
338           default = {
339             smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
340             non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
341           };
342         };
343       };
344     };
345   };
348   ###### implementation
350   config = mkIf cfg.enable {
351     services.rspamd.overrides = configOverrides;
352     services.rspamd.workers = mkIf cfg.postfix.enable {
353       controller = {};
354       rspamd_proxy = {
355         bindSockets = [ {
356           mode = "0660";
357           socket = "/run/rspamd/rspamd-milter.sock";
358           owner = cfg.user;
359           group = postfixCfg.group;
360         } ];
361         extraConfig = ''
362           upstream "local" {
363             default = yes; # Self-scan upstreams are always default
364             self_scan = yes; # Enable self-scan
365           }
366         '';
367       };
368     };
369     services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
371     systemd.services.postfix = mkIf cfg.postfix.enable {
372       serviceConfig.SupplementaryGroups = [ postfixCfg.group ];
373     };
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;
381       group = cfg.group;
382     };
384     users.groups.${cfg.group} = {
385       gid = config.ids.gids.rspamd;
386     };
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 ];
397       serviceConfig = {
398         ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f";
399         Restart = "always";
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;
417         PrivateTmp = true;
418         # we need to chown socket to rspamd-milter
419         PrivateUsers = !cfg.postfix.enable;
420         ProtectClock = true;
421         ProtectControlGroups = true;
422         ProtectHome = true;
423         ProtectHostname = true;
424         ProtectKernelLogs = true;
425         ProtectKernelModules = true;
426         ProtectKernelTunables = true;
427         ProtectSystem = "strict";
428         RemoveIPC = true;
429         RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
430         RestrictNamespaces = true;
431         RestrictRealtime = true;
432         RestrictSUIDSGID = true;
433         SystemCallArchitectures = "native";
434         SystemCallFilter = "@system-service";
435         UMask = "0077";
436       };
437     };
438   };
439   imports = [
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")
445   ];