From 23364b04e86313c73aaa4224221946ded5f65ba5 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sat, 21 Mar 2026 22:36:09 +0100 Subject: [PATCH 1/3] ldap: allow local accounts and aliases with ldap enabled In conflicts between local addresses and LDAP addresses the local one will always take priority in mail routing. This is something we now document and guarantee through tests. --- README.md | 2 - docs/ldap.rst | 30 ++++++++++---- docs/release-notes.rst | 2 + mail-server/assertions.nix | 10 ----- tests/ldap.nix | 84 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e8367d4..16ad813 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,6 @@ SNM branch corresponding to your NixOS version. * [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac) * Improve the Forwarding Experience * [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html) -* User management - * [ ] Allow local and LDAP user to coexist * OpenID Connect * Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166) diff --git a/docs/ldap.rst b/docs/ldap.rst index 4750e31..d65ccfd 100644 --- a/docs/ldap.rst +++ b/docs/ldap.rst @@ -40,15 +40,25 @@ follow best practices to simplify maintenance. Limitations ~~~~~~~~~~~ -We have various assertions in place, that prevent using LDAP together with -other features. Most of them are not technical limitations per se, but instead -lack configuration or validation. +Design choices +^^^^^^^^^^^^^^ -- Local users (:option:`mailserver.loginAccounts`) and aliases - (:option:`mailserver.extraVirtualAliases`) are not currently allowed with - :option:`mailserver.ldap.enable` enabled -- Aliases based on LDAP attributes are currently not implemented -- Quotas based on LDAP attributes are currently not implemented +These are intentional choices in how the mail server operates that affect the +LDAP integration. + +- For mail address routing local accounts always take priority over LDAP accounts. + +Planned +^^^^^^^ + +These are features we are interested in but require implementation, +documentation and tests. + +- Aliases based on LDAP attributes +- Quotas based on LDAP attributes + +Avoided +^^^^^^^ The following features will likely never be implemented, since they would complicate the setup significantly. @@ -58,7 +68,9 @@ complicate the setup significantly. - Use of ``homeDirectory``, ``uid``, ``gid`` LDAP attributes (we are committed to a virtual setup with one vmail user/uid/gid and UUID based home directories) - +- Declarative aliases through :option:`mailserver.extraVirtualAliases`. These + are limited to local accounts, because Postfix enforces sender ownership based + on login identity and does not consult virtual aliases for authorization. Enabling LDAP support ~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/release-notes.rst b/docs/release-notes.rst index e3122d4..9ee6ccc 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -30,6 +30,8 @@ NixOS 26.05 rather than their email address, which is more convenient and consistent with typical LDAP practices. The exact attribute can be customized through :option:`mailserver.ldap.attributes.username`. +- Local and LDAP accounts can now co-exist. For overlapping names and addresses + the local account will always win. - The following integrations are deprecated and will be removed before the next release: diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 8a346ad..a3c2428 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -98,16 +98,6 @@ in ) config.mailserver.dkim.domains ) ) - ++ lib.optionals config.mailserver.ldap.enable [ - { - assertion = config.mailserver.loginAccounts == { }; - message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts"; - } - { - assertion = config.mailserver.extraVirtualAliases == { }; - message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; - } - ] ++ lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail") [ diff --git a/tests/ldap.nix b/tests/ldap.nix index a67362b..7cde1d0 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -1,7 +1,25 @@ +{ + pkgs, + ... +}: let + hashPassword = + password: + pkgs.runCommand "password-${password}-hashed" + { + buildInputs = [ pkgs.mkpasswd ]; + inherit password; + } + '' + mkpasswd -s <<<"$password" > $out + ''; + bindPassword = "unsafegibberish"; alicePassword = "testalice"; bobPassword = "testbob"; + carolPassword = "testcarol"; + frankPassword = "testfrank"; + malloryPassword = "testmallory"; in { name = "ldap"; @@ -83,6 +101,22 @@ in mail: bob@example.com homeDirectory: /home/bob userPassword: ${bobPassword} + + dn: cn=carol,ou=users,dc=example + entryUUID: 41240499-27e2-4fa2-be4f-4113a77661b1 + objectClass: inetOrgPerson + uid: carol + sn: Baz + mail: carol@example.com + userPassword: ${carolPassword} + + dn: cn=frank,ou=users,dc=example + entryUUID: ca16f594-f6b2-418f-87d3-0d02d746461f + objectClass: inetOrgPerson + uid: frank + sn: Moo + mail: frank@example.com + userPassword: ${frankPassword} ''; }; @@ -93,6 +127,24 @@ in localDnsResolver = false; indexDir = "/var/lib/dovecot/indices"; + extraVirtualAliases = { + # Steal frank@example.com from LDAP user frank + "frank@example.com" = "mallory@example.com"; + }; + + loginAccounts = { + # Colliding local account takes precedence over LDAP account with + # same address. + "carol@example.com" = { + hashedPasswordFile = hashPassword carolPassword; + }; + # Another account used as a virtual alias target to steal + # frank@example.com from the LDAP user frank + "mallory@example.com" = { + hashedPasswordFile = hashPassword malloryPassword; + }; + }; + ldap = { enable = true; uris = [ @@ -237,5 +289,37 @@ in ])) machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob'") + with subtest("Local addresses take priority over those learnt from LDAP"): + # carol@example.com is routed to the local user account + machine.succeed(" ".join([ + "mail-check send-and-read", + "--smtp-port 465", + "--smtp-ssl", + "--smtp-host localhost", + "--smtp-username alice", # LDAP user + "--imap-host localhost", + "--imap-username carol@example.com", # Local user + "--from-addr alice@example.com", + "--to-addr carol@example.com", + "--src-password-file <(echo '${alicePassword}')", + "--dst-password-file <(echo '${carolPassword}')", + "--ignore-dkim-spf" + ])) + + # frank@example.com gets routed to mallory@example.com due to a virtual alias + machine.succeed(" ".join([ + "mail-check send-and-read", + "--smtp-port 465", + "--smtp-ssl", + "--smtp-host localhost", + "--smtp-username alice", # LDAP user + "--imap-host localhost", + "--imap-username mallory@example.com", # Local user + "--from-addr alice@example.com", + "--to-addr frank@example.com", + "--src-password-file <(echo '${alicePassword}')", + "--dst-password-file <(echo '${malloryPassword}')", + "--ignore-dkim-spf" + ])) ''; } From 31c7607ef48a8b8901db638bafc56c67585200f2 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 23 Mar 2026 00:03:41 +0100 Subject: [PATCH 2/3] Rename extraVirtualAliases to aliases and update description The extra and virtual parts are redundant and Postfix specific and not at all required. Compare forwards for example. --- default.nix | 23 +++++++++++------------ docs/ldap.rst | 6 +++--- mail-server/postfix.nix | 2 +- tests/external.nix | 4 ++-- tests/ldap.nix | 2 +- 5 files changed, 18 insertions(+), 19 deletions(-) diff --git a/default.nix b/default.nix index 0aa69fb..2bef03a 100644 --- a/default.nix +++ b/default.nix @@ -649,7 +649,7 @@ in ''; }; - extraVirtualAliases = mkOption { + aliases = mkOption { type = let loginAccount = mkOptionType { @@ -660,7 +660,6 @@ in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount)); example = { - "info@example.com" = "user1@example.com"; "postmaster@example.com" = "user1@example.com"; "abuse@example.com" = "user1@example.com"; "multi@example.com" = [ @@ -669,15 +668,14 @@ in ]; }; description = '' - Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that - all mail to `info@example.com` is forwarded to `user1@example.com`. Note - that it is expected that `postmaster@example.com` and `abuse@example.com` is - forwarded to some valid email address. (Alternatively you can create login - accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows - the user `user1@example.com` to send emails as `info@example.com`. - It's also possible to create an alias for multiple accounts. In this - example all mails for `multi@example.com` will be forwarded to both - `user1@example.com` and `user2@example.com`. + Aliases are additional mail addresses routed to one or more existing local accounts. + + The target accounts are allowed to use the alias as the sender address. + + :::{note} + This feature is limited to local accounts and does not support LDAP or + other external accounts. + ::: ''; default = { }; }; @@ -692,7 +690,7 @@ in the value `{ "user@example.com" = "user@elsewhere.com"; }` means that mails to `user@example.com` are forwarded to `user@elsewhere.com`. The difference with the - {option}`mailserver.extraVirtualAliases` option is that `user@elsewhere.com` + {option}`mailserver.aliases` option is that `user@elsewhere.com` can't send mail as `user@example.com`. Also, this option allows to forward mails to external addresses. ''; @@ -1681,5 +1679,6 @@ in [ "mailserver" "ldap" "postfix" "mailAttribute" ] [ "mailserver" "ldap" "attributes" "mail" ] ) + (mkRenamedOptionModule [ "mailserver" "extraVirtualAliases" ] [ "mailserver" "aliases" ]) ]; } diff --git a/docs/ldap.rst b/docs/ldap.rst index d65ccfd..41b61ba 100644 --- a/docs/ldap.rst +++ b/docs/ldap.rst @@ -68,9 +68,9 @@ complicate the setup significantly. - Use of ``homeDirectory``, ``uid``, ``gid`` LDAP attributes (we are committed to a virtual setup with one vmail user/uid/gid and UUID based home directories) -- Declarative aliases through :option:`mailserver.extraVirtualAliases`. These - are limited to local accounts, because Postfix enforces sender ownership based - on login identity and does not consult virtual aliases for authorization. +- Declarative aliases through :option:`mailserver.aliases`. These are limited + to local accounts, because Postfix enforces sender ownership based on login + identity and does not consult virtual aliases for authorization. Enabling LDAP support ~~~~~~~~~~~~~~~~~~~~~ diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 05794ac..70ae68e 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -94,7 +94,7 @@ let mergeLookupTables lookupTables; # extra_valiases_postfix :: Map String [String] - extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases; + extra_valiases_postfix = attrsToLookupTable cfg.aliases; # forwards :: Map String [String] forwards = attrsToLookupTable cfg.forwards; diff --git a/tests/external.nix b/tests/external.nix index fd2c267..a149144 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -85,7 +85,7 @@ }; }; - extraVirtualAliases = { + aliases = { "single-alias@example.com" = "user1@example.com"; "multi-alias@example.com" = [ "user1@example.com" @@ -494,7 +494,7 @@ # if this succeeds, it means that user1 received the mail that was intended for chuck. client.fail("fetchmail --nosslcertck -v") - with subtest("extraVirtualAliases"): + with subtest("Test sending from alias address (mailserver.aliases)"): client.execute("rm ~/mail/*") # send email from single-alias to user1 client.succeed( diff --git a/tests/ldap.nix b/tests/ldap.nix index 7cde1d0..40b1390 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -127,7 +127,7 @@ in localDnsResolver = false; indexDir = "/var/lib/dovecot/indices"; - extraVirtualAliases = { + aliases = { # Steal frank@example.com from LDAP user frank "frank@example.com" = "mallory@example.com"; }; From ff5efdeeb6a1d6779452135c807ca7156f13f6d5 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 23 Mar 2026 00:53:43 +0100 Subject: [PATCH 3/3] Update forwards option description Mixing examples and description in the description makes it very noisy and unfocused. --- default.nix | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/default.nix b/default.nix index 2bef03a..8461eb1 100644 --- a/default.nix +++ b/default.nix @@ -683,16 +683,18 @@ in forwards = mkOption { type = with types; attrsOf (either (listOf str) str); example = { - "user@example.com" = "user@elsewhere.com"; + "user@example.com" = "user@example.edu"; + "gamenight@example.com" = [ + "bob@example.com" + "frank@example.org" + "wendy@example.net" + ]; }; description = '' - To forward mails to an external address. For instance, - the value `{ "user@example.com" = "user@elsewhere.com"; }` - means that mails to `user@example.com` are forwarded to - `user@elsewhere.com`. The difference with the - {option}`mailserver.aliases` option is that `user@elsewhere.com` - can't send mail as `user@example.com`. Also, this option - allows to forward mails to external addresses. + Forwards route mail from local addresses to one or more local or external addresses. + + Unlike {option}`mailserver.aliases`, the target addresses cannot send + mail using the forward address. ''; default = { }; };