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";