e9337b346f
The "login" prefix makes this option more confusing rather than clearer, because what other account types are there? LDAP ones for example, but you can login with those too, so the prefix is pointless.
327 lines
11 KiB
Nix
327 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;
|
|
mailDirectory = "/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")
|
|
|
|
# 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"
|
|
]))
|
|
'';
|
|
}
|