From 1a64deeb894dc95e2645a75771732c6cc53a79ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Isma=C3=ABl=20Bouya?= Date: Wed, 4 Oct 2023 01:35:06 +0200 Subject: Squash changes containing private information There were a lot of changes since the previous commit, but a lot of them contained personnal information about users. All thos changes got stashed into a single commit (history is kept in a different place) and private information was moved in a separate private repository --- systems/eldiron/mail/postfix.nix | 497 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 497 insertions(+) create mode 100644 systems/eldiron/mail/postfix.nix (limited to 'systems/eldiron/mail/postfix.nix') diff --git a/systems/eldiron/mail/postfix.nix b/systems/eldiron/mail/postfix.nix new file mode 100644 index 0000000..f95ee1b --- /dev/null +++ b/systems/eldiron/mail/postfix.nix @@ -0,0 +1,497 @@ +{ lib, pkgs, config, options, ... }: +let + getDomains = p: lib.mapAttrsToList (n: v: v.fqdn) (lib.filterAttrs (n: v: v.receive) p.emailPolicies); + bydomain = builtins.mapAttrs (n: getDomains) config.myServices.dns.zones; + receiving_domains = lib.flatten (builtins.attrValues bydomain); +in +{ + options.services.postfix.submissionOptions' = options.services.postfix.submissionOptions // { + type = with lib.types; attrsOf (either str (listOf str)); + apply = builtins.mapAttrs (n: v: if builtins.isList v then builtins.concatStringsSep "," v else v); + }; + config = lib.mkIf config.myServices.mail.enable { + myServices.dns.zones."immae.eu" = with config.myServices.dns.helpers; lib.mkMerge [ + mailMX + (mailCommon "immae.eu") + mailSend + { + # Virtual forwards and mailboxes for real users + emailPolicies."mail".receive = true; + # multi-domain generic mails: + # hostmaster, cron, httpd, naemon, postmaster + # system virtual mailboxes: + # devnull, printer, testconnect + emailPolicies."".receive = true; + subdomains.mail = lib.mkMerge [ (mailCommon "immae.eu") mailSend ]; + subdomains.smtp = ips servers.eldiron.ips.main; + + # DMARC reports + subdomains._dmarc.subdomains._report.subdomains = let + getDomains = p: lib.mapAttrsToList (n: v: v.fqdn) p.emailPolicies; + bydomain = builtins.mapAttrs (n: getDomains) config.myServices.dns.zones; + hostsWithMail = lib.flatten (builtins.attrValues bydomain); + nvpairs = builtins.map (e: { name = e; value = { TXT = [ "v=DMARC1;" ]; }; }) hostsWithMail; + in + builtins.listToAttrs nvpairs; + } + ]; + + myServices.chatonsProperties.hostings.mx-backup = { + file.datetime = "2022-08-22T01:00:00"; + hosting = { + name = "MX Backup"; + description = "Serveur e-mail secondaire"; + logo = "https://www.postfix.org/favicon.ico"; + website = "https://mail.immae.eu/"; + status.level = "OK"; + status.description = "OK"; + registration.load = "OPEN"; + install.type = "PACKAGE"; + }; + software = { + name = "Postfix"; + website = "http://www.postfix.org/"; + license.url = "http://postfix.mirrors.ovh.net/postfix-release/LICENSE"; + license.name = "Eclipse Public license (EPL 2.0) and IBM Public License (IPL 1.0)"; + version = pkgs.postfix.version; + source.url = "http://www.postfix.org/download.html"; + }; + }; + secrets.keys = { + "postfix/mysql_alias_maps" = { + user = config.services.postfix.user; + group = config.services.postfix.group; + permissions = "0440"; + text = '' + # We need to specify that option to trigger ssl connection + tls_ciphers = TLSv1.2 + user = ${config.myEnv.mail.postfix.mysql.user} + password = ${config.myEnv.mail.postfix.mysql.password} + hosts = unix:${config.myEnv.mail.postfix.mysql.socket} + dbname = ${config.myEnv.mail.postfix.mysql.database} + query = SELECT DISTINCT destination + FROM forwardings + WHERE + ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s')) + AND active = 1 + AND '%s' NOT IN + ( + SELECT source + FROM forwardings_blacklisted + WHERE source = '%s' + ) UNION + SELECT 'devnull@immae.eu' + FROM forwardings_blacklisted + WHERE source = '%s' + ''; + }; + "postfix/ldap_mailboxes" = { + user = config.services.postfix.user; + group = config.services.postfix.group; + permissions = "0440"; + text = '' + server_host = ldaps://${config.myEnv.mail.dovecot.ldap.host}:636 + search_base = ${config.myEnv.mail.dovecot.ldap.base} + query_filter = ${config.myEnv.mail.dovecot.ldap.postfix_mailbox_filter} + bind_dn = ${config.myEnv.mail.dovecot.ldap.dn} + bind_pw = ${config.myEnv.mail.dovecot.ldap.password} + result_attribute = immaePostfixAddress + result_format = dummy + version = 3 + ''; + }; + "postfix/mysql_sender_login_maps" = { + user = config.services.postfix.user; + group = config.services.postfix.group; + permissions = "0440"; + text = '' + # We need to specify that option to trigger ssl connection + tls_ciphers = TLSv1.2 + user = ${config.myEnv.mail.postfix.mysql.user} + password = ${config.myEnv.mail.postfix.mysql.password} + hosts = unix:${config.myEnv.mail.postfix.mysql.socket} + dbname = ${config.myEnv.mail.postfix.mysql.database} + query = SELECT DISTINCT destination + FROM forwardings + WHERE + ( + (regex = 1 AND CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') REGEXP CONCAT('^',source,'$') ) + OR + (regex = 0 AND source = CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d')) + ) + AND active = 1 + UNION SELECT CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') AS destination + ''; + }; + "postfix/mysql_sender_relays_maps" = { + user = config.services.postfix.user; + group = config.services.postfix.group; + permissions = "0440"; + text = '' + # We need to specify that option to trigger ssl connection + tls_ciphers = TLSv1.2 + user = ${config.myEnv.mail.postfix.mysql.user} + password = ${config.myEnv.mail.postfix.mysql.password} + hosts = unix:${config.myEnv.mail.postfix.mysql.socket} + dbname = ${config.myEnv.mail.postfix.mysql.database} + # INSERT INTO sender_relays + # (`from`, owner, relay, login, password, regex, active) + # VALUES + # ( 'sender@otherhost.org' + # , 'me@mail.immae.eu' + # , '[otherhost.org]:587' + # , 'otherhostlogin' + # , AES_ENCRYPT('otherhostpassword', '${config.myEnv.mail.postfix.mysql.password_encrypt}') + # , '0' + # , '1'); + + query = SELECT DISTINCT `owner` + FROM sender_relays + WHERE + ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) + AND active = 1 + ''; + }; + "postfix/mysql_sender_relays_hosts" = { + user = config.services.postfix.user; + group = config.services.postfix.group; + permissions = "0440"; + text = '' + # We need to specify that option to trigger ssl connection + tls_ciphers = TLSv1.2 + user = ${config.myEnv.mail.postfix.mysql.user} + password = ${config.myEnv.mail.postfix.mysql.password} + hosts = unix:${config.myEnv.mail.postfix.mysql.socket} + dbname = ${config.myEnv.mail.postfix.mysql.database} + + query = SELECT DISTINCT relay + FROM sender_relays + WHERE + ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) + AND active = 1 + ''; + }; + "postfix/mysql_sender_relays_creds" = { + user = config.services.postfix.user; + group = config.services.postfix.group; + permissions = "0440"; + text = '' + # We need to specify that option to trigger ssl connection + tls_ciphers = TLSv1.2 + user = ${config.myEnv.mail.postfix.mysql.user} + password = ${config.myEnv.mail.postfix.mysql.password} + hosts = unix:${config.myEnv.mail.postfix.mysql.socket} + dbname = ${config.myEnv.mail.postfix.mysql.database} + + query = SELECT DISTINCT CONCAT(`login`, ':', AES_DECRYPT(`password`, '${config.myEnv.mail.postfix.mysql.password_encrypt}')) + FROM sender_relays + WHERE + ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) + AND active = 1 + ''; + }; + "postfix/ldap_ejabberd_users_immae_fr" = { + user = config.services.postfix.user; + group = config.services.postfix.group; + permissions = "0440"; + text = '' + server_host = ldaps://${config.myEnv.jabber.ldap.host}:636 + search_base = ${config.myEnv.jabber.ldap.base} + query_filter = ${config.myEnv.jabber.postfix_user_filter} + domain = immae.fr + bind_dn = ${config.myEnv.jabber.ldap.dn} + bind_pw = ${config.myEnv.jabber.ldap.password} + result_attribute = immaeXmppUid + result_format = ejabberd@localhost + version = 3 + ''; + }; + }; + + networking.firewall.allowedTCPPorts = [ 25 465 587 ]; + + users.users.postfixscripts = { + group = "keys"; + uid = config.ids.uids.postfixscripts; + description = "Postfix scripts user"; + }; + users.users."${config.services.postfix.user}".extraGroups = [ "keys" ]; + services.filesWatcher.postfix = { + restart = true; + paths = [ + config.secrets.fullPaths."postfix/mysql_alias_maps" + config.secrets.fullPaths."postfix/ldap_mailboxes" + config.secrets.fullPaths."postfix/mysql_sender_login_maps" + config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr" + ]; + }; + services.postfix = { + extraAliases = let + testmail = pkgs.writeScript "testmail" '' + #! ${pkgs.stdenv.shell} + ${pkgs.coreutils}/bin/touch \ + "/var/lib/naemon/checks/email/$(${pkgs.procmail}/bin/formail -x To: | ${pkgs.coreutils}/bin/tr -d ' <>')" + ''; + in + ''testmail: "|${testmail}"''; + mapFiles = let + virtual_map = { + virtual = let + cfg = config.myEnv.monitoring.email_check.eldiron; + address = "${cfg.mail_address}@${cfg.mail_domain}"; + aliases = config.myEnv.mail.postfix.common_aliases; + admins = builtins.concatStringsSep "," config.myEnv.mail.postfix.admins; + in pkgs.writeText "postfix-virtual" ( + builtins.concatStringsSep "\n" ( + [ "${address} testmail@localhost" + ] ++ + map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases + ++ lib.lists.flatten ( + map (domain: + map (alias: "${alias}@${domain} ${admins}") aliases + ) receiving_domains + ) + )); + }; + sasl_access = { + host_sender_login = with lib.attrsets; let + addresses = zipAttrs (lib.flatten (mapAttrsToList + (n: v: (map (e: { "${e}" = "${n}@immae.eu"; }) v.emails)) config.myEnv.servers)); + aliases = config.myEnv.mail.postfix.common_aliases; + joined = builtins.concatStringsSep ","; + admins = joined config.myEnv.mail.postfix.admins; + in pkgs.writeText "host-sender-login" + (builtins.concatStringsSep "\n" ( + mapAttrsToList (n: v: "${n} ${joined v}") addresses + ++ lib.lists.flatten ( + map (domain: + map (alias: "${alias}@${domain} ${admins}") aliases + ) receiving_domains + ) + ++ map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases + )); + }; + in + virtual_map // sasl_access; + config = { + ### postfix module overrides + readme_directory = "${pkgs.postfix}/share/postfix/doc"; + smtp_tls_CAfile = lib.mkForce ""; + smtp_tls_cert_file = lib.mkForce ""; + smtp_tls_key_file = lib.mkForce ""; + + message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited" + mailbox_size_limit = "1073741825"; # Workaround, local delivered mails should all go through scripts + alias_database = "\$alias_maps"; + + ### Aliases scripts user + default_privs = "postfixscripts"; + + ### Virtual mailboxes config + virtual_alias_maps = [ + "hash:/etc/postfix/virtual" + "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}" + "ldap:${config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"}" + ]; + virtual_mailbox_domains = receiving_domains; + virtual_mailbox_maps = [ + "ldap:${config.secrets.fullPaths."postfix/ldap_mailboxes"}" + ]; + dovecot_destination_recipient_limit = "1"; + virtual_transport = "dovecot"; + + ### Relay domains + smtpd_relay_restrictions = [ + "defer_unauth_destination" + ]; + + ### Additional smtpd configuration + smtpd_tls_received_header = "yes"; + smtpd_tls_loglevel = "1"; + + ### Email sending configuration + smtp_tls_security_level = "may"; + smtp_tls_loglevel = "1"; + + ### Force ip bind for smtp + smtp_bind_address = builtins.head config.hostEnv.ips.main.ip4; + smtp_bind_address6 = builtins.head config.hostEnv.ips.main.ip6; + + # Use some relays when authorized senders are not myself + smtp_sasl_mechanism_filter = [ + "plain" + "login" + ]; # GSSAPI Not correctly supported by postfix + smtp_sasl_auth_enable = "yes"; + smtp_sasl_password_maps = [ + "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_creds"}" + ]; + smtp_sasl_security_options = "noanonymous"; + smtp_sender_dependent_authentication = "yes"; + sender_dependent_relayhost_maps = [ + "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_hosts"}" + ]; + + ### opendkim, opendmarc, openarc milters + non_smtpd_milters = [ + "unix:${config.myServices.mail.milters.sockets.opendkim}" + ]; + smtpd_milters = [ + "unix:${config.myServices.mail.milters.sockets.opendkim}" + "unix:${config.myServices.mail.milters.sockets.openarc}" + "unix:${config.myServices.mail.milters.sockets.opendmarc}" + ]; + + smtp_use_tls = true; + smtpd_use_tls = true; + smtpd_tls_chain_files = [ + "/var/lib/acme/mail/full.pem" + "/var/lib/acme/mail-rsa/full.pem" + ]; + + maximal_queue_lifetime = "6w"; + bounce_queue_lifetime = "6w"; + }; + enable = true; + enableSmtp = true; + enableSubmission = true; + submissionOptions = config.services.postfix.submissionOptions'; + submissionOptions' = { + # Don’t use "long form", only commas (cf + # http://www.postfix.org/master.5.html long form is not handled + # well by the submission function) + smtpd_tls_security_level = "encrypt"; + smtpd_sasl_auth_enable = "yes"; + smtpd_tls_auth_only = "yes"; + smtpd_sasl_tls_security_options = "noanonymous"; + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "private/auth"; + smtpd_reject_unlisted_recipient = "no"; + smtpd_client_restrictions = [ + "permit_sasl_authenticated" + "reject" + ]; + smtpd_relay_restrictions = [ + "permit_sasl_authenticated" + "reject" + ]; + # Refuse to send e-mails with a From that is not handled + smtpd_sender_restrictions = [ + "reject_sender_login_mismatch" + "reject_unlisted_sender" + "permit_sasl_authenticated,reject" + ]; + smtpd_sender_login_maps = [ + "hash:/etc/postfix/host_sender_login" + "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_maps"}" + "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}" + ]; + smtpd_recipient_restrictions = [ + "permit_sasl_authenticated" + "reject" + ]; + milter_macro_daemon_name = "ORIGINATING"; + smtpd_milters = [ + # FIXME: put it back when opensmtpd is upgraded and able to + # rewrite the from header + #"unix:/run/milter_verify_from/verify_from.sock" + "unix:${config.myServices.mail.milters.sockets.opendkim}" + ]; + }; + destination = ["localhost"]; + # This needs to reverse DNS + hostname = config.hostEnv.fqdn; + setSendmail = true; + recipientDelimiter = "+"; + masterConfig = { + submissions = { + type = "inet"; + private = false; + command = "smtpd"; + args = ["-o" "smtpd_tls_wrappermode=yes" ] ++ (let + mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ]; + in lib.concatLists (lib.mapAttrsToList mkKeyVal config.services.postfix.submissionOptions) + ); + }; + dovecot = { + type = "unix"; + privileged = true; + chroot = false; + command = "pipe"; + args = let + # rspamd could be used as a milter, but then it cannot apply + # its checks "per user" (milter is not yet dispatched to + # users), so we wrap dovecot-lda inside rspamc per recipient + # here. + rspamc_dovecot = pkgs.writeScriptBin "rspamc_dovecot" '' + #! ${pkgs.stdenv.shell} + set -o pipefail + sender="$1" + original_recipient="$2" + user="$3" + + ${pkgs.coreutils}/bin/cat - | \ + ${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d "$user" --mime | \ + ${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$sender" -a "$original_recipient" -d "$user" + if echo ''${PIPESTATUS[@]} | ${pkgs.gnugrep}/bin/grep -qE '^[0 ]+$'; then + exit 0 + else + # src/global/sys_exits.h to retry + exit 75 + fi + ''; + in [ + "flags=ODRhu" "user=vhost:vhost" + "argv=${rspamc_dovecot}/bin/rspamc_dovecot \${sender} \${original_recipient} \${user}@\${nexthop}" + ]; + }; + }; + }; + security.acme.certs."mail" = { + postRun = '' + systemctl restart postfix.service + ''; + extraDomainNames = [ "smtp.immae.eu" ]; + }; + security.acme.certs."mail-rsa" = { + postRun = '' + systemctl restart postfix.service + ''; + extraDomainNames = [ "smtp.immae.eu" ]; + }; + system.activationScripts.testmail = { + deps = [ "users" ]; + text = let + allCfg = config.myEnv.monitoring.email_check; + cfg = allCfg.eldiron; + reverseTargets = builtins.attrNames (lib.attrsets.filterAttrs (k: v: builtins.elem "eldiron" v.targets) allCfg); + to_email = cfg': host': + let sep = if lib.strings.hasInfix "+" cfg'.mail_address then "_" else "+"; + in "${cfg'.mail_address}${sep}${host'}@${cfg'.mail_domain}"; + mails_to_receive = builtins.concatStringsSep " " (map (to_email cfg) reverseTargets); + in '' + install -m 0555 -o postfixscripts -g keys -d /var/lib/naemon/checks/email + for f in ${mails_to_receive}; do + if [ ! -f /var/lib/naemon/checks/email/$f ]; then + install -m 0644 -o postfixscripts -g keys /dev/null -T /var/lib/naemon/checks/email/$f + touch -m -d @0 /var/lib/naemon/checks/email/$f + fi + done + ''; + }; + systemd.services.postfix.serviceConfig.Slice = "mail.slice"; + + myServices.monitoring.fromMasterObjects.service = [ + { + service_description = "postfix SSL is up to date"; + host_name = config.hostEnv.fqdn; + use = "external-service"; + check_command = "check_smtp"; + + servicegroups = "webstatus-ssl"; + _webstatus_name = "SMTP"; + _webstatus_url = "smtp.immae.eu"; + } + ]; + }; +} -- cgit v1.2.3