Files
simple-nixos-mailserver/tests/ldap.nix
T
2026-04-20 15:39:36 +02:00

333 lines
11 KiB
Nix

{
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";
malloryPassword = "testmallory";
in
{
name = "ldap";
nodes = {
machine =
{ pkgs, ... }:
{
imports = [
../default.nix
./lib/config.nix
];
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
# unsafegibberish
userPassword: {SSHA}JNr6l3s/RHo1LKRXqFsJg8sXznyRid8L
dn: ou=users,dc=example
objectClass: organizationalUnit
ou: users
dn: cn=alice,ou=users,dc=example
entryUUID: c52f777b-a6e8-4507-80f9-c4de47e8520d
objectClass: inetOrgPerson
uid: alice
sn: Foo
mail: alice@example.com
# testalice
userPassword: {SSHA}gkJq4Dm4jfIKjxviR0WD63wMt0Ti6zMB
dn: cn=bob,ou=users,dc=example
entryUUID: f3b4e8ea-087f-42cc-95f0-cbfd99386092
objectClass: inetOrgPerson
objectClass: posixAccount
uid: bob
uidNumber: 9999
gidNumber: 9999
sn: Bar
mail: bob@example.com
homeDirectory: /home/bob
# testbob
userPassword: {SSHA}qqUveZGZrDrjYFnREXLDZc//y89RppVN
dn: cn=carol,ou=users,dc=example
entryUUID: 41240499-27e2-4fa2-be4f-4113a77661b1
objectClass: inetOrgPerson
uid: carol
sn: Baz
mail: carol@example.com
# testcarol
userPassword: {SSHA}69HOuP+OPWE+3+tDucFZxzXDC7p4e3ML
dn: cn=frank,ou=users,dc=example
entryUUID: ca16f594-f6b2-418f-87d3-0d02d746461f
objectClass: inetOrgPerson
uid: frank
sn: Moo
mail: frank@example.com
# testfrank
userPassword: {SSHA}xqtMl8/uJ6HEFWDzLYpAE+Wq7FvKrtkm
'';
};
systemd.services.dovecot.serviceConfig = {
CacheDirectory = "dovecot";
StateDirectory = "dovecot";
};
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" ];
localDnsResolver = false;
storage.path = "/var/lib/dovecot/vmail";
indexDir = "/var/cache/dovecot/indices";
aliases = {
# Steal frank@example.com from LDAP user frank
"frank@example.com" = "mallory@example.com";
};
accounts = {
# 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 = [
"ldap://"
];
bind = {
dn = "cn=mail,dc=example";
passwordFile = "/etc/bind-password";
};
base = "ou=users,dc=example";
scope = "sub";
};
forwards = {
"bob_fw@example.com" = "bob@example.com";
};
};
};
};
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")
machine.wait_for_open_unix_socket("/run/rspamd/rspamd-milter.sock")
# 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")
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice")
test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob")
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob")
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 -u alice")
machine.log(machine.succeed("doveadm user -u bob"))
machine.succeed("doveadm user -f uid bob@example.com | grep ${toString nodes.machine.mailserver.storage.uid}")
machine.succeed("doveadm user -f gid bob@example.com | grep ${toString nodes.machine.mailserver.storage.uid}")
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.storage.path}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092")
machine.succeed("doveadm user -f mail_path bob@example.com | grep ${nodes.machine.mailserver.storage.path}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092")
machine.succeed("doveadm user -f mail_index_path bob@example.com | grep ${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'")
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",
"--imap-host localhost",
"--imap-username bob",
"--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'")
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",
"--imap-host localhost",
"--imap-username bob",
"--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",
"--imap-host localhost",
"--imap-username bob",
"--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",
"--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'")
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"
]))
'';
}