feat: add support for DKIM private key files

This gives people an option to declaratively manage these secrets,
rather than having them generated.
This commit is contained in:
Jeremy Fleischman 2024-10-31 15:07:47 -05:00
parent 433520257a
commit bb34b1bb24
No known key found for this signature in database
4 changed files with 72 additions and 19 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

@ -794,6 +794,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";
@ -816,8 +831,8 @@ 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.

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 ];
}) })
]; ];