about summary refs log tree commit diff stats
path: root/modules/by-name/st
diff options
context:
space:
mode:
Diffstat (limited to 'modules/by-name/st')
-rw-r--r--modules/by-name/st/stalwart-mail/module.nix64
-rw-r--r--modules/by-name/st/stalwart-mail/settings.nix94
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";
         };
       };
     };