aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-03-04 20:20:43 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-03-09 13:44:30 +0100
commit8545bb05ca55d479a6f58fdc48890678fe14ed4c (patch)
treefa4322ca30982436e800fca84314c712050a0f30 /modules
parentmodules/stalwart-mail: Avoid hardcoding `vhack.eu` email address (diff)
downloadnixos-server-8545bb05ca55d479a6f58fdc48890678fe14ed4c.zip
modules/stalwart-mail: Select DKIM keys per-domain
Diffstat (limited to 'modules')
-rw-r--r--modules/by-name/st/stalwart-mail/module.nix64
-rw-r--r--modules/by-name/st/stalwart-mail/settings.nix92
2 files changed, 118 insertions, 38 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;
- };
+ # 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;
- auth = {
+ # 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";
};
};
};