]> Git — Sourcephile - sourcephile-nix.git/blob - install/overlays/servers/mail/rspamd/service.nix
nix: improve bootstrap/mermet/ upto ssh root@
[sourcephile-nix.git] / install / overlays / servers / mail / rspamd / service.nix
1 { config, options, pkgs, lib, ... }:
2
3 with lib;
4
5 let
6
7 cfg = config.services.rspamd-upstream;
8 opts = options.services.rspamd-upstream;
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 = ''
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 };
47
48 traceWarning = w: x: builtins.trace "\e[1;31mwarning: ${w}\e[0m" x;
49
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_storage" "rspamd_proxy" "lua" "proxy"
65 ]);
66 description = ''
67 The type of this worker. The type <literal>proxy</literal> is
68 deprecated and only kept for backwards compatibility and should be
69 replaced with <literal>rspamd_proxy</literal>.
70 '';
71 apply = let
72 from = "services.rspamd-upstream.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 };
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 = if value.extraConfig == "" then "true" else "false";
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 = ''
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 = "Text of the file.";
199 };
200
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 };
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 });
221 in
222
223 {
224
225 ###### interface
226
227 options = {
228
229 services.rspamd-upstream = {
230
231 enable = mkEnableOption "rspamd, the Rapid spam filtering system";
232
233 debug = mkOption {
234 type = types.bool;
235 default = false;
236 description = "Whether to run the rspamd daemon in debug mode.";
237 };
238
239 locals = mkOption {
240 type = with types; attrsOf (submodule (configFileModule "locals"));
241 default = {};
242 description = ''
243 Local configuration files, written into <filename>/etc/rspamd/local.d/{name}</filename>.
244 '';
245 example = literalExample ''
246 { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
247 "arc.conf".text = "allow_envfrom_empty = true;";
248 }
249 '';
250 };
251
252 overrides = mkOption {
253 type = with types; attrsOf (submodule (configFileModule "overrides"));
254 default = {};
255 description = ''
256 Overridden configuration files, written into <filename>/etc/rspamd/override.d/{name}</filename>.
257 '';
258 example = literalExample ''
259 { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
260 "arc.conf".text = "allow_envfrom_empty = true;";
261 }
262 '';
263 };
264
265 localLuaRules = mkOption {
266 default = null;
267 type = types.nullOr types.path;
268 description = ''
269 Path of file to link to <filename>/etc/rspamd/rspamd.local.lua</filename> for local
270 rules written in Lua
271 '';
272 };
273
274 workers = mkOption {
275 type = with types; attrsOf (submodule workerOpts);
276 description = ''
277 Attribute set of workers to start.
278 '';
279 default = {
280 normal = {};
281 controller = {};
282 };
283 example = literalExample ''
284 {
285 normal = {
286 includes = [ "$CONFDIR/worker-normal.inc" ];
287 bindSockets = [{
288 socket = "/run/rspamd/rspamd.sock";
289 mode = "0660";
290 owner = "${cfg.user}";
291 group = "${cfg.group}";
292 }];
293 };
294 controller = {
295 includes = [ "$CONFDIR/worker-controller.inc" ];
296 bindSockets = [ "[::1]:11334" ];
297 };
298 }
299 '';
300 };
301
302 extraConfig = mkOption {
303 type = types.lines;
304 default = "";
305 description = ''
306 Extra configuration to add at the end of the rspamd configuration
307 file.
308 '';
309 };
310
311 user = mkOption {
312 type = types.string;
313 default = "rspamd";
314 description = ''
315 User to use when no root privileges are required.
316 '';
317 };
318
319 group = mkOption {
320 type = types.string;
321 default = "rspamd";
322 description = ''
323 Group to use when no root privileges are required.
324 '';
325 };
326
327 postfix = {
328 enable = mkOption {
329 type = types.bool;
330 default = false;
331 description = "Add rspamd milter to postfix main.conf";
332 };
333
334 config = mkOption {
335 type = with types; attrsOf (either bool (either str (listOf str)));
336 description = ''
337 Addon to postfix configuration
338 '';
339 default = {
340 smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
341 non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
342 };
343 example = {
344 smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
345 non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
346 };
347 };
348 };
349 };
350 };
351
352
353 ###### implementation
354
355 config = mkIf cfg.enable {
356 services.rspamd-upstream.overrides = configOverrides;
357 services.rspamd-upstream.workers = mkIf cfg.postfix.enable {
358 controller = {};
359 rspamd_proxy = {
360 bindSockets = [ {
361 mode = "0660";
362 socket = "/run/rspamd/rspamd-milter.sock";
363 owner = cfg.user;
364 group = postfixCfg.group;
365 } ];
366 extraConfig = ''
367 upstream "local" {
368 default = yes; # Self-scan upstreams are always default
369 self_scan = yes; # Enable self-scan
370 }
371 '';
372 };
373 };
374 services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
375
376 # Allow users to run 'rspamc' and 'rspamadm'.
377 environment.systemPackages = [ pkgs.rspamd ];
378
379 users.users = singleton {
380 name = cfg.user;
381 description = "rspamd daemon";
382 uid = config.ids.uids.rspamd;
383 group = cfg.group;
384 };
385
386 users.groups = singleton {
387 name = cfg.group;
388 gid = config.ids.gids.rspamd;
389 };
390
391 environment.etc."rspamd".source = rspamdDir;
392
393 systemd.services.rspamd-upstream = {
394 description = "Rspamd Service";
395
396 wantedBy = [ "multi-user.target" ];
397 after = [ "network.target" ];
398 restartTriggers = [ rspamdDir ];
399
400 serviceConfig = {
401 ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} --user=${cfg.user} --group=${cfg.group} --pid=/run/rspamd.pid -c /etc/rspamd/rspamd.conf -f";
402 Restart = "always";
403 RuntimeDirectory = "rspamd";
404 PrivateTmp = true;
405 };
406
407 preStart = ''
408 ${pkgs.coreutils}/bin/mkdir -p /var/lib/rspamd
409 ${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} /var/lib/rspamd
410 '';
411 };
412 };
413 imports = [
414 (mkRemovedOptionModule [ "services" "rspamd-upstream" "socketActivation" ]
415 "Socket activation never worked correctly and could at this time not be fixed and so was removed")
416 (mkRenamedOptionModule [ "services" "rspamd-upstream" "bindSocket" ]
417 [ "services" "rspamd-upstream" "workers" "normal" "bindSockets" ])
418 (mkRenamedOptionModule [ "services" "rspamd-upstream" "bindUISocket" ]
419 [ "services" "rspamd-upstream" "workers" "controller" "bindSockets" ])
420 ];
421 }