From 6ac55b7e44b15c9df7f67b626eecf6d93b9863f6 Mon Sep 17 00:00:00 2001 From: Kaare Hoff Skovgaard Date: Fri, 1 Aug 2025 00:53:09 +0200 Subject: [PATCH] I think I finally understand how the postfix and dovecot integration works now. Now the regular accounts should work again, and with proper handling of catch all aliases for domains, as well as handling postmaster and abuse emails being forwarded to khs with proper auto tagging of the mails. --- .../nixos/services/vault-agent/default.nix | 2 +- .../mx.kaareskovgaard.net/default.nix | 4 + .../mailserver/accounts.nix | 105 ++++++++----- .../mailserver/accounts/mailbox_map.nix | 140 ------------------ .../mailserver/default.nix | 6 + .../mx.kaareskovgaard.net/mailserver/dkim.nix | 20 ++- .../mailserver/openid-connect.nix | 1 - .../mx.kaareskovgaard.net/users.nix | 4 +- rust/program/ed25519-helper/src/main.rs | 2 +- 9 files changed, 95 insertions(+), 189 deletions(-) delete mode 100644 nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts/mailbox_map.nix diff --git a/nix/modules/nixos/services/vault-agent/default.nix b/nix/modules/nixos/services/vault-agent/default.nix index 4dfdc40..9f87199 100644 --- a/nix/modules/nixos/services/vault-agent/default.nix +++ b/nix/modules/nixos/services/vault-agent/default.nix @@ -35,9 +35,9 @@ let runtimeInputs = [ pkgs.systemd ]; text = '' chown ${lib.escapeShellArg template.owner}:${lib.escapeShellArg template.group} ${lib.escapeShellArg template.destination} + ${template.exec} ${restartUnits template.restartUnits} ${reloadOrRestartUnits template.reloadOrRestartUnits} - ${template.exec} ''; meta = { mainProgram = "restart-command"; diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix index e0255ad..7f21f0f 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/default.nix @@ -34,9 +34,13 @@ ]; }; "mx.kaareskovgaard.net" = { + postmaster = "kaare+postmaster@agerlin-skovgaard.dk"; + abuse = "kaare+abuse@agerlin-skovgaard.dk"; domains = [ "agerlin-skovgaard.dk" "agerlinskovgaard.dk" + "k.agerlin-skovgaard.dk" + "k.agerlinskovgaard.dk" ]; accounts = import ./users.nix; }; diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix index 0478f30..eb7c1a2 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix @@ -7,22 +7,39 @@ let cfg = config.khscodes."mx.kaareskovgaard.net"; passDbFile = "/run/secret/dovecot/passwd"; + isCatchAllAddress = addr: lib.strings.hasPrefix "@" addr; bogusPasswdFile = pkgs.writeTextFile { name = "bogus-passwd"; text = "$6$1234"; }; + userDbAccountOpts = + name: account: + lib.strings.concatStringsSep " " ( + (lib.lists.optional (account.quota != null) "userdb_quota_rule=*:storage=${account.quota}") + ++ [ "userdb_user=${name}" ] + ); + userDbLine = + addr: name: account: + "${addr}:::::::${userDbAccountOpts name account}"; userDbFile = pkgs.writeTextFile { name = "userdb"; - text = ''''; + text = lib.strings.concatStringsSep "\n" ( + lib.lists.flatten ( + lib.mapAttrsToList ( + name: account: + [ + (userDbLine name name account) + ] + ++ (lib.lists.map (addr: userDbLine addr name account) ( + # Don't include catch all addresses, they become aliases in postfix + lib.lists.filter (addr: !(isCatchAllAddress addr)) account.addresses + )) + ) cfg.accounts + ) + ); }; - accountPrimaryEmail = - name: account: if account.isLdapAccount then lib.lists.head account.aliases else name; - accountAlternativeEmails = - name: account: - if account.isLdapAccount then - lib.lists.ifilter0 (idx: _: idx > 0) account.aliases - else - account.aliases; + firstNonCatchAllAddress = + account: lib.lists.head (lib.lists.filter (addr: !(isCatchAllAddress addr)) account.addresses); accountOption = lib.khscodes.mkSubmodule { description = "mail account"; options = { @@ -32,7 +49,7 @@ let description = "Username"; }; - aliases = lib.mkOption { + addresses = lib.mkOption { type = lib.types.listOf lib.types.str; example = [ "abuse@example.com" @@ -40,31 +57,9 @@ let ]; default = [ ]; description = '' - A list of aliases of this login account. - Note: Use list entries like "@example.com" to create a catchAll - that allows sending from all email addresses in these domain. - ''; - }; - - aliasesRegexp = lib.mkOption { - type = lib.types.listOf lib.types.str; - example = [ ''/^tom\..*@domain\.com$/'' ]; - default = [ ]; - description = '' - Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). - ''; - }; - - catchAll = lib.mkOption { - type = lib.types.listOf (lib.types.enum config.mailserver.domains); - example = [ - "example.com" - "example2.com" - ]; - default = [ ]; - description = '' - For which domains should this account act as a catch all? - Note: Does not allow sending from all addresses of these domains. + A list of addresses for the account. + Note: Use list entires like "@example.com" to create a catchAll + that allows sending from an email address in these domains. ''; }; @@ -145,6 +140,31 @@ let ) # Just make sure the file is not empty + "\n"; + + catchAllAliases = lib.strings.concatStringsSep "\n" ( + lib.lists.flatten ( + lib.attrsets.mapAttrsToList ( + name: account: + let + regularEmailAddr = firstNonCatchAllAddress account; + catchAlls = lib.lists.filter isCatchAllAddress account.addresses; + in + lib.lists.map (addr: "${addr} ${regularEmailAddr}") catchAlls + ) cfg.accounts + ) + ); + postmasterAliases = lib.strings.concatStringsSep "\n" ( + lib.lists.map (domain: "postmaster@${domain} ${cfg.postmaster}") cfg.domains + ); + + abuseAliases = lib.strings.concatStringsSep "\n" ( + lib.lists.map (domain: "abuse@${domain} ${cfg.abuse}") cfg.domains + ); + extraVirtualAliases = lib.strings.concatStringsSep "\n" [ + catchAllAliases + postmasterAliases + abuseAliases + ]; in { options.khscodes."mx.kaareskovgaard.net".accounts = lib.mkOption { @@ -156,14 +176,12 @@ in loginAccounts = lib.attrsets.mapAttrs (name: value: { inherit (value) name - aliasesRegexp - catchAll quota sieveScript sendOnly sendOnlyRejectMessage - aliases ; + aliases = value.addresses; hashedPasswordFile = bogusPasswdFile; }) cfg.accounts; extraVirtualAliases = { }; @@ -228,9 +246,18 @@ in # with our own. preStart = lib.mkAfter '' cp ${passDbFile} /run/dovecot2/passwd - # cp ${userDbFile} /run/dovecot2/userdb + cp ${userDbFile} /run/dovecot2/userdb ''; }; + # The stuff simple nixos-mailserver sets up here creates aliases to the accounts listed, + # but I use usernames for the accounts, which ends up creating forwards to stuff like khs@mx.kaareskovgaard.net, which causes issues. + # Instead I just want the entries in virtual_mailbox_maps for all regular addresses, which will leave the addresses alone (ie. not rewrite them) + # and send them off via lmtp to dovecot2. Dovecot2 has a fixed userdb which accepts all regular email addresses for every login and maps it to that + # users mailbox. The only thing not handled this way is catchAll addresses (and regexes, which I do not support at the moment, and hopefully won't need). + services.postfix = { + virtual = lib.mkForce extraVirtualAliases; + config.virtual_alias_maps = lib.mkForce [ "hash:/etc/postfix/virtual" ]; + }; # This prevents local usernames without domain names to get rewritten. # services.postfix.submissionOptions = submissionOptions; # services.postfix.submissionsOptions = submissionOptions; diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts/mailbox_map.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts/mailbox_map.nix deleted file mode 100644 index 3e3712b..0000000 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts/mailbox_map.nix +++ /dev/null @@ -1,140 +0,0 @@ -{ - lib, - accounts, - accountPrimaryEmail, - accountAlternativeEmails, - extraVirtualAliases ? { }, -}: -let - # I would like to not use emails as usernames, but this is not something postfix is particularly happy with, - # as far as I can see. I *think* I can do this by randomly selecting an address, ie. the first, as the "primary" - # address, and then map all other addresses to that one. Then create a virtual mailbox map that maps that back - # to the username before delivering it to lmtp. All of this just worked when using ldap, but that meant - # not using code configured accounts set up with opentofu as well, and would also make the email delivery - # take a hard dependency on ldap (and not just the login process). - - # Most of these functions are almost the same as what is found inside nixos-simple-mailserver, - # just modified with this in mind. - - # Merge several lookup tables. A lookup table is a attribute set where - # - the key is an address (user@example.com) or a domain (@example.com) - # - the value is a list of addresses - mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables; - - # lookupTableToString :: Map String [String] -> String - lookupTableToString = - attrs: - let - valueToString = value: lib.concatStringsSep ", " value; - in - lib.concatStringsSep "\n" ( - lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs - ); - # attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String] - attrsToLookupTable = - aliases: - let - lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases; - in - mergeLookupTables lookupTables; - - # valiases_postfix :: Map String [String] - valiases_postfix = mergeLookupTables ( - lib.flatten ( - lib.mapAttrsToList ( - name: value: - let - to = name; - in - map (from: { "${from}" = to; }) ( - (accountAlternativeEmails name value) ++ lib.singleton (accountPrimaryEmail name value) - ) - ) accounts - ) - ); - # catchAllPostfix :: Map String [String] - catchAllPostfix = mergeLookupTables ( - lib.flatten ( - lib.mapAttrsToList ( - name: value: - let - to = name; - in - map (from: { "@${from}" = to; }) value.catchAll - ) accounts - ) - ); - - # mailserverExtraVirtualAliases :: Map String (Map String) - mailserverExtraVirtualAliases = lib.attrsets.mapAttrs ( - name: value: - let - value = if lib.lists.isList value then value else [ value ]; - to = name; - in - { - name = to; - value = lib.lists.map ( - addr: - assert (lib.attrsets.hasAttr addr accounts); - accountPrimaryEmail name accounts."${addr}" - ) value; - } - ) extraVirtualAliases; - - # extra_valiases_postfix :: Map String [String] - extra_valiases_postfix = attrsToLookupTable extraVirtualAliases; - - # all_valiases_postfix :: Map String [String] - all_valiases_postfix = mergeLookupTables [ - valiases_postfix - extra_valiases_postfix - ]; - - # valiases_file :: Path - valiases_file = - let - content = lookupTableToString (mergeLookupTables [ - all_valiases_postfix - catchAllPostfix - ]); - in - builtins.toFile "valias" content; - - regex_valiases_postfix = mergeLookupTables ( - lib.flatten ( - lib.mapAttrsToList ( - name: value: - let - to = name; - in - map (from: { "${from}" = to; }) value.aliasesRegexp - ) accounts - ) - ); - regex_valiases_file = - let - content = lookupTableToString regex_valiases_postfix; - in - builtins.toFile "regex_valias" content; - - # vaccounts_file :: Path - # see - # https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/ - # for details on how this file looks. By using the same file as valiases, - # every alias is owned (uniquely) by its user. - # The user's own address is already in all_valiases_postfix. - vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix); - regex_vaccounts_file = builtins.toFile "regex_vaccounts" ( - lookupTableToString regex_valiases_postfix - ); -in -{ - inherit - valiases_file - regex_valiases_file - vaccounts_file - regex_vaccounts_file - mailserverExtraVirtualAliases - ; -} diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/default.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/default.nix index 789e0a6..cbb60be 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/default.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/default.nix @@ -14,6 +14,12 @@ in type = lib.types.listOf lib.types.str; default = [ ]; }; + postmaster = lib.mkOption { + type = lib.types.str; + }; + abuse = lib.mkOption { + type = lib.types.str; + }; }; imports = [ inputs.simple-nixos-mailserver.nixosModules.mailserver diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dkim.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dkim.nix index 81e0de5..4ed1876 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dkim.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/dkim.nix @@ -192,6 +192,8 @@ in ''; restartUnits = [ "rspamd.service" + # See note on why there's a condition set on postfix for this to make sense. + "postfix.service" ]; } ]) cfg.domains @@ -222,12 +224,18 @@ in # without this postfix won't forward the mails to rspamd to be signed. non_smtpd_milters = [ "unix:/run/rspamd/rspamd-milter.sock" ]; }; - systemd.services.rspamd = { - unitConfig = { - ConditionPathExists = domainKeyPaths; - }; - serviceConfig = { - ReadOnlyPaths = domainKeyPaths; + systemd.services = { + # Postfix technically does not need these files to exist. But without + # configuring this here, it will attempt to start (ie. getting restarted) many times during startup + # and eventually hit a limit on the number of times. So ensuring it cannot try to start, should prevent this. + postfix.unitConfig.ConditionPathExists = domainKeyPaths; + rspamd = { + unitConfig = { + ConditionPathExists = domainKeyPaths; + }; + serviceConfig = { + ReadOnlyPaths = domainKeyPaths; + }; }; }; }; diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/openid-connect.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/openid-connect.nix index 3f5014d..4142a1b 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/openid-connect.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/openid-connect.nix @@ -16,7 +16,6 @@ in {{- with secret "kanidm/data/apps/dovecot" -}} scope = email openid profile username_attribute = username - debug = yes introspection_url = https://dovecot:{{ .Data.data.basic_secret }}@login.kaareskovgaard.net/oauth2/token/introspect introspection_mode = post {{- end -}} diff --git a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix index b7920d8..44e0b70 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/users.nix @@ -2,9 +2,11 @@ "khs" = { isLdapAccount = true; name = "Kaare Agerlin Skovgaard"; - aliases = [ + addresses = [ "kaare@agerlin-skovgaard.dk" "kaare@agerlinskovgaard.dk" + "@k.agerlinskovgaard.dk" + "@k.agerlin-skovgaard.dk" ]; quota = "10G"; }; diff --git a/rust/program/ed25519-helper/src/main.rs b/rust/program/ed25519-helper/src/main.rs index c856367..a694a7a 100644 --- a/rust/program/ed25519-helper/src/main.rs +++ b/rust/program/ed25519-helper/src/main.rs @@ -42,6 +42,6 @@ fn pem_private_key_to_sodium_private_key(p: PemPrivateKeyToSodiumPrivateKey) -> let libsodium_seed = &result[16..48]; let keypair = libsodium_rs::crypto_sign::KeyPair::from_seed(libsodium_seed)?; let mut stdout = std::io::stdout(); - stdout.write(keypair.secret_key.as_bytes())?; + stdout.write_all(keypair.secret_key.as_bytes())?; Ok(()) }