Merge branch 'rspamd-dkim-signing' into 'master'

Use rspamd for DKIM signing, drop OpennDKIM

Closes #210

See merge request simple-nixos-mailserver/nixos-mailserver!374
This commit is contained in:
Martin Weinelt 2025-05-03 01:59:03 +00:00
commit 2c69257bdc
10 changed files with 74 additions and 126 deletions

View File

@ -49,7 +49,7 @@ can stay up to date with bug fixes and updates.
* Virus Scanning * Virus Scanning
- [x] via clamav - [x] via clamav
* DKIM Signing * DKIM Signing
- [x] via opendkim - [x] via rspamd
* User Management * User Management
- [x] declarative user management - [x] declarative user management
- [x] declarative password management - [x] declarative password management

View File

@ -802,6 +802,19 @@ in
''; '';
}; };
dkimKeyType = mkOption {
type = types.enum [ "rsa" "ed25519" ];
default = "rsa";
description = ''
The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018).
If you have already deployed a key with a different type than specified
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
this package to generate a key with the new type, you will either have to
change the selector or delete the old key file.
'';
};
dkimKeyBits = mkOption { dkimKeyBits = mkOption {
type = types.int; type = types.int;
default = 1024; default = 1024;
@ -815,26 +828,6 @@ in
''; '';
}; };
dkimHeaderCanonicalization = mkOption {
type = types.enum ["relaxed" "simple"];
default = "relaxed";
description = ''
DKIM canonicalization algorithm for message headers.
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
'';
};
dkimBodyCanonicalization = mkOption {
type = types.enum ["relaxed" "simple"];
default = "relaxed";
description = ''
DKIM canonicalization algorithm for message bodies.
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
'';
};
dmarcReporting = { dmarcReporting = {
enable = mkOption { enable = mkOption {
type = types.bool; type = types.bool;
@ -1299,7 +1292,6 @@ in
./mail-server/networking.nix ./mail-server/networking.nix
./mail-server/systemd.nix ./mail-server/systemd.nix
./mail-server/dovecot.nix ./mail-server/dovecot.nix
./mail-server/opendkim.nix
./mail-server/postfix.nix ./mail-server/postfix.nix
./mail-server/rspamd.nix ./mail-server/rspamd.nix
./mail-server/nginx.nix ./mail-server/nginx.nix
@ -1308,5 +1300,11 @@ in
SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings. SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings.
It may be that they are redundant and are already configured in rspamd like for skip_addresses. It may be that they are redundant and are already configured in rspamd like for skip_addresses.
'') '')
(lib.mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] ''
DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization.
'')
(lib.mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] ''
DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization.
'')
]; ];
} }

View File

