Merge branch 'dkim-path' into 'master'

feat: add support for DKIM private key files

See merge request simple-nixos-mailserver/nixos-mailserver!344
This commit is contained in:
Jeremy Fleischman 2025-05-23 08:07:37 +00:00
commit 7825f53317
4 changed files with 77 additions and 24 deletions

View File

@ -22,7 +22,7 @@ SNM branch corresponding to your NixOS version.
## Features ## Features
* [x] Continous Integration Testing * [x] Continuous Integration Testing
* [x] Multiple Domains * [x] Multiple Domains
* Postfix * Postfix
* [x] SMTP on port 25 * [x] SMTP on port 25
@ -44,6 +44,7 @@ SNM branch corresponding to your NixOS version.
* [x] Via ClamAV * [x] Via ClamAV
* DKIM Signing * DKIM Signing
* [x] Via Rspamd * [x] Via Rspamd
* [x] Allow passing DKIM signing keys
* User Management * User Management
* [x] Declarative user management * [x] Declarative user management
* [x] Declarative password management * [x] Declarative password management
@ -64,7 +65,6 @@ SNM branch corresponding to your NixOS version.
* [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac) * [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac)
* DKIM Signing * DKIM Signing
* [ ] Allow per domain selectors * [ ] Allow per domain selectors
* [ ] Allow passing DKIM signing keys
* Improve the Forwarding Experience * Improve the Forwarding Experience
* [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html) * [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html)
* [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd) * [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd)

View File

@ -826,6 +826,21 @@ in
''; '';
}; };
dkimDomainPrivateKeyFiles = mkOption {
type = types.nullOr (types.attrsOf types.path);
default = null;
example = {
"mail.example.com" = "/run/secrets/dkim/mail.example.com.mail.key";
};
description = ''
Paths to opendkim private keys generated with `opendkim-genkey`,
indexed by domain name.
If `null`, then the keys are auto generated.
If set, then there must be an entry for every domain in
{option}`config.mailserver.domains`.
'';
};
dkimKeyDirectory = mkOption { dkimKeyDirectory = mkOption {
type = types.path; type = types.path;
default = "/var/dkim"; default = "/var/dkim";
@ -848,15 +863,15 @@ in
}; };
dkimKeyBits = mkOption { dkimKeyBits = mkOption {
type = types.int; type = types.nullOr types.int;
default = 1024; default = if cfg.dkimDomainPrivateKeyFiles == null then 1024 else null;
description = '' description = ''
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys. How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
If you have already deployed a key with a different number of bits than specified If you have already deployed a key with a different number of bits than specified
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
this package to generate a key with the new number of bits, you will either have to this package to generate a key with the new number of bits, you will either have to
change the selector or delete the old key file. change the selector or delete the old key file.
''; '';
}; };

View File

@ -14,5 +14,28 @@
assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn;
message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName"; message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName";
} }
] ++ (
let
sortedDomains = builtins.sort (a: b: a < b) config.mailserver.domains;
sortedDkimDomains = builtins.attrNames config.mailserver.dkimDomainPrivateKeyFiles;
prettyDomains = builtins.concatStringsSep ", " sortedDomains;
prettyDkimDomains = builtins.concatStringsSep ", " sortedDkimDomains;
in
lib.optionals (config.mailserver.enable && config.mailserver.dkimDomainPrivateKeyFiles != null && sortedDomains != sortedDkimDomains) [
{
assertion = config.mailserver.dkimKeyBits != null;
message = "When you bring your own DKIM private keys (mailserver.dkimDomainPrivateKeyFiles != null), the DKIM domains (${prettyDkimDomains}) must be identical to the mailserver.domains (${prettyDomains}).";
}
]
) ++ lib.optionals (config.mailserver.enable && config.mailserver.dkimDomainPrivateKeyFiles != null) [
{
assertion = config.mailserver.dkimKeyBits == null;
message = "When you bring your own DKIM private keys (mailserver.dkimDomainPrivateKeyFiles != null), you must not specify key generation options (mailserver.dkimKeyBits)";
}
] ++ lib.optionals (config.mailserver.enable && config.mailserver.dkimDomainPrivateKeyFiles == null) [
{
assertion = config.mailserver.dkimKeyBits != null;
message = "When generating DKIM private keys (mailserver.dkimDomainPrivateKeyFiles = null), you must specify key generation options (mailserver.dkimKeyBits)";
}
]; ];
} }

View File

@ -26,22 +26,37 @@ let
rspamdUser = config.services.rspamd.user; rspamdUser = config.services.rspamd.user;
rspamdGroup = config.services.rspamd.group; rspamdGroup = config.services.rspamd.group;
createDkimKeypair = domain: let createOrLinkDkimCerts = domain: let
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key"; privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt"; publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
in pkgs.writeShellScript "dkim-keygen-${domain}" '' in pkgs.writeShellScript "dkim-keygen-${domain}" (
if [ ! -f "${privateKey}" ] if cfg.dkimDomainPrivateKeyFiles != null then
then let
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \ dkimPrivateKeyFile = cfg.dkimDomainPrivateKeyFiles.${domain};
--domain "${domain}" \ in
--selector "${cfg.dkimSelector}" \ ''
--type "${cfg.dkimKeyType}" \ if [ ! -e "${dkimPrivateKeyFile}" ]; then
--bits ${toString cfg.dkimKeyBits} \ echo "DKIM keyfile does not exist: ${dkimPrivateKeyFile}"
--privkey "${privateKey}" > "${publicKey}" exit 1
chmod 0644 "${publicKey}" fi
echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
fi ln -sf "${dkimPrivateKeyFile}" "${privateKey}"
''; ''
else
''
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 {
@ -165,7 +180,7 @@ in
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
} }
(lib.optionalAttrs cfg.dkimSigning { (lib.optionalAttrs cfg.dkimSigning {
ExecStartPre = map createDkimKeypair cfg.domains; ExecStartPre = map createOrLinkDkimCerts cfg.domains;
ReadWritePaths = [ cfg.dkimKeyDirectory ]; ReadWritePaths = [ cfg.dkimKeyDirectory ];
}) })
]; ];