diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 148befc..d2da51b 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -163,283 +163,281 @@ let in { - config = - with cfg; - lib.mkIf enable { - assertions = [ + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = junkMailboxNumber == 1; + message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)"; + } + ]; + + warnings = + lib.optional + ( + (builtins.length cfg.fullTextSearch.languages > 1) + && (builtins.elem "stopwords" cfg.fullTextSearch.filters) + ) + '' + Using stopwords in `mailserver.fullTextSearch.filters` with multiple + languages in `mailserver.fullTextSearch.languages` configured WILL + cause some searches to fail. + + The recommended solution is to NOT use the stopword filter when + multiple languages are present in the configuration. + ''; + + # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads + # the global config and tries to open shared libraries configured in there, + # which are usually not compatible. + environment.systemPackages = [ + pkgs.dovecot_pigeonhole + ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; + + # For compatibility with python imaplib + environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; + + services.dovecot2 = { + enable = true; + enableImap = cfg.enableImap || cfg.enableImapSsl; + enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl; + enablePAM = false; + enableQuota = true; + mailGroup = cfg.vmailGroupName; + mailUser = cfg.vmailUserName; + mailLocation = dovecotMaildir; + sslServerCert = certificatePath; + sslServerKey = keyPath; + enableDHE = lib.mkDefault false; + enableLmtp = true; + mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ + "fts" + "fts_flatcurve" + ]; + protocols = lib.optional cfg.enableManageSieve "sieve"; + + pluginSettings = { + sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; + sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; + sieve_default_name = "default"; + } // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings); + + sieve = { + extensions = [ + "fileinto" + ]; + + scripts.after = builtins.toFile "spam.sieve" '' + require "fileinto"; + + if header :is "X-Spam" "Yes" { + fileinto "${junkMailboxName}"; + stop; + } + ''; + + pipeBins = map lib.getExe [ + (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham") + (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam") + ]; + }; + + imapsieve.mailbox = [ { - assertion = junkMailboxNumber == 1; - message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)"; + name = junkMailboxName; + causes = [ + "COPY" + "APPEND" + ]; + before = ./dovecot/imap_sieve/report-spam.sieve; + } + { + name = "*"; + from = junkMailboxName; + causes = [ "COPY" ]; + before = ./dovecot/imap_sieve/report-ham.sieve; } ]; - warnings = - lib.optional - ( - (builtins.length cfg.fullTextSearch.languages > 1) - && (builtins.elem "stopwords" cfg.fullTextSearch.filters) - ) - '' - Using stopwords in `mailserver.fullTextSearch.filters` with multiple - languages in `mailserver.fullTextSearch.languages` configured WILL - cause some searches to fail. + mailboxes = cfg.mailboxes; - The recommended solution is to NOT use the stopword filter when - multiple languages are present in the configuration. - ''; + extraConfig = '' + #Extra Config + ${lib.optionalString cfg.debug '' + mail_debug = yes + auth_debug = yes + verbose_ssl = yes + ''} - # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads - # the global config and tries to open shared libraries configured in there, - # which are usually not compatible. - environment.systemPackages = [ - pkgs.dovecot_pigeonhole - ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; - - # For compatibility with python imaplib - environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; - - services.dovecot2 = { - enable = true; - enableImap = enableImap || enableImapSsl; - enablePop3 = enablePop3 || enablePop3Ssl; - enablePAM = false; - enableQuota = true; - mailGroup = vmailGroupName; - mailUser = vmailUserName; - mailLocation = dovecotMaildir; - sslServerCert = certificatePath; - sslServerKey = keyPath; - enableDHE = lib.mkDefault false; - enableLmtp = true; - mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ - "fts" - "fts_flatcurve" - ]; - protocols = lib.optional cfg.enableManageSieve "sieve"; - - pluginSettings = { - sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; - sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; - sieve_default_name = "default"; - } // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings); - - sieve = { - extensions = [ - "fileinto" - ]; - - scripts.after = builtins.toFile "spam.sieve" '' - require "fileinto"; - - if header :is "X-Spam" "Yes" { - fileinto "${junkMailboxName}"; - stop; - } - ''; - - pipeBins = map lib.getExe [ - (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham") - (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam") - ]; - }; - - imapsieve.mailbox = [ - { - name = junkMailboxName; - causes = [ - "COPY" - "APPEND" - ]; - before = ./dovecot/imap_sieve/report-spam.sieve; - } - { - name = "*"; - from = junkMailboxName; - causes = [ "COPY" ]; - before = ./dovecot/imap_sieve/report-ham.sieve; - } - ]; - - mailboxes = cfg.mailboxes; - - extraConfig = '' - #Extra Config - ${lib.optionalString debug '' - mail_debug = yes - auth_debug = yes - verbose_ssl = yes - ''} - - ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) '' - service imap-login { - inet_listener imap { - ${ - if cfg.enableImap then - '' - port = 143 - '' - else - '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - '' - } - } - inet_listener imaps { - ${ - if cfg.enableImapSsl then - '' - port = 993 - ssl = yes - '' - else - '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - '' - } + ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) '' + service imap-login { + inet_listener imap { + ${ + if cfg.enableImap then + '' + port = 143 + '' + else + '' + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = 0 + '' } } - ''} - ${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) '' - service pop3-login { - inet_listener pop3 { - ${ - if cfg.enablePop3 then - '' - port = 110 - '' - else - '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - '' - } - } - inet_listener pop3s { - ${ - if cfg.enablePop3Ssl then - '' - port = 995 - ssl = yes - '' - else - '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - '' - } + inet_listener imaps { + ${ + if cfg.enableImapSsl then + '' + port = 993 + ssl = yes + '' + else + '' + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = 0 + '' } } - ''} - - protocol imap { - mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} - mail_plugins = $mail_plugins imap_sieve } - - service imap { - vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB - } - - protocol pop3 { - mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} - } - - mail_access_groups = ${vmailGroupName} - - # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7 - ssl = required - ssl_min_protocol = TLSv1.2 - ssl_prefer_server_ciphers = no - ssl_curve_list = X25519:prime256v1:secp384r1 - - service lmtp { - unix_listener dovecot-lmtp { - group = ${postfixCfg.group} - mode = 0600 - user = ${postfixCfg.user} + ''} + ${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) '' + service pop3-login { + inet_listener pop3 { + ${ + if cfg.enablePop3 then + '' + port = 110 + '' + else + '' + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = 0 + '' + } } - vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB - } - - service quota-status { - inet_listener { - port = 0 + inet_listener pop3s { + ${ + if cfg.enablePop3Ssl then + '' + port = 995 + ssl = yes + '' + else + '' + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = 0 + '' + } } - unix_listener quota-status { - user = postfix - } - vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB } + ''} - recipient_delimiter = ${cfg.recipientDelimiter} - lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} + protocol imap { + mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} + mail_plugins = $mail_plugins imap_sieve + } - protocol lmtp { - mail_plugins = $mail_plugins sieve + service imap { + vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB + } + + protocol pop3 { + mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} + } + + mail_access_groups = ${cfg.vmailGroupName} + + # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7 + ssl = required + ssl_min_protocol = TLSv1.2 + ssl_prefer_server_ciphers = no + ssl_curve_list = X25519:prime256v1:secp384r1 + + service lmtp { + unix_listener dovecot-lmtp { + group = ${postfixCfg.group} + mode = 0600 + user = ${postfixCfg.user} } + vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB + } + service quota-status { + inet_listener { + port = 0 + } + unix_listener quota-status { + user = postfix + } + vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB + } + + recipient_delimiter = ${cfg.recipientDelimiter} + lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} + + protocol lmtp { + mail_plugins = $mail_plugins sieve + } + + passdb { + driver = passwd-file + args = ${passwdFile} + } + + userdb { + driver = passwd-file + args = ${userdbFile} + default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} + } + + ${lib.optionalString cfg.ldap.enable '' passdb { - driver = passwd-file - args = ${passwdFile} + driver = ldap + args = ${ldapConfFile} } userdb { - driver = passwd-file - args = ${userdbFile} - default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} + driver = ldap + args = ${ldapConfFile} + default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} } + ''} - ${lib.optionalString cfg.ldap.enable '' - passdb { - driver = ldap - args = ${ldapConfFile} - } - - userdb { - driver = ldap - args = ${ldapConfFile} - default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} - } - ''} - - service auth { - unix_listener auth { - mode = 0660 - user = ${postfixCfg.user} - group = ${postfixCfg.group} - } + service auth { + unix_listener auth { + mode = 0660 + user = ${postfixCfg.user} + group = ${postfixCfg.group} } + } - auth_mechanisms = plain login + auth_mechanisms = plain login - namespace inbox { - separator = ${cfg.hierarchySeparator} - inbox = yes - } + namespace inbox { + separator = ${cfg.hierarchySeparator} + inbox = yes + } - service indexer-worker { - ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' - vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)} - ''} - } + service indexer-worker { + ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' + vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)} + ''} + } - lda_mailbox_autosubscribe = yes - lda_mailbox_autocreate = yes - ''; - }; - - systemd.services.dovecot2 = { - preStart = - '' - ${genPasswdScript} - '' - + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); - }; - - systemd.services.postfix.restartTriggers = [ - genPasswdScript - ] ++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]); + lda_mailbox_autosubscribe = yes + lda_mailbox_autocreate = yes + ''; }; + + systemd.services.dovecot2 = { + preStart = + '' + ${genPasswdScript} + '' + + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); + }; + + systemd.services.postfix.restartTriggers = [ + genPasswdScript + ] ++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]); + }; } diff --git a/mail-server/environment.nix b/mail-server/environment.nix index b853211..462cb05 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -25,17 +25,15 @@ let cfg = config.mailserver; in { - config = - with cfg; - lib.mkIf enable { - environment.systemPackages = - with pkgs; - [ - dovecot - openssh - postfix - rspamd - ] - ++ (if certificateScheme == "selfsigned" then [ openssl ] else [ ]); - }; + config = lib.mkIf cfg.enable { + environment.systemPackages = + with pkgs; + [ + dovecot + openssh + postfix + rspamd + ] + ++ (if cfg.certificateScheme == "selfsigned" then [ openssl ] else [ ]); + }; } diff --git a/mail-server/networking.nix b/mail-server/networking.nix index 587a8ae..f560ec0 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -20,21 +20,19 @@ let cfg = config.mailserver; in { - config = - with cfg; - lib.mkIf (enable && openFirewall) { + config = lib.mkIf (cfg.enable && cfg.openFirewall) { - networking.firewall = { - allowedTCPPorts = - [ 25 ] - ++ lib.optional enableSubmission 587 - ++ lib.optional enableSubmissionSsl 465 - ++ lib.optional enableImap 143 - ++ lib.optional enableImapSsl 993 - ++ lib.optional enablePop3 110 - ++ lib.optional enablePop3Ssl 995 - ++ lib.optional enableManageSieve 4190 - ++ lib.optional (certificateScheme == "acme-nginx") 80; - }; + networking.firewall = { + allowedTCPPorts = + [ 25 ] + ++ lib.optional cfg.enableSubmission 587 + ++ lib.optional cfg.enableSubmissionSsl 465 + ++ lib.optional cfg.enableImap 143 + ++ lib.optional cfg.enableImapSsl 993 + ++ lib.optional cfg.enablePop3 110 + ++ lib.optional cfg.enablePop3Ssl 995 + ++ lib.optional cfg.enableManageSieve 4190 + ++ lib.optional (cfg.certificateScheme == "acme-nginx") 80; }; + }; } diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 4237efc..680077d 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -233,183 +233,181 @@ let }; in { - config = - with cfg; - lib.mkIf enable { + config = lib.mkIf cfg.enable { - systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { - preStart = '' - ${appendPwdInVirtualMailboxMap} - ${appendPwdInSenderLoginMap} - ''; - restartTriggers = [ - appendPwdInVirtualMailboxMap - appendPwdInSenderLoginMap + systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { + preStart = '' + ${appendPwdInVirtualMailboxMap} + ${appendPwdInSenderLoginMap} + ''; + restartTriggers = [ + appendPwdInVirtualMailboxMap + appendPwdInSenderLoginMap + ]; + }; + + services.postfix = { + enable = true; + hostname = "${cfg.sendingFqdn}"; + networksStyle = "host"; + mapFiles."valias" = valiases_file; + mapFiles."regex_valias" = regex_valiases_file; + mapFiles."vaccounts" = vaccounts_file; + mapFiles."regex_vaccounts" = regex_vaccounts_file; + mapFiles."denied_recipients" = denied_recipients_file; + mapFiles."reject_senders" = reject_senders_file; + mapFiles."reject_recipients" = reject_recipients_file; + enableSubmission = cfg.enableSubmission; + enableSubmissions = cfg.enableSubmissionSsl; + virtual = lookupTableToString (mergeLookupTables [ + all_valiases_postfix + catchAllPostfix + forwards + ]); + + config = { + smtpd_tls_chain_files = [ + "${keyPath}" + "${certificatePath}" ]; + + # Extra Config + mydestination = ""; + recipient_delimiter = cfg.recipientDelimiter; + smtpd_banner = "${cfg.fqdn} ESMTP NO UCE"; + disable_vrfy_command = true; + message_size_limit = toString cfg.messageSizeLimit; + + # virtual mail system + virtual_uid_maps = "static:5000"; + virtual_gid_maps = "static:5000"; + virtual_mailbox_base = cfg.mailDirectory; + virtual_mailbox_domains = vhosts_file; + virtual_mailbox_maps = + [ + (mappedFile "valias") + ] + ++ lib.optionals cfg.ldap.enable [ + "ldap:${ldapVirtualMailboxMapFile}" + ] + ++ lib.optionals (regex_valiases_postfix != { }) [ + (mappedRegexFile "regex_valias") + ]; + virtual_alias_maps = lib.mkAfter ( + lib.optionals (regex_valiases_postfix != { }) [ + (mappedRegexFile "regex_valias") + ] + ); + 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"; + + # sasl with dovecot + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "/run/dovecot2/auth"; + smtpd_sasl_auth_enable = true; + smtpd_relay_restrictions = [ + "permit_mynetworks" + "permit_sasl_authenticated" + "reject_unauth_destination" + ]; + + # reject selected senders + smtpd_sender_restrictions = [ + "check_sender_access ${mappedFile "reject_senders"}" + ]; + + smtpd_recipient_restrictions = [ + # reject selected recipients + "check_recipient_access ${mappedFile "denied_recipients"}" + "check_recipient_access ${mappedFile "reject_recipients"}" + # quota checking + "check_policy_service unix:/run/dovecot2/quota-status" + ]; + + # TLS for incoming mail is optional + smtpd_tls_security_level = "may"; + + # But required for authentication attempts + smtpd_tls_auth_only = true; + + # TLS versions supported for the SMTP server + smtpd_tls_protocols = ">=TLSv1.2"; + smtpd_tls_mandatory_protocols = ">=TLSv1.2"; + + # Require ciphersuites that OpenSSL classifies as "High" + smtpd_tls_ciphers = "high"; + smtpd_tls_mandatory_ciphers = "high"; + + # Exclude cipher suites with undesirable properties + smtpd_tls_exclude_ciphers = "eNULL, aNULL"; + smtpd_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; + + # Opportunistic DANE support when delivering mail to other servers + # https://www.postfix.org/postconf.5.html#smtp_tls_security_level + smtp_dns_support_level = "dnssec"; + smtp_tls_security_level = "dane"; + + # TLS versions supported for the SMTP client + smtp_tls_protocols = ">=TLSv1.2"; + smtp_tls_mandatory_protocols = ">=TLSv1.2"; + + # Require ciphersuites that OpenSSL classifies as "High" + smtp_tls_ciphers = "high"; + smtp_tls_mandatory_ciphers = "high"; + + # Exclude ciphersuites with undesirable properties + smtp_tls_exclude_ciphers = "eNULL, aNULL"; + smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; + + # Restrict and prioritize the following curves in the given order + # Excludes curves that have no widespread support, so we don't bloat the handshake needlessly. + # https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves + # https://ssl-config.mozilla.org/#server=postfix&version=3.10&config=intermediate&openssl=3.4.1&guideline=5.7 + tls_eecdh_auto_curves = [ + "X25519" + "prime256v1" + "secp384r1" + ]; + + # Disable FFDHE on TLSv1.3 because it is slower than elliptic curves + # https://www.postfix.org/postconf.5.html#tls_ffdhe_auto_groups + tls_ffdhe_auto_groups = [ ]; + + # As long as all cipher suites are considered safe, let the client use its preferred cipher + tls_preempt_cipherlist = false; + + # Log only a summary message on TLS handshake completion + smtp_tls_loglevel = "1"; + smtpd_tls_loglevel = "1"; + + smtpd_milters = smtpdMilters; + non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ]; + milter_protocol = "6"; + milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}"; }; - services.postfix = { - enable = true; - hostname = "${sendingFqdn}"; - networksStyle = "host"; - mapFiles."valias" = valiases_file; - mapFiles."regex_valias" = regex_valiases_file; - mapFiles."vaccounts" = vaccounts_file; - mapFiles."regex_vaccounts" = regex_vaccounts_file; - mapFiles."denied_recipients" = denied_recipients_file; - mapFiles."reject_senders" = reject_senders_file; - mapFiles."reject_recipients" = reject_recipients_file; - enableSubmission = cfg.enableSubmission; - enableSubmissions = cfg.enableSubmissionSsl; - virtual = lookupTableToString (mergeLookupTables [ - all_valiases_postfix - catchAllPostfix - forwards - ]); + submissionOptions = submissionOptions; + submissionsOptions = submissionOptions; - config = { - smtpd_tls_chain_files = [ - "${keyPath}" - "${certificatePath}" - ]; - - # Extra Config - mydestination = ""; - recipient_delimiter = cfg.recipientDelimiter; - smtpd_banner = "${fqdn} ESMTP NO UCE"; - disable_vrfy_command = true; - message_size_limit = toString cfg.messageSizeLimit; - - # virtual mail system - virtual_uid_maps = "static:5000"; - virtual_gid_maps = "static:5000"; - virtual_mailbox_base = mailDirectory; - virtual_mailbox_domains = vhosts_file; - virtual_mailbox_maps = - [ - (mappedFile "valias") - ] - ++ lib.optionals cfg.ldap.enable [ - "ldap:${ldapVirtualMailboxMapFile}" - ] - ++ lib.optionals (regex_valiases_postfix != { }) [ - (mappedRegexFile "regex_valias") - ]; - virtual_alias_maps = lib.mkAfter ( - lib.optionals (regex_valiases_postfix != { }) [ - (mappedRegexFile "regex_valias") - ] - ); - 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"; - - # sasl with dovecot - smtpd_sasl_type = "dovecot"; - smtpd_sasl_path = "/run/dovecot2/auth"; - smtpd_sasl_auth_enable = true; - smtpd_relay_restrictions = [ - "permit_mynetworks" - "permit_sasl_authenticated" - "reject_unauth_destination" - ]; - - # reject selected senders - smtpd_sender_restrictions = [ - "check_sender_access ${mappedFile "reject_senders"}" - ]; - - smtpd_recipient_restrictions = [ - # reject selected recipients - "check_recipient_access ${mappedFile "denied_recipients"}" - "check_recipient_access ${mappedFile "reject_recipients"}" - # quota checking - "check_policy_service unix:/run/dovecot2/quota-status" - ]; - - # TLS for incoming mail is optional - smtpd_tls_security_level = "may"; - - # But required for authentication attempts - smtpd_tls_auth_only = true; - - # TLS versions supported for the SMTP server - smtpd_tls_protocols = ">=TLSv1.2"; - smtpd_tls_mandatory_protocols = ">=TLSv1.2"; - - # Require ciphersuites that OpenSSL classifies as "High" - smtpd_tls_ciphers = "high"; - smtpd_tls_mandatory_ciphers = "high"; - - # Exclude cipher suites with undesirable properties - smtpd_tls_exclude_ciphers = "eNULL, aNULL"; - smtpd_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; - - # Opportunistic DANE support when delivering mail to other servers - # https://www.postfix.org/postconf.5.html#smtp_tls_security_level - smtp_dns_support_level = "dnssec"; - smtp_tls_security_level = "dane"; - - # TLS versions supported for the SMTP client - smtp_tls_protocols = ">=TLSv1.2"; - smtp_tls_mandatory_protocols = ">=TLSv1.2"; - - # Require ciphersuites that OpenSSL classifies as "High" - smtp_tls_ciphers = "high"; - smtp_tls_mandatory_ciphers = "high"; - - # Exclude ciphersuites with undesirable properties - smtp_tls_exclude_ciphers = "eNULL, aNULL"; - smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; - - # Restrict and prioritize the following curves in the given order - # Excludes curves that have no widespread support, so we don't bloat the handshake needlessly. - # https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves - # https://ssl-config.mozilla.org/#server=postfix&version=3.10&config=intermediate&openssl=3.4.1&guideline=5.7 - tls_eecdh_auto_curves = [ - "X25519" - "prime256v1" - "secp384r1" - ]; - - # Disable FFDHE on TLSv1.3 because it is slower than elliptic curves - # https://www.postfix.org/postconf.5.html#tls_ffdhe_auto_groups - tls_ffdhe_auto_groups = [ ]; - - # As long as all cipher suites are considered safe, let the client use its preferred cipher - tls_preempt_cipherlist = false; - - # Log only a summary message on TLS handshake completion - smtp_tls_loglevel = "1"; - smtpd_tls_loglevel = "1"; - - smtpd_milters = smtpdMilters; - non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ]; - milter_protocol = "6"; - milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}"; + masterConfig = { + "lmtp" = { + # Add headers when delivering, see http://www.postfix.org/smtp.8.html + # D => Delivered-To, O => X-Original-To, R => Return-Path + args = [ "flags=O" ]; }; - - submissionOptions = submissionOptions; - submissionsOptions = submissionOptions; - - masterConfig = { - "lmtp" = { - # Add headers when delivering, see http://www.postfix.org/smtp.8.html - # D => Delivered-To, O => X-Original-To, R => Return-Path - args = [ "flags=O" ]; - }; - "submission-header-cleanup" = { - type = "unix"; - private = false; - chroot = false; - maxproc = 0; - command = "cleanup"; - args = [ - "-o" - "header_checks=pcre:${submissionHeaderCleanupRules}" - ]; - }; + "submission-header-cleanup" = { + type = "unix"; + private = false; + chroot = false; + maxproc = 0; + command = "cleanup"; + args = [ + "-o" + "header_checks=pcre:${submissionHeaderCleanupRules}" + ]; }; }; }; + }; } diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index a4fcdce..7ed2a0e 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -52,223 +52,221 @@ let ''; in { - config = - with cfg; - lib.mkIf enable { - environment.systemPackages = lib.mkBefore [ - (pkgs.runCommand "rspamc-wrapped" - { - nativeBuildInputs = with pkgs; [ makeWrapper ]; - } - '' - makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \ - --add-flags "-h /run/rspamd/worker-controller.sock" - '' - ) - ]; + config = lib.mkIf cfg.enable { + environment.systemPackages = lib.mkBefore [ + (pkgs.runCommand "rspamc-wrapped" + { + nativeBuildInputs = with pkgs; [ makeWrapper ]; + } + '' + makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \ + --add-flags "-h /run/rspamd/worker-controller.sock" + '' + ) + ]; - services.rspamd = { - enable = true; - inherit debug; - locals = { - "milter_headers.conf" = { - text = '' - extended_spam_headers = true; - ''; - }; - "redis.conf" = { - text = - '' - servers = "${ - if cfg.redis.port == null then - cfg.redis.address - else - "${cfg.redis.address}:${toString cfg.redis.port}" - }"; - '' - + (lib.optionalString (cfg.redis.password != null) '' - password = "${cfg.redis.password}"; - ''); - }; - "classifier-bayes.conf" = { - text = '' - cache { - backend = "redis"; - } - ''; - }; - "antivirus.conf" = lib.mkIf cfg.virusScanning { - text = '' - clamav { - action = "reject"; - symbol = "CLAM_VIRUS"; - type = "clamav"; - log_clean = true; - servers = "/run/clamav/clamd.ctl"; - scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all - } - ''; - }; - "dkim_signing.conf" = { - text = '' - enabled = ${lib.boolToString cfg.dkimSigning}; - path = "${cfg.dkimKeyDirectory}/$domain.$selector.key"; - selector = "${cfg.dkimSelector}"; - # Allow for usernames w/o domain part - allow_username_mismatch = true - ''; - }; - "dmarc.conf" = { - text = '' - ${lib.optionalString cfg.dmarcReporting.enable '' - reporting { - enabled = true; - email = "${cfg.dmarcReporting.email}"; - domain = "${cfg.dmarcReporting.domain}"; - org_name = "${cfg.dmarcReporting.organizationName}"; - from_name = "${cfg.dmarcReporting.fromName}"; - msgid_from = "${cfg.dmarcReporting.domain}"; - ${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) '' - exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains}; - ''} - }''} - ''; - }; + services.rspamd = { + enable = true; + inherit (cfg) debug; + locals = { + "milter_headers.conf" = { + text = '' + extended_spam_headers = true; + ''; }; - - workers.rspamd_proxy = { - type = "rspamd_proxy"; - bindSockets = [ - { - socket = "/run/rspamd/rspamd-milter.sock"; - mode = "0664"; - } - ]; - count = 1; # Do not spawn too many processes of this type - extraConfig = '' - milter = yes; # Enable milter mode - timeout = 120s; # Needed for Milter usually - - upstream "local" { - default = yes; # Self-scan upstreams are always default - self_scan = yes; # Enable self-scan + "redis.conf" = { + text = + '' + servers = "${ + if cfg.redis.port == null then + cfg.redis.address + else + "${cfg.redis.address}:${toString cfg.redis.port}" + }"; + '' + + (lib.optionalString (cfg.redis.password != null) '' + password = "${cfg.redis.password}"; + ''); + }; + "classifier-bayes.conf" = { + text = '' + cache { + backend = "redis"; } ''; }; - workers.controller = { - type = "controller"; - count = 1; - bindSockets = [ - { - socket = "/run/rspamd/worker-controller.sock"; - mode = "0666"; + "antivirus.conf" = lib.mkIf cfg.virusScanning { + text = '' + clamav { + action = "reject"; + symbol = "CLAM_VIRUS"; + type = "clamav"; + log_clean = true; + servers = "/run/clamav/clamd.ctl"; + scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all } - ]; - includes = [ ]; - extraConfig = '' - static_dir = "''${WWWDIR}"; # Serve the web UI static assets ''; }; - - }; - - services.redis.servers.rspamd.enable = lib.mkDefault true; - - systemd.tmpfiles.settings."10-rspamd.conf" = { - "${cfg.dkimKeyDirectory}" = { - d = { - # Create /var/dkim owned by rspamd user/group - user = rspamdUser; - group = rspamdGroup; - }; - Z = { - # Recursively adjust permissions in /var/dkim - user = rspamdUser; - group = rspamdGroup; - }; + "dkim_signing.conf" = { + text = '' + enabled = ${lib.boolToString cfg.dkimSigning}; + path = "${cfg.dkimKeyDirectory}/$domain.$selector.key"; + selector = "${cfg.dkimSelector}"; + # Allow for usernames w/o domain part + allow_username_mismatch = true + ''; + }; + "dmarc.conf" = { + text = '' + ${lib.optionalString cfg.dmarcReporting.enable '' + reporting { + enabled = true; + email = "${cfg.dmarcReporting.email}"; + domain = "${cfg.dmarcReporting.domain}"; + org_name = "${cfg.dmarcReporting.organizationName}"; + from_name = "${cfg.dmarcReporting.fromName}"; + msgid_from = "${cfg.dmarcReporting.domain}"; + ${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) '' + exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains}; + ''} + }''} + ''; }; }; - systemd.services.rspamd = { - requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - serviceConfig = lib.mkMerge [ + workers.rspamd_proxy = { + type = "rspamd_proxy"; + bindSockets = [ { - SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; + socket = "/run/rspamd/rspamd-milter.sock"; + mode = "0664"; } - (lib.optionalAttrs cfg.dkimSigning { - ExecStartPre = map createDkimKeypair cfg.domains; - ReadWritePaths = [ cfg.dkimKeyDirectory ]; - }) ]; - }; + count = 1; # Do not spawn too many processes of this type + extraConfig = '' + milter = yes; # Enable milter mode + timeout = 120s; # Needed for Milter usually - systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { - # Explicitly select yesterday's date to work around broken - # default behaviour when called without a date. - # https://github.com/rspamd/rspamd/issues/4062 - script = '' - ${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d") + upstream "local" { + default = yes; # Self-scan upstreams are always default + self_scan = yes; # Enable self-scan + } ''; - serviceConfig = { - User = "${config.services.rspamd.user}"; - Group = "${config.services.rspamd.group}"; - - AmbientCapabilities = [ ]; - CapabilityBoundingSet = ""; - DevicePolicy = "closed"; - IPAddressAllow = "localhost"; - LockPersonality = true; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateMounts = true; - PrivateTmp = true; - PrivateUsers = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - ProcSubset = "pid"; - ProtectSystem = "strict"; - RemoveIPC = true; - RestrictAddressFamilies = [ - "AF_INET" - "AF_INET6" - ]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ - "@system-service" - "~@privileged" - ]; - UMask = "0077"; - }; }; - - systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { - description = "Daily delivery of aggregated DMARC reports"; - wantedBy = [ - "timers.target" + workers.controller = { + type = "controller"; + count = 1; + bindSockets = [ + { + socket = "/run/rspamd/worker-controller.sock"; + mode = "0666"; + } ]; - timerConfig = { - OnCalendar = "daily"; - Persistent = true; - RandomizedDelaySec = 86400; - FixedRandomDelay = true; + includes = [ ]; + extraConfig = '' + static_dir = "''${WWWDIR}"; # Serve the web UI static assets + ''; + }; + + }; + + services.redis.servers.rspamd.enable = lib.mkDefault true; + + systemd.tmpfiles.settings."10-rspamd.conf" = { + "${cfg.dkimKeyDirectory}" = { + d = { + # Create /var/dkim owned by rspamd user/group + user = rspamdUser; + group = rspamdGroup; + }; + Z = { + # Recursively adjust permissions in /var/dkim + user = rspamdUser; + group = rspamdGroup; }; }; - - systemd.services.postfix = { - after = [ rspamdSocket ]; - requires = [ rspamdSocket ]; - }; - - users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ]; }; + + systemd.services.rspamd = { + requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); + after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); + serviceConfig = lib.mkMerge [ + { + SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; + } + (lib.optionalAttrs cfg.dkimSigning { + ExecStartPre = map createDkimKeypair cfg.domains; + ReadWritePaths = [ cfg.dkimKeyDirectory ]; + }) + ]; + }; + + systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { + # Explicitly select yesterday's date to work around broken + # default behaviour when called without a date. + # https://github.com/rspamd/rspamd/issues/4062 + script = '' + ${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d") + ''; + serviceConfig = { + User = "${config.services.rspamd.user}"; + Group = "${config.services.rspamd.group}"; + + AmbientCapabilities = [ ]; + CapabilityBoundingSet = ""; + DevicePolicy = "closed"; + IPAddressAllow = "localhost"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + }; + + systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { + description = "Daily delivery of aggregated DMARC reports"; + wantedBy = [ + "timers.target" + ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + RandomizedDelaySec = 86400; + FixedRandomDelay = true; + }; + }; + + systemd.services.postfix = { + after = [ rspamdSocket ]; + requires = [ rspamdSocket ]; + }; + + users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ]; + }; } diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index dd4bd63..8fb0da7 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -32,61 +32,59 @@ let [ "acme-finished-${cfg.fqdn}.target" ]; in { - config = - with cfg; - lib.mkIf enable { - # Create self signed certificate - systemd.services.mailserver-selfsigned-certificate = - lib.mkIf (cfg.certificateScheme == "selfsigned") - { - after = [ "local-fs.target" ]; - script = '' - # Create certificates if they do not exist yet - dir="${cfg.certificateDirectory}" - fqdn="${cfg.fqdn}" - [[ $fqdn == /* ]] && fqdn=$(< "$fqdn") - key="$dir/key-${cfg.fqdn}.pem"; - cert="$dir/cert-${cfg.fqdn}.pem"; + config = lib.mkIf cfg.enable { + # Create self signed certificate + systemd.services.mailserver-selfsigned-certificate = + lib.mkIf (cfg.certificateScheme == "selfsigned") + { + after = [ "local-fs.target" ]; + script = '' + # Create certificates if they do not exist yet + dir="${cfg.certificateDirectory}" + fqdn="${cfg.fqdn}" + [[ $fqdn == /* ]] && fqdn=$(< "$fqdn") + key="$dir/key-${cfg.fqdn}.pem"; + cert="$dir/cert-${cfg.fqdn}.pem"; - if [[ ! -f $key || ! -f $cert ]]; then - mkdir -p "${cfg.certificateDirectory}" - (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) && - "${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \ - -days 3650 -out "$cert" - fi - ''; - serviceConfig = { - Type = "oneshot"; - PrivateTmp = true; - }; - }; - - # Create maildir folder before dovecot startup - systemd.services.dovecot2 = { - wants = certificatesDeps; - after = certificatesDeps; - preStart = - let - directories = lib.strings.escapeShellArgs ( - [ mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir - ); - in - '' - # Create mail directory and set permissions. See - # . - # Prevent world-readable paths, even temporarily. - umask 007 - mkdir -p ${directories} - chgrp "${vmailGroupName}" ${directories} - chmod 02770 ${directories} + if [[ ! -f $key || ! -f $cert ]]; then + mkdir -p "${cfg.certificateDirectory}" + (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) && + "${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \ + -days 3650 -out "$cert" + fi ''; - }; + serviceConfig = { + Type = "oneshot"; + PrivateTmp = true; + }; + }; - # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work - systemd.services.postfix = { - wants = certificatesDeps; - after = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service" ++ certificatesDeps; - requires = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service"; - }; + # Create maildir folder before dovecot startup + systemd.services.dovecot2 = { + wants = certificatesDeps; + after = certificatesDeps; + preStart = + let + directories = lib.strings.escapeShellArgs ( + [ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir + ); + in + '' + # Create mail directory and set permissions. See + # . + # Prevent world-readable paths, even temporarily. + umask 007 + mkdir -p ${directories} + chgrp "${cfg.vmailGroupName}" ${directories} + chmod 02770 ${directories} + ''; }; + + # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work + systemd.services.postfix = { + wants = certificatesDeps; + after = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service" ++ certificatesDeps; + requires = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service"; + }; + }; }