@ -4,6 +4,8 @@ Release Notes
NixOS 25.05 NixOS 25.05
----------- -----------
- OpenDKIM has been removed and DKIM signing is now handled by Rspamd, which only supports ``relaxed``` canoncalizaliaton.
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/374` __)
- Rspamd now connects to Redis over its Unix Domain Socket by default - Rspamd now connects to Redis over its Unix Domain Socket by default
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/375>`__) (`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/375>`__)

View File

@ -173,7 +173,7 @@ Note that it can take a while until a DNS entry is propagated.
Set ``DKIM`` signature Set ``DKIM`` signature
^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
On your server, the ``opendkim`` systemd service generated a file On your server, the ``rspamd`` systemd service generated a file
containing your DKIM public key in the file containing your DKIM public key in the file
``/var/dkim/example.com.mail.txt``. The content of this file looks ``/var/dkim/example.com.mail.txt``. The content of this file looks
like like

View File

@ -22,7 +22,7 @@ in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
dovecot opendkim openssh postfix rspamd dovecot openssh postfix rspamd
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []); ] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
}; };
} }

View File

@ -1,89 +0,0 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2017 Brian Olsen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.mailserver;
dkimUser = config.services.opendkim.user;
dkimGroup = config.services.opendkim.group;
createDomainDkimCert = dom:
let
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
in
''
if [ ! -f "${dkim_key}" ]
then
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-d "${dom}" \
--bits="${toString cfg.dkimKeyBits}" \
--directory="${cfg.dkimKeyDirectory}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
chmod 644 "${dkim_txt}"
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
fi
'';
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
keyTable = pkgs.writeText "opendkim-KeyTable"
(lib.concatStringsSep "\n" (lib.flip map cfg.domains
(dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key")));
signingTable = pkgs.writeText "opendkim-SigningTable"
(lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}")));
dkim = config.services.opendkim;
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
in
{
config = mkIf (cfg.dkimSigning && cfg.enable) {
services.opendkim = {
enable = true;
selector = cfg.dkimSelector;
keyPath = cfg.dkimKeyDirectory;
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
configFile = pkgs.writeText "opendkim.conf" (''
Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization}
UMask 0002
Socket ${dkim.socket}
KeyTable file:${keyTable}
SigningTable file:${signingTable}
'' + (lib.optionalString cfg.debug ''
Syslog yes
SyslogSuccess yes
LogWhy yes
''));
};
users.users = optionalAttrs (config.services.postfix.user == "postfix") {
postfix.extraGroups = [ "${dkimGroup}" ];
};
systemd.services.opendkim = {
preStart = lib.mkForce createAllCerts;
serviceConfig = {
ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
PermissionsStartOnly = lib.mkForce false;
};
};
systemd.tmpfiles.rules = [
"d '${cfg.dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -"
];
};
}

View File

@ -126,9 +126,7 @@ let
inetSocket = addr: port: "inet:[${toString port}@${addr}]"; inetSocket = addr: port: "inet:[${toString port}@${addr}]";
unixSocket = sock: "unix:${sock}"; unixSocket = sock: "unix:${sock}";
smtpdMilters = smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig; policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
@ -300,9 +298,9 @@ in
tls_random_source = "dev:/dev/urandom"; tls_random_source = "dev:/dev/urandom";
smtpd_milters = smtpdMilters; smtpd_milters = smtpdMilters;
non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"]; non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
milter_protocol = "6"; milter_protocol = "6";
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}";
# Fix for https://www.postfix.org/smtp-smuggling.html # Fix for https://www.postfix.org/smtp-smuggling.html
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline; smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;

View File

@ -22,6 +22,26 @@ let
postfixCfg = config.services.postfix; postfixCfg = config.services.postfix;
rspamdCfg = config.services.rspamd; rspamdCfg = config.services.rspamd;
rspamdSocket = "rspamd.service"; rspamdSocket = "rspamd.service";
rspamdUser = config.services.rspamd.user;
rspamdGroup = config.services.rspamd.group;
createDkimKeypair = domain: let
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
in pkgs.writeShellScript "dkim-keygen-${domain}" ''
if [ ! -f "${privateKey}" ]
then
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
--domain "${domain}" \
--selector "${cfg.dkimSelector}" \
--type "${cfg.dkimKeyType}" \
--bits ${toString cfg.dkimKeyBits} \
--privkey "${privateKey}" > "${publicKey}"
chmod 0644 "${publicKey}"
echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
fi
'';
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
@ -66,8 +86,11 @@ in
} }
''; }; ''; };
"dkim_signing.conf" = { text = '' "dkim_signing.conf" = { text = ''
# Disable outbound email signing, we use opendkim for this enabled = ${lib.boolToString cfg.dkimSigning};
enabled = false; path = "${cfg.dkimKeyDirectory}/$domain.$selector.key";
selector = "${cfg.dkimSelector}";
# Allow for usernames w/o domain part
allow_username_mismatch = true
''; }; ''; };
"dmarc.conf" = { text = '' "dmarc.conf" = { text = ''
${lib.optionalString cfg.dmarcReporting.enable '' ${lib.optionalString cfg.dmarcReporting.enable ''
@ -119,10 +142,29 @@ in
services.redis.servers.rspamd.enable = lib.mkDefault true; services.redis.servers.rspamd.enable = lib.mkDefault true;
systemd.tmpfiles.settings."10-rspamd.conf" = {
"${cfg.dkimKeyDirectory}" = {
d = {
# Create /var/dkim owned by rspamd user/group
user = rspamdUser;
group = rspamdGroup;
};
Z = {
# Recursively adjust permissions in /var/dkim
user = rspamdUser;
group = rspamdGroup;
};
};
};
systemd.services.rspamd = { systemd.services.rspamd = {
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
serviceConfig.SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; serviceConfig.SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
serviceConfig = lib.optionalAttrs cfg.dkimSigning {
ExecStartPre = map createDkimKeypair cfg.domains;
ReadWritePaths = [ cfg.dkimKeyDirectory ];
};
}; };
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) { systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {

View File

@ -75,11 +75,8 @@ in
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
systemd.services.postfix = { systemd.services.postfix = {
wants = certificatesDeps; wants = certificatesDeps;
after = [ "dovecot2.service" ] after = [ "dovecot2.service" ] ++ certificatesDeps;
++ lib.optional cfg.dkimSigning "opendkim.service" requires = [ "dovecot2.service" ];
++ certificatesDeps;
requires = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "opendkim.service";
}; };
}; };
} }

View File

@ -402,7 +402,7 @@ pkgs.nixosTest {
client.succeed("fetchmail --nosslcertck -v") client.succeed("fetchmail --nosslcertck -v")
client.succeed("cat ~/mail/* >&2") client.succeed("cat ~/mail/* >&2")
# make sure it is dkim signed # make sure it is dkim signed
client.succeed("grep DKIM ~/mail/*") client.succeed("grep DKIM-Signature: ~/mail/*")
with subtest("aliases"): with subtest("aliases"):
client.execute("rm ~/mail/*") client.execute("rm ~/mail/*")