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
* [x] Continous Integration Testing
* [x] Continuous Integration Testing
* [x] Multiple Domains
* Postfix
* [x] SMTP on port 25
@ -44,6 +44,7 @@ SNM branch corresponding to your NixOS version.
* [x] Via ClamAV
* DKIM Signing
* [x] Via Rspamd
* [x] Allow passing DKIM signing keys
* User Management
* [x] Declarative user 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)
* DKIM Signing
* [ ] Allow per domain selectors
* [ ] Allow passing DKIM signing keys
* 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 [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 {
type = types.path;
default = "/var/dkim";
@ -816,8 +831,8 @@ in
};
dkimKeyBits = mkOption {
type = types.int;
default = 1024;
type = types.nullOr types.int;
default = if cfg.dkimDomainPrivateKeyFiles == null then 1024 else null;
description = ''
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;
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;
rspamdGroup = config.services.rspamd.group;
createDkimKeypair = domain: let
createOrLinkDkimCerts = 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 pkgs.writeShellScript "dkim-keygen-${domain}" (
if cfg.dkimDomainPrivateKeyFiles != null then
let
dkimPrivateKeyFile = cfg.dkimDomainPrivateKeyFiles.${domain};
in
''
if [ ! -e "${dkimPrivateKeyFile}" ]; then
echo "DKIM keyfile does not exist: ${dkimPrivateKeyFile}"
exit 1
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
{
config = with cfg; lib.mkIf enable {
@ -165,7 +180,7 @@ in
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
}
(lib.optionalAttrs cfg.dkimSigning {
ExecStartPre = map createDkimKeypair cfg.domains;
ExecStartPre = map createOrLinkDkimCerts cfg.domains;
ReadWritePaths = [ cfg.dkimKeyDirectory ];
})
];