From bb34b1bb244573a16733a5e4c9c3b31a282c0089 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Thu, 31 Oct 2024 15:07:47 -0500 Subject: [PATCH] feat: add support for DKIM private key files This gives people an option to declaratively manage these secrets, rather than having them generated. --- README.md | 4 ++-- default.nix | 19 ++++++++++++++-- mail-server/assertions.nix | 23 +++++++++++++++++++ mail-server/rspamd.nix | 45 +++++++++++++++++++++++++------------- 4 files changed, 72 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 11ccf3c..9d3dc58 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/default.nix b/default.nix index 3f46610..95704b8 100644 --- a/default.nix +++ b/default.nix @@ -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. diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 91921c6..10076f3 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -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)"; + } ]; } diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index 0e37c20..470217b 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -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 ]; }) ];