Files
nixpkgs/nixos/modules/services/mail/rspamd.nix
Martin Weinelt 3758224be3 rspamd: try migrating to pcre2 again
The crashes happened in 2022 and now we are yanking pcre1, so we need to
try.
2026-07-23 21:09:36 +02:00

559 lines
16 KiB
Nix
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
config,
options,
pkgs,
lib,
...
}:
with lib;
let
cfg = config.services.rspamd;
opt = options.services.rspamd;
postfixCfg = config.services.postfix;
bindSocketOpts =
{ options, config, ... }:
{
options = {
socket = mkOption {
type = types.str;
example = "localhost:11333";
description = ''
Socket for this worker to listen on in a format acceptable by rspamd.
'';
};
mode = mkOption {
type = types.str;
default = "0644";
description = "Mode to set on unix socket";
};
owner = mkOption {
type = types.str;
default = "${cfg.user}";
description = "Owner to set on unix socket";
};
group = mkOption {
type = types.str;
default = "${cfg.group}";
description = "Group to set on unix socket";
};
rawEntry = mkOption {
type = types.str;
internal = true;
};
};
config.rawEntry =
let
maybeOption = option: optionalString options.${option}.isDefined " ${option}=${config.${option}}";
in
if (!(hasPrefix "/" config.socket)) then
"${config.socket}"
else
"${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}";
};
traceWarning = w: x: builtins.trace "warning: ${w}" x;
workerOpts =
{ name, options, ... }:
{
options = {
enable = mkOption {
type = types.nullOr types.bool;
default = null;
description = "Whether to run the rspamd worker.";
};
name = mkOption {
type = types.nullOr types.str;
default = name;
description = "Name of the worker";
};
type = mkOption {
type = types.nullOr (
types.enum [
"normal"
"controller"
"fuzzy"
"rspamd_proxy"
"lua"
"proxy"
]
);
description = ''
The type of this worker. The type `proxy` is
deprecated and only kept for backwards compatibility and should be
replaced with `rspamd_proxy`.
'';
apply =
let
from = "services.rspamd.workers.\"${name}\".type";
files = options.type.files;
warning = "The option `${from}` defined in ${showFiles files} has enum value `proxy` which has been renamed to `rspamd_proxy`";
in
x: if x == "proxy" then traceWarning warning "rspamd_proxy" else x;
};
bindSockets = mkOption {
type = types.listOf (types.either types.str (types.submodule bindSocketOpts));
default = [ ];
description = ''
List of sockets to listen, in format acceptable by rspamd
'';
example = [
{
socket = "/run/rspamd.sock";
mode = "0666";
owner = "rspamd";
}
"*:11333"
];
apply =
value:
map (
each:
if (isString each) then
if (isUnixSocket each) then
{
socket = each;
owner = cfg.user;
group = cfg.group;
mode = "0644";
rawEntry = "${each}";
}
else
{
socket = each;
rawEntry = "${each}";
}
else
each
) value;
};
count = mkOption {
type = types.nullOr types.int;
default = null;
description = ''
Number of worker instances to run
'';
};
includes = mkOption {
type = types.listOf types.str;
default = [ ];
description = ''
List of files to include in configuration
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "Additional entries to put verbatim into worker section of rspamd config file.";
};
};
config =
mkIf (name == "normal" || name == "controller" || name == "fuzzy" || name == "rspamd_proxy")
{
type = mkDefault name;
includes = mkDefault [ "$CONFDIR/worker-${if name == "rspamd_proxy" then "proxy" else name}.inc" ];
bindSockets =
let
unixSocket = name: {
mode = "0660";
socket = "/run/rspamd/${name}.sock";
owner = cfg.user;
group = cfg.group;
};
in
mkDefault (
if name == "normal" then
[ (unixSocket "rspamd") ]
else if name == "controller" then
[ "localhost:11334" ]
else if name == "rspamd_proxy" then
[ (unixSocket "proxy") ]
else
[ ]
);
};
};
isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket);
mkBindSockets =
enabled: socks:
concatStringsSep "\n " (flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks));
rspamdConfFile = pkgs.writeText "rspamd.conf" ''
.include "$CONFDIR/common.conf"
options {
pidfile = "$RUNDIR/rspamd.pid";
.include "$CONFDIR/options.inc"
.include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc"
.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc"
}
logging {
type = "syslog";
.include "$CONFDIR/logging.inc"
.include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc"
.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc"
}
${concatStringsSep "\n" (
mapAttrsToList (
name: value:
let
includeName = if name == "rspamd_proxy" then "proxy" else name;
tryOverride = boolToString (value.extraConfig == "");
in
''
worker "${value.type}" {
type = "${value.type}";
${optionalString (value.enable != null) "enabled = ${lib.boolToYesNo (value.enable != false)};"}
${mkBindSockets value.enable value.bindSockets}
${optionalString (value.count != null) "count = ${toString value.count};"}
${concatStringsSep "\n " (map (each: ".include \"${each}\"") value.includes)}
.include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-${includeName}.inc"
.include(try=${tryOverride}; priority=10) "$LOCAL_CONFDIR/override.d/worker-${includeName}.inc"
}
''
) cfg.workers
)}
${optionalString (cfg.extraConfig != "") ''
.include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc"
''}
'';
filterFiles = files: filterAttrs (n: v: v.enable) files;
rspamdDir = pkgs.linkFarm "etc-rspamd-dir" (
(mapAttrsToList (name: file: {
name = "local.d/${name}";
path = file.source;
}) (filterFiles cfg.locals))
++ (mapAttrsToList (name: file: {
name = "override.d/${name}";
path = file.source;
}) (filterFiles cfg.overrides))
++ (optional (cfg.localLuaRules != null) {
name = "rspamd.local.lua";
path = cfg.localLuaRules;
})
++ [
{
name = "rspamd.conf";
path = rspamdConfFile;
}
]
);
configFileModule =
prefix:
{ name, config, ... }:
{
options = {
enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether this file ${prefix} should be generated. This
option allows specific ${prefix} files to be disabled.
'';
};
text = mkOption {
default = null;
type = types.nullOr types.lines;
description = "Text of the file.";
};
source = mkOption {
type = types.path;
description = "Path of the source file.";
};
};
config = {
source = mkIf (config.text != null) (
let
name' = "rspamd-${prefix}-" + baseNameOf name;
in
mkDefault (pkgs.writeText name' config.text)
);
};
};
configOverrides =
(mapAttrs' (
n: v:
nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" {
text = v.extraConfig;
}
) (filterAttrs (n: v: v.extraConfig != "") cfg.workers))
// (lib.optionalAttrs (cfg.extraConfig != "") {
"extra-config.inc".text = cfg.extraConfig;
});
in
{
###### interface
options = {
services.rspamd = {
enable = mkEnableOption "rspamd, the Rapid spam filtering system";
package = lib.mkPackageOption pkgs "rspamd" { };
debug = mkOption {
type = types.bool;
default = false;
description = "Whether to run the rspamd daemon in debug mode.";
};
locals = mkOption {
type = with types; attrsOf (submodule (configFileModule "locals"));
default = { };
description = ''
Local configuration files, written into {file}`/etc/rspamd/local.d/{name}`.
'';
example = literalExpression ''
{ "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
"arc.conf".text = "allow_envfrom_empty = true;";
}
'';
};
overrides = mkOption {
type = with types; attrsOf (submodule (configFileModule "overrides"));
default = { };
description = ''
Overridden configuration files, written into {file}`/etc/rspamd/override.d/{name}`.
'';
example = literalExpression ''
{ "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
"arc.conf".text = "allow_envfrom_empty = true;";
}
'';
};
localLuaRules = mkOption {
default = null;
type = types.nullOr types.path;
description = ''
Path of file to link to {file}`/etc/rspamd/rspamd.local.lua` for local
rules written in Lua
'';
};
workers = mkOption {
type = with types; attrsOf (submodule workerOpts);
description = ''
Attribute set of workers to start.
'';
default = {
normal = { };
controller = { };
};
example = literalExpression ''
{
normal = {
includes = [ "$CONFDIR/worker-normal.inc" ];
bindSockets = [{
socket = "/run/rspamd/rspamd.sock";
mode = "0660";
owner = "''${config.${opt.user}}";
group = "''${config.${opt.group}}";
}];
};
controller = {
includes = [ "$CONFDIR/worker-controller.inc" ];
bindSockets = [ "[::1]:11334" ];
};
}
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra configuration to add at the end of the rspamd configuration
file.
'';
};
extraArgs = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"--var=RBL_API_KEY=\${RBL_API_KEY}"
];
description = ''
A list of extra command line arguments to pass to rspamd.
Check `rspamd --help` for possible arguments.
'';
};
user = mkOption {
type = types.str;
default = "rspamd";
description = ''
User to use when no root privileges are required.
'';
};
group = mkOption {
type = types.str;
default = "rspamd";
description = ''
Group to use when no root privileges are required.
'';
};
postfix = {
enable = mkOption {
type = types.bool;
default = false;
description = "Add rspamd milter to postfix main.conf";
};
config = mkOption {
type =
with types;
attrsOf (oneOf [
bool
str
(listOf str)
]);
description = ''
Addon to postfix configuration
'';
default = {
smtpd_milters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
non_smtpd_milters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
};
};
};
};
};
###### implementation
config = mkIf cfg.enable {
services.rspamd.overrides = configOverrides;
services.rspamd.workers = mkIf cfg.postfix.enable {
controller = { };
rspamd_proxy = {
bindSockets = [
{
mode = "0660";
socket = "/run/rspamd/rspamd-milter.sock";
owner = cfg.user;
group = postfixCfg.group;
}
];
extraConfig = ''
upstream "local" {
default = yes; # Self-scan upstreams are always default
self_scan = yes; # Enable self-scan
}
'';
};
};
services.postfix.settings.main = mkIf cfg.postfix.enable cfg.postfix.config;
systemd.services.postfix = mkIf cfg.postfix.enable {
serviceConfig.SupplementaryGroups = [ postfixCfg.group ];
};
# Allow users to run 'rspamc' and 'rspamadm'.
environment.systemPackages = [ cfg.package ];
users.users.${cfg.user} = {
description = "rspamd daemon";
uid = config.ids.uids.rspamd;
group = cfg.group;
};
users.groups.${cfg.group} = {
gid = config.ids.gids.rspamd;
};
environment.etc.rspamd.source = rspamdDir;
systemd.services.rspamd = {
description = "Rspamd Service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
restartTriggers = [ rspamdDir ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/rspamd ${optionalString cfg.debug "-d"} ${escapeShellArgs cfg.extraArgs} -c /etc/rspamd/rspamd.conf -f";
Restart = "always";
User = "${cfg.user}";
Group = "${cfg.group}";
SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ];
RuntimeDirectory = "rspamd";
RuntimeDirectoryMode = "0755";
StateDirectory = "rspamd";
StateDirectoryMode = "0700";
AmbientCapabilities = [ ];
CapabilityBoundingSet = "";
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = false; # pcre2 jit
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
# we need to chown socket to rspamd-milter
PrivateUsers = !cfg.postfix.enable;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = "@system-service";
UMask = "0077";
};
};
};
imports = [
(mkRemovedOptionModule [
"services"
"rspamd"
"socketActivation"
] "Socket activation never worked correctly and could at this time not be fixed and so was removed")
(mkRenamedOptionModule
[ "services" "rspamd" "bindSocket" ]
[ "services" "rspamd" "workers" "normal" "bindSockets" ]
)
(mkRenamedOptionModule
[ "services" "rspamd" "bindUISocket" ]
[ "services" "rspamd" "workers" "controller" "bindSockets" ]
)
(mkRemovedOptionModule [
"services"
"rmilter"
] "Use services.rspamd.* instead to set up milter service")
];
}