Add support for DKIM key management
After bumping the generation of new DKIM keys to RSA 2048 in NixOS 25.11 key rotation for existing users could not be done safely. To resolve this situation we now support multiple generations of selectors per domain to enable proper DKIM key transitions as described in RFC6376 3.1. The added documentation introduces and motivates DKIM and guides the user through a DKIM key rotation. Additionally, DKIM key material can now also be treated as a managed secrets when autogenerated state on the mail server host is undesirable. This change is fully backwards compatible in behavior and will continue to use the previously generated DKIM key without any additional configuration up until the point when DKIM selectors are configured explicitly.
This commit is contained in:
+93
-22
@@ -31,27 +31,80 @@ let
|
||||
rspamdGroup = config.services.rspamd.group;
|
||||
|
||||
createDkimKeypair =
|
||||
domain:
|
||||
{
|
||||
domain,
|
||||
selector,
|
||||
type,
|
||||
bits,
|
||||
...
|
||||
}:
|
||||
let
|
||||
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
|
||||
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
|
||||
privkey = "${cfg.dkim.keyDirectory}/${domain}.${selector}.key";
|
||||
pubkey = "${cfg.dkim.keyDirectory}/${domain}.${selector}.txt";
|
||||
in
|
||||
pkgs.writeShellScript "dkim-keygen-${domain}" ''
|
||||
if [ ! -f "${privateKey}" ]
|
||||
pkgs.writeShellScript "dkim-keygen-${domain}-${selector}" ''
|
||||
if [ ! -f "${privkey}" ]
|
||||
then
|
||||
export PATH=${lib.makeBinPath [ pkgs.openssl ]}
|
||||
${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}"
|
||||
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen ${
|
||||
lib.cli.toCommandLineShellGNU { } {
|
||||
inherit
|
||||
domain
|
||||
selector
|
||||
type
|
||||
bits
|
||||
privkey
|
||||
;
|
||||
}
|
||||
} > "${pubkey}"
|
||||
chmod 0644 "${pubkey}"
|
||||
echo "Generated key for domain ${domain} and selector ${selector}"
|
||||
fi
|
||||
'';
|
||||
|
||||
dkimDomains = lib.unique (cfg.domains ++ (lib.optionals cfg.srs.enable [ cfg.srs.domain ]));
|
||||
mailDomains = lib.unique (
|
||||
# primary mailserver domains
|
||||
config.mailserver.domains
|
||||
# all dkim domains, even extra domains specified
|
||||
++ lib.attrNames cfg.dkim.domains
|
||||
# and the srs domain, if one is configured
|
||||
++ lib.optionals (cfg.srs.domain != null) [ cfg.srs.domain ]
|
||||
);
|
||||
|
||||
dkimKeys = lib.concatMap (
|
||||
domain:
|
||||
let
|
||||
configuredSelectors = config.mailserver.dkim.domains.${domain}.selectors or { };
|
||||
|
||||
finalSelectors =
|
||||
if configuredSelectors == { } then
|
||||
# synthesize default dkim key, if none configured
|
||||
{
|
||||
"${config.mailserver.dkim.defaults.selector}" = {
|
||||
keyType = null;
|
||||
keyLength = null;
|
||||
keyFile = null;
|
||||
};
|
||||
}
|
||||
else
|
||||
configuredSelectors;
|
||||
in
|
||||
lib.mapAttrsToList (selector: settings: rec {
|
||||
inherit domain selector;
|
||||
keyFile = settings.keyFile;
|
||||
keyPath = if keyFile != null then keyFile else "${cfg.dkim.keyDirectory}/${domain}.${selector}.key";
|
||||
bits =
|
||||
if settings.keyLength != null then
|
||||
settings.keyLength
|
||||
else
|
||||
config.mailserver.dkim.defaults.keyLength;
|
||||
type =
|
||||
if settings.keyType != null then settings.keyType else config.mailserver.dkim.defaults.keyType;
|
||||
}) finalSelectors
|
||||
) mailDomains;
|
||||
|
||||
dkimKeysToGenerate = lib.filter (key: key.keyFile == null) dkimKeys;
|
||||
|
||||
dkimKeysByDomain = lib.groupBy (item: item.domain) dkimKeys;
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
@@ -110,13 +163,31 @@ in
|
||||
};
|
||||
"dkim_signing.conf" = {
|
||||
text = ''
|
||||
enabled = ${lib.boolToString cfg.dkimSigning};
|
||||
path = "${cfg.dkimKeyDirectory}/$domain.$selector.key";
|
||||
selector = "${cfg.dkimSelector}";
|
||||
enabled = ${lib.boolToString cfg.dkim.enable};
|
||||
# Only sign explicitly configured domains
|
||||
try_fallback = false;
|
||||
# Allow for usernames w/o domain part
|
||||
allow_username_mismatch = true;
|
||||
# Don't normalize DKIM key selection for subdomains
|
||||
use_esld = false;
|
||||
|
||||
domain {
|
||||
${lib.concatStringsSep "\n\n" (
|
||||
map (domain: ''
|
||||
${domain} {
|
||||
selectors [
|
||||
${lib.concatStringsSep ",\n" (
|
||||
map (selector: ''
|
||||
{
|
||||
path: "${selector.keyPath}";
|
||||
selector: "${selector.selector}";
|
||||
}'') dkimKeysByDomain.${domain}
|
||||
)}
|
||||
]
|
||||
}
|
||||
'') (lib.attrNames dkimKeysByDomain)
|
||||
)}
|
||||
}
|
||||
'';
|
||||
};
|
||||
"dmarc.conf" = {
|
||||
@@ -183,7 +254,7 @@ in
|
||||
services.redis.servers.rspamd.enable = lib.mkDefault cfg.redis.configureLocally;
|
||||
|
||||
systemd.tmpfiles.settings."10-rspamd.conf" = {
|
||||
"${cfg.dkimKeyDirectory}" = {
|
||||
"${cfg.dkim.keyDirectory}" = {
|
||||
d = {
|
||||
# Create /var/dkim owned by rspamd user/group
|
||||
user = rspamdUser;
|
||||
@@ -204,9 +275,9 @@ in
|
||||
{
|
||||
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
|
||||
}
|
||||
(lib.optionalAttrs cfg.dkimSigning {
|
||||
ExecStartPre = map createDkimKeypair dkimDomains;
|
||||
ReadWritePaths = [ cfg.dkimKeyDirectory ];
|
||||
(lib.optionalAttrs cfg.dkim.enable {
|
||||
ExecStartPre = map createDkimKeypair dkimKeysToGenerate;
|
||||
ReadWritePaths = [ cfg.dkim.keyDirectory ];
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user