324 lines
11 KiB
Nix
324 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";
|
|
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;
|
|
storage.path = "/var/lib/dovecot/vmail";
|
|
indexDir = "/var/lib/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 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"
|
|
]))
|
|
'';
|
|
}
|