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