diff options
-rw-r--r-- | modules/by-name/st/stalwart-mail/module.nix | 64 | ||||
-rw-r--r-- | modules/by-name/st/stalwart-mail/settings.nix | 94 |
2 files changed, 119 insertions, 39 deletions
diff --git a/modules/by-name/st/stalwart-mail/module.nix b/modules/by-name/st/stalwart-mail/module.nix index 76149c3..3ef7d85 100644 --- a/modules/by-name/st/stalwart-mail/module.nix +++ b/modules/by-name/st/stalwart-mail/module.nix @@ -100,12 +100,46 @@ in { security = lib.mkOption { type = lib.types.nullOr (lib.types.submodule { options = { - dkimPrivateKeyPath = lib.mkOption { - type = lib.types.path; + verificationMode = lib.mkOption { + type = lib.types.enum ["relaxed" "strict"]; description = '' - The path to the dkim private key agenix file. + Whether to allow invalid signatures/checks or not. ''; + default = "relaxed"; }; + + dkimKeys = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + dkimPublicKey = lib.mkOption { + type = lib.types.str; + description = '' + The base 64 encoded representation of the public dkim key. + ''; + }; + dkimPrivateKeyPath = lib.mkOption { + type = lib.types.path; + description = '' + The path to the dkim private key agenix file. + Generate it via the `./gen_key` script: + ''; + }; + keyAlgorithm = lib.mkOption { + type = lib.types.enum ["ed25519-sha256" "rsa-sha-256" "rsa-sha-1"]; + description = "The algorithm of the used key"; + }; + }; + }); + description = '' + Which key to use for which domain. The attr keys are the domains + ''; + default = {}; + }; + allowInsecureSmtp = lib.mkEnableOption '' + insecure SMTP listener (on port 25). + + This is important, if an legacy mail server might want to send you mail. + ''; }; }); description = '' @@ -130,7 +164,6 @@ in { inherit (cfg) package; # dataDir = cfg.dataDirectory; }; - security.acme.certs = { "${cfg.fqdn}" = { domain = cfg.fqdn; @@ -138,14 +171,21 @@ in { }; }; - age.secrets = lib.mkIf (cfg.security != null) { - stalwartMailDkim = { - file = cfg.security.dkimPrivateKeyPath; - mode = "600"; - owner = "stalwart-mail"; - group = "stalwart-mail"; - }; - }; + age.secrets = let + keys = + lib.mapAttrs' ( + keyDomain: keyConfig: + lib.nameValuePair "stalwartMail${keyDomain}" + { + file = keyConfig.dkimPrivateKeyPath; + mode = "600"; + owner = "stalwart-mail"; + group = "stalwart-mail"; + } + ) + cfg.security.dkimKeys; + in + lib.mkIf (cfg.security != null) keys; vhack.persist.directories = [ { diff --git a/modules/by-name/st/stalwart-mail/settings.nix b/modules/by-name/st/stalwart-mail/settings.nix index 06b5062..8619c98 100644 --- a/modules/by-name/st/stalwart-mail/settings.nix +++ b/modules/by-name/st/stalwart-mail/settings.nix @@ -5,44 +5,77 @@ ... }: let cfg = config.vhack.stalwart-mail; + + signaturesByDomain = + (builtins.map ({name, ...}: { + "if" = "sender_domain = '${name}'"; + "then" = "'${name}'"; + }) + (lib.attrsToList cfg.security.dkimKeys)) + ++ [{"else" = false;}]; in { config.services.stalwart-mail.settings = lib.mkIf cfg.enable { - signature."ed25519" = lib.mkIf (cfg.security != null) { - private-key = "%{file:${config.age.secrets.stalwartMailDkim.path}}%"; - domain = "${cfg.fqdn}"; - selector = "ed-default"; - headers = ["From" "To" "Cc" "Date" "Subject" "Message-ID" "Organization" "MIME-Version" "Content-Type" "In-Reply-To" "References" "List-Id" "User-Agent" "Thread-Topic" "Thread-Index"]; - algorithm = "ed25519-sha256"; - canonicalization = "relaxed/relaxed"; # TODO: What does this do? <2025-02-07> - expire = "50d"; - report = true; - }; - - auth = { + # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.3 + signature = let + signatures = + lib.mapAttrs (keyDomain: keyConfig: { + private-key = "%{file:${config.age.secrets."stalwartMail${keyDomain}".path}}%"; + + domain = keyDomain; + + selector = "mail"; + headers = ["From" "To" "Cc" "Date" "Subject" "Message-ID" "Organization" "MIME-Version" "Content-Type" "In-Reply-To" "References" "List-Id" "User-Agent" "Thread-Topic" "Thread-Index"]; + algorithm = keyConfig.keyAlgorithm; + + # How do we canonicalize the headers/body? + # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.4 + canonicalization = "simple/simple"; + + expire = "50d"; + report = true; + }) + cfg.security.dkimKeys; + in + lib.mkIf (cfg.security != null) signatures; + + auth = let + # NOTE(@bpeetz): We disable all the checks if the `listener` is submissions, because the + # user's email client will obviously not have the right IP address to pass SPF or + # IPREV. It will also not be able to sign the message with DKIM (as we has to key). <2025-02-25> + ifNotSmpt = valueTrue: valueFalse: [ + { + "if" = "listener != 'submissions'"; + "then" = valueTrue; + } + {"else" = valueFalse;} + ]; + in { iprev = { - verify = "relaxed"; + verify = ifNotSmpt cfg.security.verificationMode "disable"; }; spf = { verify = { - ehlo = "relaxed"; - mail-from = "relaxed"; + ehlo = ifNotSmpt cfg.security.verificationMode "disable"; + + mail-from = ifNotSmpt cfg.security.verificationMode "disable"; }; }; dmarc = { - verify = "relaxed"; + verify = ifNotSmpt cfg.security.verificationMode "disable"; }; arc = { - seal = lib.mkIf (cfg.security != null) "ed25519"; - verify = "relaxed"; + seal = lib.mkIf (cfg.security != null) signaturesByDomain; + verify = ifNotSmpt cfg.security.verificationMode "disable"; }; dkim = { - verify = "relaxed"; + verify = ifNotSmpt cfg.security.verificationMode "disable"; # Ignore insecure dkim signed messages (i.e., messages containing both # signed and appended not-signed content.) strict = true; - sign = lib.mkIf (cfg.security != null) "['ed25519']"; + sign = + lib.mkIf (cfg.security != null) signaturesByDomain; }; }; report = { @@ -60,7 +93,7 @@ in { contact-info = "'${cfg.admin}'"; send = "daily"; max-size = 26214400; # 25 MiB - sign = lib.mkIf (cfg.security != null) "['ed25519']"; + sign = lib.mkIf (cfg.security != null) "'${cfg.fqdn}'"; }; dmarc = { aggregate = { @@ -70,32 +103,32 @@ in { contact-info = "'${cfg.admin}'"; send = "weekly"; max-size = 26214400; # 25MiB - sign = lib.mkIf (cfg.security != null) "['ed25519']"; + sign = lib.mkIf (cfg.security != null) "'${cfg.fqdn}'"; }; from-name = "'Report Subsystem'"; from-address = "'noreply-dmarc@${cfg.fqdn}'"; subject = "'DMARC Authentication Failure Report'"; send = "1/1d"; - sign = lib.mkIf (cfg.security != null) "['ed25519']"; + sign = lib.mkIf (cfg.security != null) signaturesByDomain; }; spf = { from-name = "'Report Subsystem'"; from-address = "'noreply-spf@${cfg.fqdn}'"; subject = "'SPF Authentication Failure Report'"; send = "1/1d"; - sign = lib.mkIf (cfg.security != null) "['ed25519']"; + sign = lib.mkIf (cfg.security != null) signaturesByDomain; }; dkim = { from-name = "'Report Subsystem'"; from-address = "'noreply-dkim@${cfg.fqdn}'"; subject = "'DKIM Authentication Failure Report'"; send = "1/1d"; - sign = lib.mkIf (cfg.security != null) "['ed25519']"; + sign = lib.mkIf (cfg.security != null) signaturesByDomain; }; dsn = { from-name = "'Mail Delivery Subsystem'"; from-address = "'MAILER-DAEMON@${cfg.fqdn}'"; - sign = lib.mkIf (cfg.security != null) "['ed25519']"; + sign = lib.mkIf (cfg.security != null) signaturesByDomain; }; }; queue = { @@ -106,9 +139,16 @@ in { }; outbound = { tls = { - starttls = "require"; + starttls = + if cfg.security.verificationMode == "strict" + then "require" + else "optional"; allow-invalid-certs = false; ip-strategy = "ipv6_then_ipv4"; + mta-sts = + if cfg.security.verificationMode == "strict" + then "require" + else "optional"; }; }; }; |