ldap: migrate to UUID based Dovecot home directories

The LDAP support was not in a good shape when it was merged. This is a
breaking change and course correction to apply best practices going
forward.

This fixes various issues experienced with the Dovecot LDAP home
directory.

The gravest issue is that the `homeDirectory` attribute from
the `posixAccount` schema would overwrite the Dovecot home directory and
cause permission errors. This was possible because we defined the home
variable in `default_fields` that is inherently mutable and just a preset
if no other value gets transmitted from LDAP. This did not surface in
tests, because our LDAP schema was too minimal compared to a common
production dataset.

The most annoying issue and the actual breaking change is that we now
default to UUID based home directories. Every entry in an IDM that
supports LDAP comes with a unique identifier that does not change upon
account name changes. We want those to enable simple account name
migrations that don't require any manual data migration.

To migrate existing dovecot home directories a migration script is
included, which will be backported to the 25.11 release, so the migration
can already be started from the previous release version.
This commit is contained in:
Martin Weinelt
2026-03-12 03:18:48 +01:00
parent fa0d5c9694
commit 091eda1ed2
3 changed files with 37 additions and 21 deletions
+16 -8
View File
@@ -379,19 +379,24 @@ in
'';
};
dovecot = {
userAttrs = mkOption {
type = types.nullOr types.str;
default = null;
attributes = {
uuid = mkOption {
type = types.str;
default = "entryUUID";
example = "uuid";
description = ''
LDAP attributes to be retrieved during userdb lookups.
The long-term stable LDAP attribute to reference accounts across
username changes. Used to determine a stable Dovecot home and
mail directory location.
See the users_attrs reference at
https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#user-attrs
in the Dovecot manual.
Typically the `entryUUID` attribute as defined by [RFC4530].
[RFC4530]: https://www.rfc-editor.org/rfc/rfc4530.html
'';
};
};
dovecot = {
userFilter = mkOption {
type = types.str;
default = "mail=%{user}";
@@ -1630,5 +1635,8 @@ in
[ "mailserver" "dkimKeyBits" ]
[ "mailserver" "dkim" "defaults" "keyLength" ]
)
(mkRemovedOptionModule [ "mailserver" "ldap" "dovecot" "userAttrs" ] ''
The user_attrs field is now used internally to map the home and mail directories.
'')
];
}
+8 -9
View File
@@ -61,6 +61,7 @@ let
postfixCfg = config.services.postfix;
ldapUuidAttribute = cfg.ldap.attributes.uuid;
ldapConfig = pkgs.writeTextFile {
name = "dovecot-ldap.conf.ext.template";
text = ''
@@ -76,9 +77,12 @@ let
auth_bind = yes
base = ${cfg.ldap.searchBase}
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
user_attrs = ${cfg.ldap.dovecot.userAttrs}
''}
user_attrs = \
${ldapUuidAttribute}=${ldapUuidAttribute}, \
=home=/var/vmail/ldap/%{ldap:${ldapUuidAttribute}}, \
=mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{ldap:${ldapUuidAttribute}}"
}
user_filter = ${cfg.ldap.dovecot.userFilter}
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
@@ -444,13 +448,8 @@ in
driver = ldap
args = ${ldapConfFile}
default_fields = \
home=${cfg.mailDirectory}/ldap/%{user} \
uid=${toString cfg.vmailUID} \
gid=${toString cfg.vmailUID} \
mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{user}"
}
gid=${toString cfg.vmailUID}
}
''}
+13 -4
View File
@@ -72,6 +72,7 @@ in
ou: users
dn: cn=alice,ou=users,dc=example
entryUUID: c52f777b-a6e8-4507-80f9-c4de47e8520d
objectClass: inetOrgPerson
cn: alice
sn: Foo
@@ -79,10 +80,16 @@ in
userPassword: ${alicePassword}
dn: cn=bob,ou=users,dc=example
entryUUID: f3b4e8ea-087f-42cc-95f0-cbfd99386092
objectClass: inetOrgPerson
objectClass: posixAccount
cn: bob
uid: bob
uidNumber: 9999
gidNumber: 9999
sn: Bar
mail: bob@example.com
homeDirectory: /home/bob
userPassword: ${bobPassword}
'';
};
@@ -164,6 +171,12 @@ in
machine.succeed("doveadm user -u alice@example.com")
machine.succeed("doveadm user -u bob@example.com")
machine.succeed("doveadm user -f uid bob@example.com | grep ${toString nodes.machine.mailserver.vmailUID}")
machine.succeed("doveadm user -f gid bob@example.com | grep ${toString nodes.machine.mailserver.vmailUID}")
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.mailDirectory}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092")
machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092'")
with subtest("Files containing secrets are only readable by root"):
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'")
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
@@ -234,9 +247,5 @@ in
]))
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'")
with subtest("Check dovecot mail and index locations"):
# If these paths change we need a migration
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.mailDirectory}/ldap/bob@example.com")
machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/bob@example.com'")
'';
}