From 5b7eb05635f9ce03fad73cb2b3fc6d40bc045cba Mon Sep 17 00:00:00 2001 From: niten Date: Thu, 21 Sep 2023 18:01:44 -0700 Subject: [PATCH] LDAP with password --- dovecot.nix | 83 +++++---------------- mail-server.nix | 83 ++++++++++++++++++--- postfix.nix | 194 ++++++++++++++++++++++++++++++++++++------------ rspamd.nix | 6 ++ 4 files changed, 244 insertions(+), 122 deletions(-) diff --git a/dovecot.nix b/dovecot.nix index 87c8b78..fbec774 100644 --- a/dovecot.nix +++ b/dovecot.nix @@ -54,7 +54,6 @@ in { description = "Port on which to serve metrics data."; default = 5034; }; - }; mail-user = mkOption { @@ -153,7 +152,7 @@ in { specialUse = "Sent"; }; Archive = { - atuo = "no"; + auto = "no"; specialUse = "Archive"; }; Flagged = { @@ -180,44 +179,9 @@ in { default = 5; }; - ldap = let - ldapOpts = { - options = { - host = mkOption { - type = str; - description = "LDAP hostname."; - }; - - port = mkOption { - type = str; - description = "Port on which LDAP is listening."; - }; - - base = mkOption { - type = str; - description = "Base of the LDAP server database."; - example = "dc=mydomain,dc=org"; - }; - - bind-dn = mkOption { - type = str; - description = '' - DN used for fetching user information. - - Needs access to homeDirectory, uidNumber, gidNumber, and uid, but not - password attributes. - ''; - }; - - bind-password-file = mkOption { - type = str; - description = "Path to file containing bind password for bind-dn."; - }; - }; - }; - in mkOption { - type = nullOr ldapOpts; - default = null; + ldap-conf = mkOption { + type = str; + description = "Path to LDAP dovecot2 configuration."; }; }; @@ -240,7 +204,7 @@ in { group = cfg.mail-group; }; "${cfg.metrics.user}" = { - isSystemUser = true; + isySstemUser = true; group = cfg.metrics.group; }; }; @@ -306,6 +270,9 @@ in { name = "rspam_pipe_bin"; paths = [ learnHam learnSpam ]; }; + + mailUserUid = config.users.users."${cfg.mail-user}".uid; + mailUserGid = config.users.group."${cfg.mail-group}".gid; in '' ## Extra Config @@ -331,6 +298,8 @@ in { # When looking up usernames, just use the name, not the full address auth_username_format = %n + auth_mechanisms = login plain + service lmtp { # Enable logging in debug mode ${optionalString cfg.debug "executable = lmtp -L"} @@ -346,14 +315,13 @@ in { # user = root } - auth_mechanisms = login plain - ${optionalString (cfg.dovecot.ldap != null) '' - passdb { - driver = ldap - args = ${cfg.dovecot.ldap.generated-ldap-config} - } - ''} + passdb { + driver = ldap + args = ${cfg.ldap-conf} + } + + # All users map to one actual system user userdb { driver = static args = uid=${ @@ -361,23 +329,6 @@ in { } home=${cfg.state-directory}/mail/%u } - # Used by postfix to authorize users - service auth { - inet_listener auth { - address = 0.0.0.0 - port = ${toString cfg.ports.auth} - } - inet_listener auth-userdb { - address = 0.0.0.0 - port = ${toString cfg.ports.userdb} - } - } - - service auth-worker { - user = ${config.services.dovecot2.user} - idle_kill = 3 - } - service imap { vsz_limit = 1024M } @@ -402,7 +353,7 @@ in { imapsieve_mailbox2_causes = COPY imapsieve_mailbox2_before = file:${sievePath}/report-ham.sieve - sieve_pipe_bin_dir = ${pipeBin}/pipe/bin + sieve_pipe_bin_dir = ${pipeBin}/bin sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment } diff --git a/mail-server.nix b/mail-server.nix index fb06c06..e896197 100644 --- a/mail-server.nix +++ b/mail-server.nix @@ -75,6 +75,44 @@ in { }; }; + fudo.secrets.host-secrets."${hostname}" = { + dovecotLdapConfig = { + source-file = pkgs.writeText "dovecot-ldap.conf" + (concatStringsSep "\n" [ + "uris = ldap://ldap-proxymine:${ldapPort}" + "ldap_version = 3" + "dn = ${cfg.ldap.bind-dn}" + "dnpass = ${readFile cfg.ldap.bind-password-file}" + "auth_bind = yes" + "auth_bind_userdn = uid=%u,${cfg.ldap.member-ou},${cfg.ldap.base}" + "base = ${cfg.ldap.base}" + ]); + target-file = "/run/dovecot-secret/ldap.conf"; + }; + }; + + users.users = { + mailserver-dovecot = { + uid = 4455; + isSystemUser = true; + }; + mailserver-antivirus = { + uid = 4456; + isSystemUser = true; + + }; + mailserver-dkim = { + uid = 4457; + isSystemUser = true; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.state-directory}/dovecot 0700 mailserver-dovecot - - -" + "d ${cfg.state-directory}/antivirus 0700 mailserver-antivirus - - -" + "d ${cfg.state-directory}/dkim 0700 mailserver-dkim - - -" + ]; + virtualisation.arion.projects.mail-server.settings = let image = { pkgs, ... }: { project.name = "fudo-mailserver"; @@ -89,6 +127,10 @@ in { authPort = 5447; userdbPort = 5448; metricsPort = 5034; + mkUserMap = username: + let uid = config.users.users."${username}".uid; + in "${uid}:${uid}"; + in { smtp = { networks = [ @@ -96,14 +138,19 @@ in { # Needs access to internet to forward emails "external_network" ]; + volumes = [ + "${hostSecrets.dovecotLdapConfig.target-file}:/run/dovecot2/conf.d/ldap.conf:ro" + ]; ports = [ "25:25" "587:587" "465:465" "2525:2525" ]; nixos = { useSystemd = true; configuration = [ (import ./postfix.nix) + (import ./dovecot.nix) { boot.tmpOnTmpfs = true; system.nssModules = lib.mkForce [ ]; + fudo.mail.postfix = { enable = true; debug = cfg.debug; @@ -127,6 +174,19 @@ in { sasl-domain = cfg.sasl-domain; message-size-limit = cfg.message-size-limit; ports = { metrics = metricsPort; }; + rspamd-server = { + host = "antispam"; + port = antispamPort; + }; + lmtp-server = { + host = "imap"; + port = lmtpPort; + }; + dkim-server = { + host = "dkim"; + port = dkimPort; + }; + ldap-conf = "/run/dovecot2/conf.d/ldap.conf"; }; } ]; @@ -135,6 +195,11 @@ in { imap = { networks = [ "internal_network" ]; ports = [ "143:143" "993:993" ]; + user = mkUserMap "mailserver-dovecot"; + volumes = [ + "${cfg.state-directory}/dovecot:/state" + "${hostSecrets.dovecotLdapConfig.target-file}:/run/dovecot2/conf.d/ldap.conf:ro" + ]; nixos = { useSystemd = true; configuration = [ @@ -145,7 +210,7 @@ in { fudo.mail.dovecot = { enable = true; debug = cfg.debug; - state-directory = "${cfg.state-directory}/dovecot"; + state-directory = "/state"; ports = { lmtp = lmtpPort; auth = authPort; @@ -162,13 +227,7 @@ in { host = "antispam"; port = antispamPort; }; - ldap = mkIf cfg.ldap-proxy { - host = "ldap-proxy"; - port = 3389; - base = cfg.ldap.base; - bind-dn = cfg.ldap.bind-dn; - bind-password-file = cfg.ldap.bind-password-file; - }; + ldap-conf = "/run/dovecot2/conf.d/ldap.conf"; }; } ]; @@ -219,6 +278,8 @@ in { # Needs external access for database updates "external_network" ]; + user = mkUserMap "mailserver-antivirus"; + volumes = [ "${cfg.state-directory}/antivirus:/state" ]; nixos = { useSystemd = true; configuration = [ @@ -228,7 +289,7 @@ in { system.nssModules = lib.mkForce [ ]; fudo.mail.clamav = { enable = true; - state-directory = "${cfg.state-directory}/rspamd"; + state-directory = "/state"; port = antispamPort; }; } @@ -237,6 +298,8 @@ in { }; dkim = { networks = [ "internal_network" ]; + user = mkUserMap "mailserver-dkim"; + volumes = [ "${cfg.state-directory}/dkim:/state" ]; nixos = { useSystemd = true; configuration = [ @@ -250,7 +313,7 @@ in { domains = [ cfg.primary-domain ] ++ cfg.extra-domains; }; port = dkimPort; - state-directory = "${cfg.state-directory}/dkim"; + state-directory = "/state"; } ]; }; diff --git a/postfix.nix b/postfix.nix index 24d3227..291d002 100644 --- a/postfix.nix +++ b/postfix.nix @@ -118,6 +118,44 @@ in { default = 1725; }; }; + + ldap-conf = mkOption { + type = str; + description = "Path to LDAP dovecot2 configuration."; + }; + + rspamd-server = { + host = mkOption { + type = str; + description = "Hostname of rspamd server."; + }; + port = mkOption { + type = port; + description = "Port on which rspamd is running."; + }; + }; + + lmtp-server = { + host = mkOption { + type = str; + description = "Hostname of lmtp server."; + }; + port = mkOption { + type = port; + description = "Port on which lmtp is running."; + }; + }; + + dkim-server = { + host = mkOption { + type = str; + description = "Hostname of dkim server."; + }; + port = mkOption { + type = port; + description = "Port on which dkim is running."; + }; + }; }; config = mkIf cfg.enable { @@ -140,6 +178,44 @@ in { port = cfg.metrics-port; }; + dovecot2 = { + enable = true; + enablePAM = false; + enableImap = false; + extraConfig = '' + # Extra Config + ${lib.optionalString cfg.debug "auth_debug = yes"} + + # When looking up usernames, just use the name, not the full address + auth_username_format = %n + + auth_mechanisms = login plain + + passdb { + driver = ldap + args = ${ldapConfig} + } + + userdb = { + driver = ldap + args = ${ldap-conf} + } + + service auth { + unix_listener auth { + mode = 0600 + user = ${config.services.postfix.user} + group = ${config.services.postfix.group} + } + } + + service auth-worker { + user = ${config.services.dovecot2.user} + idle_kill = 3 + } + ''; + }; + postfix = { enable = true; @@ -199,6 +275,52 @@ in { pcreFile = name: "pcre:/var/lib/postfix/conf/${name}"; mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; + sender-restrictions = [ + "check_sender_access ${mapped-file "reject_senders"}" + "reject_sender_login_mismatch" + "reject_non_fqdn_sender" + "reject_unknown_sender_domain" + "permit_mynetworks" + "permit_sasl_authenticated" + ] ++ (map (blacklist: "reject_rbl_client ${blacklist}") + cfg.blacklist.dns) ++ [ "reject" ]; + + relay-restrictions = [ + "reject_unauth_destination" + "reject_unauth_pipelining" + "reject_unauth_destination" + "reject_unknown_sender_domain" + "permit_mynetworks" + "permit_sasl_authenticated" + ] ++ (map (blacklist: "reject_rbl_client ${blacklist}") + cfg.blacklist.dns) ++ [ "reject" ]; + + recipient-restrictions = [ + "check_sender_access ${mapped-file "reject_recipients"}" + "reject_unknown_sender_domain" + "reject_unknown_recipient_domain" + "reject_unauth_pipelining" + "reject_unauth_destination" + "reject_invalid_hostname" + "reject_non_fqdn_hostname" + "reject_non_fqdn_sender" + "reject_non_fqdn_recipient" + "check_policy_service unix:private/policy-spf" + ] ++ (map (blacklist: "reject_rbl_client ${blacklist}") + cfg.blacklist.dns) + ++ [ "permit_mynetworks" "permit_sasl_authenticated" "reject" ]; + + client-restrictions = + [ "permit_sasl_authenticated" "permit_mynetworks" "reject" ]; + + helo-restrictions = [ + "permit_mynetworks" + "reject_invalid_hostname" + "reject_non_fqdn_helo_hostname" + "reject_unknown_helo_hostname" + ] ++ (map (blacklist: "reject_rbl_client ${blacklist}") + cfg.blacklist.dns) ++ [ "permit" ]; + in { virtual_mailbox_domains = allDomains; virtual_mailbox_maps = mappedFile "virtual_mailbox_map"; @@ -209,7 +331,8 @@ in { # virtual_gid_maps = let gid = config.users.groups."${cfg.group}".gid; # in "static: ${toString gid}"; - virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; + virtual_transport = + "lmtp:inet:${cfg.lmtp-server.host}:${cfg.lmtp-server.port}"; message_size_limit = toString (cfg.message-size-limit * 1024 * 1024); @@ -226,6 +349,7 @@ in { smtpd_sasl_path = "/run/dovecot2/auth"; smtpd_sasl_auth_enable = "yes"; smtpd_sasl_local_domain = cfg.sasl-domain; + smtp_sasl_authenticated_header = "yes"; smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_tls_security_options = "noanonymous"; @@ -241,47 +365,24 @@ in { "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; smtpd_milters = [ - "unix:/run/rspamd/rspamd-milter.sock" - "unix:/var/run/opendkim/opendkim.sock" + "inet:${cfg.rspamd-server.host}:${cfg.rspamd-server.port}" + "inet:${cfg.dkim.host}:${cfg.dkim.port}" ]; non_smtpd_milters = [ - "unix:/run/rspamd/rspamd-milter.sock" - "unix:/var/run/opendkim/opendkim.sock" + "inet:${cfg.rspamd-server.host}:${cfg.rspamd-server.port}" + "inet:${cfg.dkim.host}:${cfg.dkim.port}" ]; - smtpd_relay_restrictions = [ - "permit_mynetworks" - "permit_sasl_authenticated" - "reject_unauth_destination" - "reject_unauth_pipelining" - "reject_unauth_destination" - "reject_unknown_sender_domain" - ]; + helo_required = true; - smtpd_sender_restrictions = [ - "check_sender_access ${mapped-file "reject_senders"}" - "permit_mynetworks" - "permit_sasl_authenticated" - "reject_unknown_sender_domain" - ]; + smtpd_relay_restrictions = relay-restrictions; - smtpd_recipient_restrictions = [ - "check_sender_access ${mapped-file "reject_recipients"}" - "permit_mynetworks" - "permit_sasl_authenticated" - "check_policy_service unix:private/policy-spf" - "reject_unknown_recipient_domain" - "reject_unauth_pipelining" - "reject_unauth_destination" - "reject_invalid_hostname" - "reject_non_fqdn_hostname" - "reject_non_fqdn_sender" - "reject_non_fqdn_recipient" - ]; + smtpd_sender_restrictions = sender-restrictions; - smtpd_helo_restrictions = - [ "permit_mynetworks" "reject_invalid_hostname" "permit" ]; + smtpd_recipient_restrictions = recipient-restrictions; + + smtpd_helo_restrictions = helo-restrictions; # Handled by submission smtpd_tls_security_level = "may"; @@ -328,16 +429,16 @@ in { smtpd_sasl_path = "/run/dovecot2/auth"; smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_local_domain = cfg.domain; - smtpd_client_restrictions = "permit_sasl_authenticated,reject"; - smtpd_sender_restrictions = - "reject_sender_login_mismatch,reject_unknown_sender_domain"; - smtpd_recipient_restrictions = - "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; + smtpd_client_restrictions = client-restrictions; + smtpd_sender_restrictions = sender-restrictions; + smtpd_recipient_restrictions = recipient-restrictions; cleanup_service_name = "submission-header-cleanup"; }; masterConfig = { - "policy-spf" = let + # See: http://www.postfix.org/smtp.8.html + lmtp.args = [ "flags=DO" ]; + policy-spf = let policySpfFile = pkgs.writeText "policyd-spf.conf" (cfg.postfix.policy-spf.extraConfig + (lib.optionalString cfg.debug "debugLevel = 4")); @@ -352,18 +453,19 @@ in { "${policydSpf}" ]; }; - "submission-header-cleanup" = let + submission-header-cleanup = let submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" '' # Removes sensitive headers from mails handed in via the submission port. # See https://thomas-leister.de/mailserver-debian-stretch/ # Uses "pcre" style regex. - /^Received:/ IGNORE - /^X-Originating-IP:/ IGNORE - /^X-Mailer:/ IGNORE - /^User-Agent:/ IGNORE - /^X-Enigmail:/ IGNORE + /^Received:/ IGNORE + /^X-Originating-IP:/ IGNORE + /^X-Mailer:/ IGNORE + /^User-Agent:/ IGNORE + /^X-Enigmail:/ IGNORE + /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.hostname}> ''; in { type = "unix"; diff --git a/rspamd.nix b/rspamd.nix index 590b17c..2da4e2a 100644 --- a/rspamd.nix +++ b/rspamd.nix @@ -62,6 +62,12 @@ in { scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all } ''; + + "rbl.conf".text = '' + rbls { + an_rbl + } + ''; }; overrides."milter_headers.conf".text = "extended_spam_headers = true;";