091eda1ed2
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.
252 lines
8.5 KiB
Nix
252 lines
8.5 KiB
Nix
let
|
|
bindPassword = "unsafegibberish";
|
|
alicePassword = "testalice";
|
|
bobPassword = "testbob";
|
|
in
|
|
{
|
|
name = "ldap";
|
|
|
|
nodes = {
|
|
machine =
|
|
{ pkgs, ... }:
|
|
{
|
|
imports = [
|
|
./../default.nix
|
|
./lib/config.nix
|
|
];
|
|
|
|
virtualisation.memorySize = 1024;
|
|
|
|
services.openssh = {
|
|
enable = true;
|
|
settings.PermitRootLogin = "yes";
|
|
};
|
|
|
|
environment.systemPackages = [
|
|
(pkgs.writeScriptBin "mail-check" ''
|
|
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
|
'')
|
|
];
|
|
|
|
environment.etc.bind-password.text = bindPassword;
|
|
|
|
services.openldap = {
|
|
enable = true;
|
|
settings = {
|
|
children = {
|
|
"cn=schema".includes = [
|
|
"${pkgs.openldap}/etc/schema/core.ldif"
|
|
"${pkgs.openldap}/etc/schema/cosine.ldif"
|
|
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
|
|
"${pkgs.openldap}/etc/schema/nis.ldif"
|
|
];
|
|
"olcDatabase={1}mdb" = {
|
|
attrs = {
|
|
objectClass = [
|
|
"olcDatabaseConfig"
|
|
"olcMdbConfig"
|
|
];
|
|
olcDatabase = "{1}mdb";
|
|
olcDbDirectory = "/var/lib/openldap/example";
|
|
olcSuffix = "dc=example";
|
|
};
|
|
};
|
|
};
|
|
};
|
|
declarativeContents."dc=example" =
|
|
#ldif
|
|
''
|
|
dn: dc=example
|
|
objectClass: domain
|
|
dc: example
|
|
|
|
dn: cn=mail,dc=example
|
|
objectClass: organizationalRole
|
|
objectClass: simpleSecurityObject
|
|
objectClass: top
|
|
cn: mail
|
|
userPassword: ${bindPassword}
|
|
|
|
dn: ou=users,dc=example
|
|
objectClass: organizationalUnit
|
|
ou: users
|
|
|
|
dn: cn=alice,ou=users,dc=example
|
|
entryUUID: c52f777b-a6e8-4507-80f9-c4de47e8520d
|
|
objectClass: inetOrgPerson
|
|
cn: alice
|
|
sn: Foo
|
|
mail: alice@example.com
|
|
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}
|
|
'';
|
|
};
|
|
|
|
mailserver = {
|
|
enable = true;
|
|
fqdn = "mail.example.com";
|
|
domains = [ "example.com" ];
|
|
localDnsResolver = false;
|
|
indexDir = "/var/lib/dovecot/indices";
|
|
|
|
ldap = {
|
|
enable = true;
|
|
uris = [
|
|
"ldap://"
|
|
];
|
|
bind = {
|
|
dn = "cn=mail,dc=example";
|
|
passwordFile = "/etc/bind-password";
|
|
};
|
|
searchBase = "ou=users,dc=example";
|
|
searchScope = "sub";
|
|
};
|
|
|
|
forwards = {
|
|
"bob_fw@example.com" = "bob@example.com";
|
|
};
|
|
|
|
vmailGroupName = "vmail";
|
|
vmailUID = 5000;
|
|
|
|
enableImap = false;
|
|
};
|
|
};
|
|
};
|
|
testScript =
|
|
{
|
|
nodes,
|
|
...
|
|
}:
|
|
# python
|
|
''
|
|
import sys
|
|
import re
|
|
|
|
machine.start()
|
|
machine.wait_for_unit("multi-user.target")
|
|
|
|
# if the schema is broken, fail fast. helps during development.
|
|
machine.wait_for_unit("openldap.service")
|
|
|
|
# TODO put this blocking into the systemd units?
|
|
machine.wait_until_succeeds(
|
|
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
)
|
|
|
|
# This function retrieves the ldap table file from a postconf
|
|
# command.
|
|
# A key lookup is achieved and the returned value is compared
|
|
# to the expected value.
|
|
def test_lookup(postconf_cmdline, key, expected):
|
|
conf = machine.succeed(postconf_cmdline).rstrip()
|
|
ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1)
|
|
value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip()
|
|
try:
|
|
assert value == expected
|
|
except AssertionError:
|
|
print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr)
|
|
raise
|
|
|
|
with subtest("Test postmap lookups"):
|
|
test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com")
|
|
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com")
|
|
|
|
test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com")
|
|
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com")
|
|
|
|
with subtest("Test doveadm lookups"):
|
|
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'")
|
|
|
|
with subtest("Test account/mail address binding via explicit TLS"):
|
|
machine.fail(" ".join([
|
|
"mail-check send-and-read",
|
|
"--smtp-port 587",
|
|
"--smtp-starttls",
|
|
"--smtp-host localhost",
|
|
"--smtp-username alice@example.com",
|
|
"--imap-host localhost",
|
|
"--imap-username bob@example.com",
|
|
"--from-addr bob@example.com",
|
|
"--to-addr aliceb@example.com",
|
|
"--src-password-file <(echo '${alicePassword}')",
|
|
"--dst-password-file <(echo '${bobPassword}')",
|
|
"--ignore-dkim-spf"
|
|
]))
|
|
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'")
|
|
|
|
with subtest("Test mail delivery via implicit TLS"):
|
|
machine.succeed(" ".join([
|
|
"mail-check send-and-read",
|
|
"--smtp-port 465",
|
|
"--smtp-ssl",
|
|
"--smtp-host localhost",
|
|
"--smtp-username alice@example.com",
|
|
"--imap-host localhost",
|
|
"--imap-username bob@example.com",
|
|
"--from-addr alice@example.com",
|
|
"--to-addr bob@example.com",
|
|
"--src-password-file <(echo '${alicePassword}')",
|
|
"--dst-password-file <(echo '${bobPassword}')",
|
|
"--ignore-dkim-spf"
|
|
]))
|
|
|
|
with subtest("Test mail forwarding via explicit TLS works"):
|
|
machine.succeed(" ".join([
|
|
"mail-check send-and-read",
|
|
"--smtp-port 587",
|
|
"--smtp-starttls",
|
|
"--smtp-host localhost",
|
|
"--smtp-username alice@example.com",
|
|
"--imap-host localhost",
|
|
"--imap-username bob@example.com",
|
|
"--from-addr alice@example.com",
|
|
"--to-addr bob_fw@example.com",
|
|
"--src-password-file <(echo '${alicePassword}')",
|
|
"--dst-password-file <(echo '${bobPassword}')",
|
|
"--ignore-dkim-spf"
|
|
]))
|
|
|
|
with subtest("Test cannot send mail via implicit TLS from forwarded address"):
|
|
machine.fail(" ".join([
|
|
"mail-check send-and-read",
|
|
"--smtp-port 465",
|
|
"--smtp-ssl",
|
|
"--smtp-host localhost",
|
|
"--smtp-username bob@example.com",
|
|
"--imap-host localhost",
|
|
"--imap-username alice@example.com",
|
|
"--from-addr bob_fw@example.com",
|
|
"--to-addr alice@example.com",
|
|
"--src-password-file <(echo '${bobPassword}')",
|
|
"--dst-password-file <(echo '${alicePassword}')",
|
|
"--ignore-dkim-spf"
|
|
]))
|
|
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'")
|
|
|
|
'';
|
|
}
|