1{ config, options, pkgs, lib, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.rspamd;
8 opt = options.services.rspamd;
9 postfixCfg = config.services.postfix;
10
11 bindSocketOpts = {options, config, ... }: {
12 options = {
13 socket = mkOption {
14 type = types.str;
15 example = "localhost:11333";
16 description = lib.mdDoc ''
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 = lib.mdDoc "Mode to set on unix socket";
24 };
25 owner = mkOption {
26 type = types.str;
27 default = "${cfg.user}";
28 description = lib.mdDoc "Owner to set on unix socket";
29 };
30 group = mkOption {
31 type = types.str;
32 default = "${cfg.group}";
33 description = lib.mdDoc "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 };
47
48 traceWarning = w: x: builtins.trace "[1;31mwarning: ${w}[0m" x;
49
50 workerOpts = { name, options, ... }: {
51 options = {
52 enable = mkOption {
53 type = types.nullOr types.bool;
54 default = null;
55 description = lib.mdDoc "Whether to run the rspamd worker.";
56 };
57 name = mkOption {
58 type = types.nullOr types.str;
59 default = name;
60 description = lib.mdDoc "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 = lib.mdDoc ''
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 = lib.mdDoc ''
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 = lib.mdDoc ''
98 Number of worker instances to run
99 '';
100 };
101 includes = mkOption {
102 type = types.listOf types.str;
103 default = [];
104 description = lib.mdDoc ''
105 List of files to include in configuration
106 '';
107 };
108 extraConfig = mkOption {
109 type = types.lines;
110 default = "";
111 description = lib.mdDoc "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 };
131
132 isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket);
133
134 mkBindSockets = enabled: socks: concatStringsSep "\n "
135 (flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks));
136
137 rspamdConfFile = pkgs.writeText "rspamd.conf"
138 ''
139 .include "$CONFDIR/common.conf"
140
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 }
147
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 }
154
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)}
170
171 ${optionalString (cfg.extraConfig != "") ''
172 .include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc"
173 ''}
174 '';
175
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 );
183
184 configFileModule = prefix: { name, config, ... }: {
185 options = {
186 enable = mkOption {
187 type = types.bool;
188 default = true;
189 description = lib.mdDoc ''
190 Whether this file ${prefix} should be generated. This
191 option allows specific ${prefix} files to be disabled.
192 '';
193 };
194
195 text = mkOption {
196 default = null;
197 type = types.nullOr types.lines;
198 description = lib.mdDoc "Text of the file.";
199 };
200
201 source = mkOption {
202 type = types.path;
203 description = lib.mdDoc "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 };
212
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 // (if cfg.extraConfig == "" then {} else {
219 "extra-config.inc".text = cfg.extraConfig;
220 });
221in
222
223{
224 ###### interface
225
226 options = {
227
228 services.rspamd = {
229
230 enable = mkEnableOption (lib.mdDoc "rspamd, the Rapid spam filtering system");
231
232 debug = mkOption {
233 type = types.bool;
234 default = false;
235 description = lib.mdDoc "Whether to run the rspamd daemon in debug mode.";
236 };
237
238 locals = mkOption {
239 type = with types; attrsOf (submodule (configFileModule "locals"));
240 default = {};
241 description = lib.mdDoc ''
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 };
250
251 overrides = mkOption {
252 type = with types; attrsOf (submodule (configFileModule "overrides"));
253 default = {};
254 description = lib.mdDoc ''
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 };
263
264 localLuaRules = mkOption {
265 default = null;
266 type = types.nullOr types.path;
267 description = lib.mdDoc ''
268 Path of file to link to {file}`/etc/rspamd/rspamd.local.lua` for local
269 rules written in Lua
270 '';
271 };
272
273 workers = mkOption {
274 type = with types; attrsOf (submodule workerOpts);
275 description = lib.mdDoc ''
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 };
300
301 extraConfig = mkOption {
302 type = types.lines;
303 default = "";
304 description = lib.mdDoc ''
305 Extra configuration to add at the end of the rspamd configuration
306 file.
307 '';
308 };
309
310 user = mkOption {
311 type = types.str;
312 default = "rspamd";
313 description = lib.mdDoc ''
314 User to use when no root privileges are required.
315 '';
316 };
317
318 group = mkOption {
319 type = types.str;
320 default = "rspamd";
321 description = lib.mdDoc ''
322 Group to use when no root privileges are required.
323 '';
324 };
325
326 postfix = {
327 enable = mkOption {
328 type = types.bool;
329 default = false;
330 description = lib.mdDoc "Add rspamd milter to postfix main.conf";
331 };
332
333 config = mkOption {
334 type = with types; attrsOf (oneOf [ bool str (listOf str) ]);
335 description = lib.mdDoc ''
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 };
346
347
348 ###### implementation
349
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;
370
371 systemd.services.postfix = mkIf cfg.postfix.enable {
372 serviceConfig.SupplementaryGroups = [ postfixCfg.group ];
373 };
374
375 # Allow users to run 'rspamc' and 'rspamadm'.
376 environment.systemPackages = [ pkgs.rspamd ];
377
378 users.users.${cfg.user} = {
379 description = "rspamd daemon";
380 uid = config.ids.uids.rspamd;
381 group = cfg.group;
382 };
383
384 users.groups.${cfg.group} = {
385 gid = config.ids.gids.rspamd;
386 };
387
388 environment.etc.rspamd.source = rspamdDir;
389
390 systemd.services.rspamd = {
391 description = "Rspamd Service";
392
393 wantedBy = [ "multi-user.target" ];
394 after = [ "network.target" ];
395 restartTriggers = [ rspamdDir ];
396
397 serviceConfig = {
398 ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f";
399 Restart = "always";
400
401 User = "${cfg.user}";
402 Group = "${cfg.group}";
403 SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ];
404
405 RuntimeDirectory = "rspamd";
406 RuntimeDirectoryMode = "0755";
407 StateDirectory = "rspamd";
408 StateDirectoryMode = "0700";
409
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 ];
446}