From 091eda1ed2ec0341303d1f287e92b3b3ebe7c048 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 12 Mar 2026 03:18:48 +0100 Subject: [PATCH] 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. --- default.nix | 24 ++++++++++++++++-------- mail-server/dovecot.nix | 17 ++++++++--------- tests/ldap.nix | 17 +++++++++++++---- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/default.nix b/default.nix index ad000ff..3472d57 100644 --- a/default.nix +++ b/default.nix @@ -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. + '') ]; } diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 7831b0f..fda742a 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -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} } ''} diff --git a/tests/ldap.nix b/tests/ldap.nix index b875715..d2e89fa 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -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'") ''; }