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)
* 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)
+20 -19
View File
@@ -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 = { };
};
@@ -685,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.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.
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 = { };
};
@@ -1681,5 +1681,6 @@ in
[ "mailserver" "ldap" "postfix" "mailAttribute" ]
[ "mailserver" "ldap" "attributes" "mail" ]
)
(mkRenamedOptionModule [ "mailserver" "extraVirtualAliases" ] [ "mailserver" "aliases" ])
];
}
+21 -9
View File
@@ -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.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
~~~~~~~~~~~~~~~~~~~~~
+2
View File
@@ -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:
-10
View File
@@ -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")
[
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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(
+84
View File
@@ -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";
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 = {
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"
]))
'';
}