eea473ea12
LDAP bind auth used to be enabled by default (and not configurable) before the dovecot 2.4 migration. I changed the default option value to match the old Dovecot 2.3 behavior. The use of authentication bind is required for LDAP servers that simply do not have such LDAP attribute like Kanidm, or in cases where the password scheme used is not supported by Dovecot.
380 lines
13 KiB
Nix
380 lines
13 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, lib, ... }:
|
|
{
|
|
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";
|
|
attributes = {
|
|
# disable auth bind
|
|
password = "userPassword";
|
|
};
|
|
};
|
|
|
|
forwards = {
|
|
"bob_fw@example.com" = "bob@example.com";
|
|
};
|
|
};
|
|
|
|
specialisation.auth_bind = {
|
|
inheritParentConfig = true;
|
|
configuration = {
|
|
mailserver = {
|
|
ldap = {
|
|
attributes = {
|
|
# enable auth bind
|
|
password = lib.mkForce null;
|
|
};
|
|
};
|
|
};
|
|
|
|
services.openldap.settings.children = {
|
|
"olcDatabase={1}mdb" = {
|
|
attrs = {
|
|
olcAccess = [
|
|
# disallow access to userPassword
|
|
''
|
|
to * attrs=userPassword
|
|
by anonymous auth
|
|
by * none
|
|
''
|
|
|
|
# default policy (same as if we would specify none as all)
|
|
''
|
|
to *
|
|
by * read
|
|
''
|
|
];
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
};
|
|
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_match = re.match('.* =.*ldap:(.*)', conf)
|
|
if not ldap_table_path_match:
|
|
raise RuntimeError(f"Failed to match LDAP table in '{postconf_cmdline}' response")
|
|
ldap_table_path = ldap_table_path_match.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"
|
|
]))
|
|
|
|
with subtest("LDAP Authentication Binds"):
|
|
machine.succeed("/run/booted-system/specialisation/auth_bind/bin/switch-to-configuration test")
|
|
machine.wait_for_unit("openldap.service")
|
|
machine.succeed("doveadm auth test alice '${alicePassword}'")
|
|
'';
|
|
}
|