Merge branch 'ldap-local-coex' into 'master'

ldap: allow coexistence with local accounts

See merge request simple-nixos-mailserver/nixos-mailserver!502
This commit is contained in:
Martin Weinelt
2026-03-23 23:26:33 +00:00
8 changed files with 130 additions and 43 deletions
-2
View File
@@ -70,8 +70,6 @@ SNM branch corresponding to your NixOS version.
* [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac) * [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac)
* Improve the Forwarding Experience * Improve the Forwarding Experience
* [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html) * [ ] 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 * OpenID Connect
* Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166) * Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166)
+20 -19
View File
@@ -649,7 +649,7 @@ in
''; '';
}; };
extraVirtualAliases = mkOption { aliases = mkOption {
type = type =
let let
loginAccount = mkOptionType { loginAccount = mkOptionType {
@@ -660,7 +660,6 @@ in
with types; with types;
attrsOf (either loginAccount (nonEmptyListOf loginAccount)); attrsOf (either loginAccount (nonEmptyListOf loginAccount));
example = { example = {
"info@example.com" = "user1@example.com";
"postmaster@example.com" = "user1@example.com"; "postmaster@example.com" = "user1@example.com";
"abuse@example.com" = "user1@example.com"; "abuse@example.com" = "user1@example.com";
"multi@example.com" = [ "multi@example.com" = [
@@ -669,15 +668,14 @@ in
]; ];
}; };
description = '' description = ''
Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that Aliases are additional mail addresses routed to one or more existing local accounts.
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 The target accounts are allowed to use the alias as the sender address.
forwarded to some valid email address. (Alternatively you can create login
accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows :::{note}
the user `user1@example.com` to send emails as `info@example.com`. This feature is limited to local accounts and does not support LDAP or
It's also possible to create an alias for multiple accounts. In this other external accounts.
example all mails for `multi@example.com` will be forwarded to both :::
`user1@example.com` and `user2@example.com`.
''; '';
default = { }; default = { };
}; };
@@ -685,16 +683,18 @@ in
forwards = mkOption { forwards = mkOption {
type = with types; attrsOf (either (listOf str) str); type = with types; attrsOf (either (listOf str) str);
example = { 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 = '' description = ''
To forward mails to an external address. For instance, Forwards route mail from local addresses to one or more local or external addresses.
the value `{ "user@example.com" = "user@elsewhere.com"; }`
means that mails to `user@example.com` are forwarded to Unlike {option}`mailserver.aliases`, the target addresses cannot send
`user@elsewhere.com`. The difference with the mail using the forward address.
{option}`mailserver.extraVirtualAliases` option is that `user@elsewhere.com`
can't send mail as `user@example.com`. Also, this option
allows to forward mails to external addresses.
''; '';
default = { }; default = { };
}; };
@@ -1681,5 +1681,6 @@ in
[ "mailserver" "ldap" "postfix" "mailAttribute" ] [ "mailserver" "ldap" "postfix" "mailAttribute" ]
[ "mailserver" "ldap" "attributes" "mail" ] [ "mailserver" "ldap" "attributes" "mail" ]
) )
(mkRenamedOptionModule [ "mailserver" "extraVirtualAliases" ] [ "mailserver" "aliases" ])
]; ];
} }
+21 -9
View File
@@ -40,15 +40,25 @@ follow best practices to simplify maintenance.
Limitations Limitations
~~~~~~~~~~~ ~~~~~~~~~~~
We have various assertions in place, that prevent using LDAP together with Design choices
other features. Most of them are not technical limitations per se, but instead ^^^^^^^^^^^^^^
lack configuration or validation.
- Local users (:option:`mailserver.loginAccounts`) and aliases These are intentional choices in how the mail server operates that affect the
(:option:`mailserver.extraVirtualAliases`) are not currently allowed with LDAP integration.
:option:`mailserver.ldap.enable` enabled
- Aliases based on LDAP attributes are currently not implemented - For mail address routing local accounts always take priority over LDAP accounts.
- Quotas based on LDAP attributes are currently not implemented
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 The following features will likely never be implemented, since they would
complicate the setup significantly. complicate the setup significantly.
@@ -58,7 +68,9 @@ complicate the setup significantly.
- Use of ``homeDirectory``, ``uid``, ``gid`` LDAP attributes (we are - Use of ``homeDirectory``, ``uid``, ``gid`` LDAP attributes (we are
committed to a virtual setup with one vmail user/uid/gid and UUID based home committed to a virtual setup with one vmail user/uid/gid and UUID based home
directories) directories)
- 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 Enabling LDAP support
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
+2
View File
@@ -30,6 +30,8 @@ NixOS 26.05
rather than their email address, which is more convenient and consistent rather than their email address, which is more convenient and consistent
with typical LDAP practices. The exact attribute can be customized through with typical LDAP practices. The exact attribute can be customized through
:option:`mailserver.ldap.attributes.username`. :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 - The following integrations are deprecated and will be removed before the next
release: release:
-10
View File
@@ -98,16 +98,6 @@ in
) config.mailserver.dkim.domains ) 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") lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")
[ [
+1 -1
View File
@@ -94,7 +94,7 @@ let
mergeLookupTables lookupTables; mergeLookupTables lookupTables;
# extra_valiases_postfix :: Map String [String] # extra_valiases_postfix :: Map String [String]
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases; extra_valiases_postfix = attrsToLookupTable cfg.aliases;
# forwards :: Map String [String] # forwards :: Map String [String]
forwards = attrsToLookupTable cfg.forwards; forwards = attrsToLookupTable cfg.forwards;
+2 -2
View File
@@ -85,7 +85,7 @@
}; };
}; };
extraVirtualAliases = { aliases = {
"single-alias@example.com" = "user1@example.com"; "single-alias@example.com" = "user1@example.com";
"multi-alias@example.com" = [ "multi-alias@example.com" = [
"user1@example.com" "user1@example.com"
@@ -494,7 +494,7 @@
# if this succeeds, it means that user1 received the mail that was intended for chuck. # if this succeeds, it means that user1 received the mail that was intended for chuck.
client.fail("fetchmail --nosslcertck -v") client.fail("fetchmail --nosslcertck -v")
with subtest("extraVirtualAliases"): with subtest("Test sending from alias address (mailserver.aliases)"):
client.execute("rm ~/mail/*") client.execute("rm ~/mail/*")
# send email from single-alias to user1 # send email from single-alias to user1
client.succeed( client.succeed(
+84
View File
@@ -1,7 +1,25 @@
{
pkgs,
...
}:
let let
hashPassword =
password:
pkgs.runCommand "password-${password}-hashed"
{
buildInputs = [ pkgs.mkpasswd ];
inherit password;
}
''
mkpasswd -s <<<"$password" > $out
'';
bindPassword = "unsafegibberish"; bindPassword = "unsafegibberish";
alicePassword = "testalice"; alicePassword = "testalice";
bobPassword = "testbob"; bobPassword = "testbob";
carolPassword = "testcarol";
frankPassword = "testfrank";
malloryPassword = "testmallory";
in in
{ {
name = "ldap"; name = "ldap";
@@ -83,6 +101,22 @@ in
mail: bob@example.com mail: bob@example.com
homeDirectory: /home/bob homeDirectory: /home/bob
userPassword: ${bobPassword} 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; localDnsResolver = false;
indexDir = "/var/lib/dovecot/indices"; indexDir = "/var/lib/dovecot/indices";
aliases = {
# 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 = { ldap = {
enable = true; enable = true;
uris = [ uris = [
@@ -237,5 +289,37 @@ in
])) ]))
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob'") 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"
]))
''; '';
} }