From cd4c06686e341174e82b9617834a0fddb03b6ca2 Mon Sep 17 00:00:00 2001 From: Kaare Hoff Skovgaard Date: Thu, 31 Jul 2025 10:34:23 +0200 Subject: [PATCH 1/2] Non working attempt at getting correct login information working --- nix/modules/nixos/os/auto-update/default.nix | 1 + .../mailserver/accounts.nix | 70 ++++++--- .../mailserver/accounts/mailbox_map.nix | 140 ++++++++++++++++++ .../mx.kaareskovgaard.net/mailserver/ldap.nix | 2 +- 4 files changed, 189 insertions(+), 24 deletions(-) create mode 100644 nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts/mailbox_map.nix diff --git a/nix/modules/nixos/os/auto-update/default.nix b/nix/modules/nixos/os/auto-update/default.nix index 721b890..765e015 100644 --- a/nix/modules/nixos/os/auto-update/default.nix +++ b/nix/modules/nixos/os/auto-update/default.nix @@ -43,6 +43,7 @@ in system.autoUpgrade = { enable = true; flake = upgradePath; + allowReboot = true; }; systemd.services.nixos-upgrade-prepare-flake = { wantedBy = [ "nixos-upgrade.service" ]; 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 cb4c0ad..d573f7d 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix @@ -7,6 +7,17 @@ let cfg = config.khscodes."mx.kaareskovgaard.net"; passDbFile = "/run/secret/dovecot/passwd"; + # This just replicates what simple-nixos-mailserver does, but using the proper usernames + userDbFile = pkgs.writeTextFile { + name = "userdb"; + text = lib.concatStringsSep "\n" ( + lib.mapAttrsToList ( + name: value: + "${name}:::::::" + + lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}" + ) cfg.accounts + ); + }; bogusPasswdFile = pkgs.writeTextFile { name = "bogus-passwd"; text = "$6$1234"; @@ -128,7 +139,7 @@ let systemAccounts = lib.attrsets.foldlAttrs ( acc: name: value: - if value.isLdapAccount then acc else acc ++ [ name ] + if value.isLdapAccount then acc else acc ++ [ (accountPrimaryEmail name value) ] ) [ ] cfg.accounts; systemAccountsPassDbTemplateContents = @@ -142,16 +153,14 @@ let # Just make sure the file is not empty + "\n"; - # 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). + data = import ./accounts/mailbox_map.nix { + inherit lib accountPrimaryEmail accountAlternativeEmails; + accounts = cfg.accounts; + extraVirtualAliases = { }; + }; - # Create extra virtual aliases for all accounts (as they are not emails, that maps them to their primary email). - # Then we map those emails back into the account name in the mailbox maps. - extraVirtualAliases = lib.mapAttrs (name: account: accountPrimaryEmail name account) cfg.accounts; + mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; + mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}"; in { options.khscodes."mx.kaareskovgaard.net".accounts = lib.mkOption { @@ -160,20 +169,34 @@ in }; config = { mailserver = { - inherit extraVirtualAliases; - loginAccounts = lib.attrsets.mapAttrs (name: value: { - inherit (value) - name - aliasesRegexp - catchAll - quota - sieveScript - sendOnly - sendOnlyRejectMessage - ; - aliases = accountAlternativeEmails name value; - hashedPasswordFile = bogusPasswdFile; + loginAccounts = lib.attrsets.mapAttrs' (name: value: { + name = accountPrimaryEmail name value; + value = { + inherit (value) + name + aliasesRegexp + catchAll + quota + sieveScript + sendOnly + sendOnlyRejectMessage + ; + aliases = accountAlternativeEmails name value; + hashedPasswordFile = bogusPasswdFile; + }; }) cfg.accounts; + extraVirtualAliases = data.mailserverExtraVirtualAliases; + }; + services.postfix = { + mapFiles."valias_maps" = data.valiases_file; + mapFiles."regex_valias_maps" = data.regex_valiases_file; + mapFiles."vaccounts" = lib.mkForce data.vaccounts_file; + mapFiles."regex_vaccounts" = lib.mkForce data.regex_vaccounts_file; + config.virtual_mailbox_maps = lib.mkForce [ + (mappedFile "valias_maps") + (mappedRegexFile "regex_valias_maps") + ]; + }; khscodes.infrastructure.vault-server-approle.policy = { "mx.kaareskovgaard.net/data/users/*" = { @@ -235,6 +258,7 @@ in # with our own. preStart = lib.mkAfter '' cp ${passDbFile} /run/dovecot2/passwd + cp ${userDbFile} /run/dovecot2/userdb ''; }; # This prevents local usernames without domain names to get rewritten. 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 new file mode 100644 index 0000000..3e3712b --- /dev/null +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts/mailbox_map.nix @@ -0,0 +1,140 @@ +{ + 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/ldap.nix b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/ldap.nix index 200476b..c8bc2f6 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/ldap.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/ldap.nix @@ -41,7 +41,7 @@ in base = dc=login,dc=kaareskovgaard,dc=net scope = subtree pass_attrs = uid=user - pass_filter = (&(class=account)(memberOf=mail_user)(uid=%u) + pass_filter = (&(class=account)(memberOf=mail_user)(uid=%u)) {{- end -}} ''; destination = secretFile; From 9c4a751fe0773c5a5afe9cb598b0f745d6103b4c Mon Sep 17 00:00:00 2001 From: Kaare Hoff Skovgaard Date: Thu, 31 Jul 2025 22:38:20 +0200 Subject: [PATCH 2/2] Some more accounts stuff --- .../mailserver/accounts.nix | 68 ++++++------------- 1 file changed, 19 insertions(+), 49 deletions(-) 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 d573f7d..0478f30 100644 --- a/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix +++ b/nix/systems/aarch64-linux/mx.kaareskovgaard.net/mailserver/accounts.nix @@ -7,21 +7,14 @@ let cfg = config.khscodes."mx.kaareskovgaard.net"; passDbFile = "/run/secret/dovecot/passwd"; - # This just replicates what simple-nixos-mailserver does, but using the proper usernames - userDbFile = pkgs.writeTextFile { - name = "userdb"; - text = lib.concatStringsSep "\n" ( - lib.mapAttrsToList ( - name: value: - "${name}:::::::" - + lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}" - ) cfg.accounts - ); - }; bogusPasswdFile = pkgs.writeTextFile { name = "bogus-passwd"; text = "$6$1234"; }; + userDbFile = pkgs.writeTextFile { + name = "userdb"; + text = ''''; + }; accountPrimaryEmail = name: account: if account.isLdapAccount then lib.lists.head account.aliases else name; accountAlternativeEmails = @@ -139,7 +132,7 @@ let systemAccounts = lib.attrsets.foldlAttrs ( acc: name: value: - if value.isLdapAccount then acc else acc ++ [ (accountPrimaryEmail name value) ] + if value.isLdapAccount then acc else acc ++ [ name ] ) [ ] cfg.accounts; systemAccountsPassDbTemplateContents = @@ -152,15 +145,6 @@ let ) # Just make sure the file is not empty + "\n"; - - data = import ./accounts/mailbox_map.nix { - inherit lib accountPrimaryEmail accountAlternativeEmails; - accounts = cfg.accounts; - extraVirtualAliases = { }; - }; - - mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; - mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}"; in { options.khscodes."mx.kaareskovgaard.net".accounts = lib.mkOption { @@ -169,34 +153,20 @@ in }; config = { mailserver = { - loginAccounts = lib.attrsets.mapAttrs' (name: value: { - name = accountPrimaryEmail name value; - value = { - inherit (value) - name - aliasesRegexp - catchAll - quota - sieveScript - sendOnly - sendOnlyRejectMessage - ; - aliases = accountAlternativeEmails name value; - hashedPasswordFile = bogusPasswdFile; - }; + loginAccounts = lib.attrsets.mapAttrs (name: value: { + inherit (value) + name + aliasesRegexp + catchAll + quota + sieveScript + sendOnly + sendOnlyRejectMessage + aliases + ; + hashedPasswordFile = bogusPasswdFile; }) cfg.accounts; - extraVirtualAliases = data.mailserverExtraVirtualAliases; - }; - services.postfix = { - mapFiles."valias_maps" = data.valiases_file; - mapFiles."regex_valias_maps" = data.regex_valiases_file; - mapFiles."vaccounts" = lib.mkForce data.vaccounts_file; - mapFiles."regex_vaccounts" = lib.mkForce data.regex_vaccounts_file; - config.virtual_mailbox_maps = lib.mkForce [ - (mappedFile "valias_maps") - (mappedRegexFile "regex_valias_maps") - ]; - + extraVirtualAliases = { }; }; khscodes.infrastructure.vault-server-approle.policy = { "mx.kaareskovgaard.net/data/users/*" = { @@ -258,7 +228,7 @@ in # with our own. preStart = lib.mkAfter '' cp ${passDbFile} /run/dovecot2/passwd - cp ${userDbFile} /run/dovecot2/userdb + # cp ${userDbFile} /run/dovecot2/userdb ''; }; # This prevents local usernames without domain names to get rewritten.