{ 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"; frankPassword = "testfrank"; 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 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 uid: 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 uid: bob uidNumber: 9999 gidNumber: 9999 sn: Bar mail: bob@example.com homeDirectory: /home/bob userPassword: ${bobPassword} dn: cn=carol,ou=users,dc=example entryUUID: 41240499-27e2-4fa2-be4f-4113a77661b1 objectClass: inetOrgPerson uid: carol sn: Baz mail: carol@example.com userPassword: ${carolPassword} dn: cn=frank,ou=users,dc=example entryUUID: ca16f594-f6b2-418f-87d3-0d02d746461f objectClass: inetOrgPerson uid: frank sn: Moo mail: frank@example.com userPassword: ${frankPassword} ''; }; mailserver = { enable = true; fqdn = "mail.example.com"; domains = [ "example.com" ]; localDnsResolver = false; indexDir = "/var/lib/dovecot/indices"; aliases = { # Steal frank@example.com from LDAP user frank "frank@example.com" = "mallory@example.com"; }; loginAccounts = { # 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") # 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") 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.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", "--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" ])) ''; }