at 23.11-pre 15 kB view raw
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 "warning: ${w}" 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}