diff options
Diffstat (limited to 'sys/mta')
| -rw-r--r-- | sys/mta/default.nix | 270 |
1 files changed, 270 insertions, 0 deletions
diff --git a/sys/mta/default.nix b/sys/mta/default.nix new file mode 100644 index 0000000..2bd0cdd --- /dev/null +++ b/sys/mta/default.nix @@ -0,0 +1,270 @@ +{ + 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; + }; + + mtaListen = 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; + + # 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-bytes-upper) + 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))); + }; + + settings.main = + { + mydomain = domain; + myhostname = mtaDomain.main; + inet_interfaces = [cfg.mtaListen]; + + myorigin = "$mydomain"; + #TODO: check_recipient_access para rechazar localhost desde afuera + mydestination = optionals isPrimary ["localhost" "$mydomain"]; + mynetworks_style = "host"; + + relayhost = optional isBackup "[${domains.smtp.main}]"; + relay_domains = + if isBackup + then allDomains + else null; + + smtpd_tls_chain_files = ["${cert}/key.pem" "${cert}/fullchain.pem"]; + + # user+extension@domain.tld + recipient_delimiter = optionalString isPrimary "+"; + + message_size_limit = 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 { + 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"]; + + # Evita race condition en bind de inet_interfaces + systemd.services.postfix-setup = { + after = ["network-online.target"]; + wants = ["network-online.target"]; + }; + }; +} |
