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:
+16
-8
@@ -379,19 +379,24 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dovecot = {
|
attributes = {
|
||||||
userAttrs = mkOption {
|
uuid = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.str;
|
||||||
default = null;
|
default = "entryUUID";
|
||||||
|
example = "uuid";
|
||||||
description = ''
|
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
|
Typically the `entryUUID` attribute as defined by [RFC4530].
|
||||||
https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#user-attrs
|
|
||||||
in the Dovecot manual.
|
[RFC4530]: https://www.rfc-editor.org/rfc/rfc4530.html
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
dovecot = {
|
||||||
userFilter = mkOption {
|
userFilter = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "mail=%{user}";
|
default = "mail=%{user}";
|
||||||
@@ -1630,5 +1635,8 @@ in
|
|||||||
[ "mailserver" "dkimKeyBits" ]
|
[ "mailserver" "dkimKeyBits" ]
|
||||||
[ "mailserver" "dkim" "defaults" "keyLength" ]
|
[ "mailserver" "dkim" "defaults" "keyLength" ]
|
||||||
)
|
)
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "ldap" "dovecot" "userAttrs" ] ''
|
||||||
|
The user_attrs field is now used internally to map the home and mail directories.
|
||||||
|
'')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ let
|
|||||||
|
|
||||||
postfixCfg = config.services.postfix;
|
postfixCfg = config.services.postfix;
|
||||||
|
|
||||||
|
ldapUuidAttribute = cfg.ldap.attributes.uuid;
|
||||||
ldapConfig = pkgs.writeTextFile {
|
ldapConfig = pkgs.writeTextFile {
|
||||||
name = "dovecot-ldap.conf.ext.template";
|
name = "dovecot-ldap.conf.ext.template";
|
||||||
text = ''
|
text = ''
|
||||||
@@ -76,9 +77,12 @@ let
|
|||||||
auth_bind = yes
|
auth_bind = yes
|
||||||
base = ${cfg.ldap.searchBase}
|
base = ${cfg.ldap.searchBase}
|
||||||
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
||||||
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
|
user_attrs = \
|
||||||
user_attrs = ${cfg.ldap.dovecot.userAttrs}
|
${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}
|
user_filter = ${cfg.ldap.dovecot.userFilter}
|
||||||
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
|
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
|
||||||
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
|
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
|
||||||
@@ -444,13 +448,8 @@ in
|
|||||||
driver = ldap
|
driver = ldap
|
||||||
args = ${ldapConfFile}
|
args = ${ldapConfFile}
|
||||||
default_fields = \
|
default_fields = \
|
||||||
home=${cfg.mailDirectory}/ldap/%{user} \
|
|
||||||
uid=${toString cfg.vmailUID} \
|
uid=${toString cfg.vmailUID} \
|
||||||
gid=${toString cfg.vmailUID} \
|
gid=${toString cfg.vmailUID}
|
||||||
mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
|
|
||||||
lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{user}"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
''}
|
''}
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -72,6 +72,7 @@ in
|
|||||||
ou: users
|
ou: users
|
||||||
|
|
||||||
dn: cn=alice,ou=users,dc=example
|
dn: cn=alice,ou=users,dc=example
|
||||||
|
entryUUID: c52f777b-a6e8-4507-80f9-c4de47e8520d
|
||||||
objectClass: inetOrgPerson
|
objectClass: inetOrgPerson
|
||||||
cn: alice
|
cn: alice
|
||||||
sn: Foo
|
sn: Foo
|
||||||
@@ -79,10 +80,16 @@ in
|
|||||||
userPassword: ${alicePassword}
|
userPassword: ${alicePassword}
|
||||||
|
|
||||||
dn: cn=bob,ou=users,dc=example
|
dn: cn=bob,ou=users,dc=example
|
||||||
|
entryUUID: f3b4e8ea-087f-42cc-95f0-cbfd99386092
|
||||||
objectClass: inetOrgPerson
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: posixAccount
|
||||||
cn: bob
|
cn: bob
|
||||||
|
uid: bob
|
||||||
|
uidNumber: 9999
|
||||||
|
gidNumber: 9999
|
||||||
sn: Bar
|
sn: Bar
|
||||||
mail: bob@example.com
|
mail: bob@example.com
|
||||||
|
homeDirectory: /home/bob
|
||||||
userPassword: ${bobPassword}
|
userPassword: ${bobPassword}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
@@ -164,6 +171,12 @@ in
|
|||||||
machine.succeed("doveadm user -u alice@example.com")
|
machine.succeed("doveadm user -u alice@example.com")
|
||||||
machine.succeed("doveadm user -u bob@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"):
|
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/postfix/*.cf | grep -e '-rw------- 1 root root'")
|
||||||
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | 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'")
|
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'")
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user