{ config, lib, pkgs, ... }: with lib; let cfg = config.local.mta; inherit (config.local) domains virtual users; inherit (config.networking) domain; isBackup = cfg.mode == "backup"; isPrimary = cfg.mode == "primary"; allDomains = optional (! virtualDomains ? ${domain}) domain ++ attrNames virtualDomains; virtualDomains = filterAttrs (name: _: name != domain) virtual; cert = config.security.acme.certs.${mtaDomain.main}.directory; mtaDomain = if isPrimary then domains.smtp else domains.smtp-backup; mdaTransport = if isPrimary then "lmtp:inet:${cfg.mdaAddr}:${toString cfg.lmtpPort}" else "error:bad transport"; in { options.local.mta = { enable = mkEnableOption "mail transfer agent"; mode = mkOption { type = types.enum [ "primary" "backup" ]; }; mdaAddr = mkOption { type = types.str; }; saslPort = mkOption { type = types.port; }; lmtpPort = mkOption { type = types.port; }; relayListen = mkOption { type = types.str; }; }; config = mkIf cfg.enable { services = { fail2ban.jails.postfix.settings = { filter = "postfix[mode=aggressive]"; }; opendkim = mkIf isPrimary { enable = true; group = "postfix"; domains = "csl:" + concatStringsSep "," ([ domain ] ++ attrNames virtualDomains); selector = "202408"; configFile = pkgs.writeText "opendkim.conf" '' UMask 007 InternalHosts 0.0.0.0/0,::/0 ''; }; postfix = { enable = true; enableSmtp = true; enableSubmissions = isPrimary; inherit domain; hostname = mtaDomain.main; #TODO: check_recipient_access para rechazar localhost desde afuera destination = optionals isPrimary [ "localhost" "$mydomain" ]; origin = "$mydomain"; networksStyle = "host"; relayHost = optionalString isBackup domains.smtp.main; lookupMX = false; relayDomains = if isBackup then allDomains else null; sslKey = "${cert}/key.pem"; sslCert = "${cert}/fullchain.pem"; # También es postmaster rootAlias = config.local.sysadmin; extraAliases = optionalString isPrimary (concatLines (flatten (mapAttrsToList (name: user: map (alias: "${alias}: ${name}") user.hardAliases) users))); localRecipients = optionals isPrimary (map (user: "${user}@${domain}") (attrNames (users // virtual.${domain}.users))); virtual = optionalString isPrimary (concatLines (flatten (mapAttrsToList (name: virtual: mapAttrsToList (alias: targets: "${alias}@${name} ${concatStringsSep ", " targets}") virtual.aliases) virtual))); mapFiles = optionalAttrs isPrimary { sender_ccerts = pkgs.writeText "postfix-sender_ccerts" (concatLines (flatten (mapAttrsToList (username: user: map (alias: "${alias}@${domain} CCERTS ${concatStringsSep "," (map (certPath: config.local.pki.byPath.${certPath}.fingerprint.sha256) user.mail.certs)}") ([ username ] ++ user.hardAliases)) (filterAttrs (_: user: user.mail.certs != [ ]) users)))); sender_login = pkgs.writeText "postfix-sender_login" (concatLines (flatten (mapAttrsToList (username: user: map (alias: "${alias}@${domain} ${username}") ([ username ] ++ user.hardAliases)) users))); virtual_recipients = pkgs.writeText "postfix-virtual_recipients" (concatLines (flatten (mapAttrsToList (virtualDomain: virtual: mapAttrsToList # El lado derecho de esta tabla debe existir pero nunca se usa (username: _: "${username}@${virtualDomain} foo") virtual.users) virtualDomains))); virtual_rules = pkgs.writeText "postfix-virtual_rules" (concatLines (flatten (mapAttrsToList (name: virtual: map (rule: "/^${rule.pattern}@${name}$/ ${concatStringsSep ", " rule.targets}") virtual.rules) virtual))); }; config = { # user+extension@domain.tld recipient_delimiter = optionalString isPrimary "+"; message_size_limit = toString (50 * 1048576); local_transport = mdaTransport; virtual_transport = mdaTransport; smtpd_tls_auth_only = true; # Nota: smtpd_tls_dh1024_param_file fue deprecado en 3.9 tls_append_default_CA = false; # Crítico # https://linux-audit.com/postfix-hardening-guide-for-security-and-privacy/ smtpd_helo_required = true; disable_vrfy_command = true; } // optionalAttrs isPrimary { virtual_alias_maps = mkAfter [ "pcre:/etc/postfix/virtual_rules" ]; virtual_mailbox_domains = attrNames virtualDomains; virtual_mailbox_maps = [ "hash:/etc/postfix/virtual_recipients" ]; smtpd_sasl_type = "dovecot"; smtpd_sasl_path = "inet:${cfg.mdaAddr}:${toString cfg.saslPort}"; smtpd_sasl_local_domain = "$mydomain"; smtpd_sasl_security_options = [ "noanonymous" ]; smtpd_tls_CAfile = "${config.local.pki.ca.mail.fullchain}"; smtpd_tls_ccert_verifydepth = "1"; # Inventado, no es parámetro de postfix local_submission_client_restrictions = [ "permit_tls_all_clientcerts" "permit_sasl_authenticated" "reject" ]; smtpd_sender_login_maps = [ "hash:/etc/postfix/sender_login" ]; smtpd_relay_restrictions = [ "permit_mynetworks" "permit_tls_all_clientcerts" "permit_sasl_authenticated" "reject_unauth_destination" ]; smtpd_sender_restrictions = [ "check_sender_access hash:/etc/postfix/sender_ccerts" "reject_sender_login_mismatch" ]; smtpd_milters = "unix:/run/opendkim/opendkim.sock"; non_smtpd_milters = "$smtpd_milters"; milter_default_action = "accept"; } // optionalAttrs isBackup { inet_interfaces = [ cfg.relayListen ]; smtpd_relay_restrictions = [ "reject_unauth_destination" ]; }; # Importante: existe submissionOptions por aparte, no son iguales submissionsOptions = optionalAttrs isPrimary { smtpd_client_restrictions = "$local_submission_client_restrictions"; smtpd_sasl_auth_enable = "yes"; smtpd_tls_ask_ccert = "yes"; smtpd_tls_security_level = "encrypt"; }; }; }; #TODO: solo para las destination addresses necesarias networking.firewall.allowedTCPPorts = optionals isPrimary [ 25 465 ]; local = { boot.impermanence.directories = [ { directory = "/var/lib/postfix"; user = "root"; group = "root"; mode = "u=rwx,g=rx,o=rx"; } ] ++ optionals isPrimary [ { directory = "/var/lib/opendkim"; user = "opendkim"; group = "postfix"; mode = "u=rwx,g=,o="; } ]; certs.smtp.enable = isPrimary; certs.smtp-backup.enable = isBackup; }; security.acme.certs.${mtaDomain.main}.reloadServices = [ "postfix.service" ]; }; }