From 08f077c5ca907c8b7f386eb9fdac251f31f711dc Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 19 Jul 2022 23:45:29 +0200 Subject: [PATCH] Add support for LDAP users Allow configuring lookups for users and their mail addresses from an LDAP directory. The LDAP username will be used as an accountname as opposed to the email address used as the `loginName` for declarative accounts. Mailbox for LDAP users will be stored below `/var/vmail/ldap/`. Configuring domains is out of scope, since domains require further configuration within the NixOS mailserver construct to set up all related services accordingly. Aliases can already be configured using `mailserver.forwards` but could be supported using LDAP at a later point. --- default.nix | 150 ++++++++++++++++++++++++++++++++++++++++ mail-server/dovecot.nix | 50 ++++++++++++++ mail-server/postfix.nix | 37 +++++++++- 3 files changed, 235 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index 2dfbbfb..4b684d1 100644 --- a/default.nix +++ b/default.nix @@ -198,6 +198,156 @@ in default = {}; }; + ldap = { + enable = mkEnableOption "LDAP support"; + + uris = mkOption { + type = types.listOf types.str; + example = literalExpression '' + [ + "ldaps://ldap1.example.com" + "ldaps://ldap2.example.com" + ] + ''; + description = '' + URIs where your LDAP server can be reached + ''; + }; + + startTls = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable StartTLS upon connection to the server. + ''; + }; + + tlsCAFile = mkOption { + type = types.path; + default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + description = '' + Certifificate trust anchors used to verify the LDAP server certificate. + ''; + }; + + bind = { + dn = mkOption { + type = types.str; + example = "cn=mail,ou=accounts,dc=example,dc=com"; + description = '' + Distinguished name used by the mail server to do lookups + against the LDAP servers. + ''; + }; + + password = mkOption { + type = types.str; + example = "not$4f3"; + description = '' + Password required to authenticate against the LDAP servers. + ''; + }; + }; + + searchBase = mkOption { + type = types.str; + example = "ou=people,ou=accounts,dc=example,dc=com"; + description = '' + Base DN at below which to search for users accounts. + ''; + }; + + searchScope = mkOption { + type = types.enum [ "sub" "base" "one" ]; + default = "sub"; + description = '' + Search scope below which users accounts are looked for. + ''; + }; + + dovecot = { + userAttrs = mkOption { + type = types.str; + default = ""; + description = '' + LDAP attributes to be retrieved during userdb lookups. + + See the users_attrs reference at + https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-attrs + in the Dovecot manual. + ''; + }; + + userFilter = mkOption { + type = types.str; + default = "cn=%u"; + example = "(&(objectClass=inetOrgPerson)(cn=%u))"; + description = '' + Filter for user lookups in Dovecot. + + See the user_filter reference at + https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-filter + in the Dovecot manual. + ''; + }; + + passAttrs = mkOption { + type = types.str; + default = "userPassword=password"; + description = '' + LDAP attributes to be retrieved during passdb lookups. + + See the pass_attrs reference at + https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-attrs + in the Dovecot manual. + ''; + }; + + passFilter = mkOption { + type = types.str; + default = "cn=%u"; + example = "(&(objectClass=inetOrgPerson)(cn=%u))"; + description = '' + Filter for password lookups in Dovecot. + + See the pass_filter reference for + https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-filter + in the Dovecot manual. + ''; + }; + }; + + postfix = { + filter = mkOption { + type = types.str; + default = "mail=%s"; + example = "(&(objectClass=inetOrgPerson)(mail=%s))"; + description = '' + LDAP filter used to search for an account by mail, where + %s is a substitute for the address in + question. + ''; + }; + + uidAttribute = mkOption { + type = types.str; + default = "cn"; + example = "uid"; + description = '' + The LDAP attribute referencing the account name for a user. + ''; + }; + + mailAttribute = mkOption { + type = types.str; + default = "mail"; + description = '' + The LDAP attribute holding mail addresses for a user. + ''; + }; + }; + }; + indexDir = mkOption { type = types.nullOr types.str; default = null; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 6730b61..33dc3c8 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -99,6 +99,12 @@ let # The assertion garantees there is exactly one Junk mailbox. junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else ""; + mkLdapSearchScope = scope: ( + if scope == "sub" then "subtree" + else if scope == "one" then "onelevel" + else scope + ); + in { config = with cfg; lib.mkIf enable { @@ -236,6 +242,19 @@ in default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} } + ${lib.optionalString cfg.ldap.enable '' + passdb { + driver = ldap + args = /etc/dovecot/dovecot-ldap.conf.ext + } + + userdb { + driver = ldap + args = /etc/dovecot/dovecot-ldap.conf.ext + default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} + } + ''} + service auth { unix_listener auth { mode = 0660 @@ -298,6 +317,37 @@ in ''; }; + environment.etc = lib.optionalAttrs (cfg.ldap.enable) { + "dovecot/dovecot-ldap.conf.ext" = { + mode = "0600"; + uid = config.ids.uids.dovecot2; + gid = config.ids.gids.dovecot2; + text = '' + ldap_version = 3 + uris = ${lib.concatStringsSep " " cfg.ldap.uris} + ${lib.optionalString cfg.ldap.startTls '' + tls = yes + ''} + tls_require_cert = hard + tls_ca_cert_file = ${cfg.ldap.tlsCAFile} + dn = ${cfg.ldap.bind.dn} + dnpass = ${cfg.ldap.bind.password} + sasl_bind = no + auth_bind = yes + base = ${cfg.ldap.searchBase} + scope = ${mkLdapSearchScope cfg.ldap.searchScope} + ${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") '' + user_attrs = ${cfg.ldap.dovecot.user_attrs} + ''} + user_filter = ${cfg.ldap.dovecot.userFilter} + ${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") '' + pass_attrs = ${cfg.ldap.dovecot.passAttrs} + ''} + pass_filter = ${cfg.ldap.dovecot.passFilter} + ''; + }; + }; + systemd.services.dovecot2 = { preStart = '' ${genPasswdScript} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 340122b..9ccf7bb 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -133,11 +133,40 @@ let smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_local_domain = "$myhostname"; smtpd_client_restrictions = "permit_sasl_authenticated,reject"; - smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts"; + smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMap}"}"; smtpd_sender_restrictions = "reject_sender_login_mismatch"; smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; cleanup_service_name = "submission-header-cleanup"; }; + + commonLdapConfig = lib.optionalString (cfg.ldap.enable) '' + server_host = ${lib.concatStringsSep " " cfg.ldap.uris} + start_tls = ${if cfg.ldap.startTls then "yes" else "no"} + version = 3 + tls_ca_cert_file = ${cfg.ldap.tlsCAFile} + tls_require_cert = yes + + search_base = ${cfg.ldap.searchBase} + scope = ${cfg.ldap.searchScope} + + bind = yes + bind_dn = ${cfg.ldap.bind.dn} + bind_pw = ${cfg.ldap.bind.password} + ''; + + ldapSenderLoginMap = lib.optionalString (cfg.ldap.enable) + (pkgs.writeText "ldap-sender-login-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.uidAttribute} + ''); + + ldapVirtualMailboxMap = lib.optionalString (cfg.ldap.enable) + (pkgs.writeText "ldap-virtual-mailbox-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.uidAttribute} + ''); in { config = with cfg; lib.mkIf enable { @@ -170,7 +199,11 @@ in virtual_gid_maps = "static:5000"; virtual_mailbox_base = mailDirectory; virtual_mailbox_domains = vhosts_file; - virtual_mailbox_maps = mappedFile "valias"; + virtual_mailbox_maps = [ + (mappedFile "valias") + ] ++ lib.optionals (cfg.ldap.enable) [ + "ldap:${ldapVirtualMailboxMap}" + ]; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients lmtp_destination_recipient_limit = "1";