diff --git a/mailsystem/default.nix b/mailsystem/default.nix index 874fa8e..db16958 100644 --- a/mailsystem/default.nix +++ b/mailsystem/default.nix @@ -1,4 +1,10 @@ -{lib, ...}: { +{ + config, + lib, + ... +}: let + cfg = config.mailsystem; +in { options.mailsystem = { enable = lib.mkEnableOption "nixos-mailsystem"; @@ -14,6 +20,32 @@ description = "Fully qualified domain name of the mail server."; }; + reverseFqdn = lib.mkOption { + type = lib.types.str; + default = cfg.fqdn; + defaultText = lib.literalMD "{option}`mailsystem.fqdn`"; + example = "server.example.com"; + description = '' + Fully qualified domain name used by the server to identify + with other servers. + + This needs to be set to the same value of the server's IP reverse DNS. + ''; + }; + + domains = lib.mkOption { + type = lib.types.listOf lib.types.str; + example = ["example.com"]; + default = []; + description = "List of domains to be served by the mail server"; + }; + + messageSizeLimit = lib.mkOption { + type = lib.types.int; + default = 64 * 1024 * 1024; + description = "Maximum accepted mail size"; + }; + vmailUID = lib.mkOption { type = lib.types.int; default = 5000; @@ -60,6 +92,17 @@ ''; }; + aliases = lib.mkOption { + type = with lib.types; listOf types.str; + example = ["abuse@example.com" "postmaster@example.com"]; + default = []; + description = '' + A list of aliases of this login account. + Note: Use list entries like "@example.com" to create a catchAll + that allows sending from all email addresses in these domain. + ''; + }; + isSystemUser = lib.mkOption { type = lib.types.bool; default = false; @@ -83,11 +126,37 @@ description = "All available accounts for the mailsystem."; default = {}; }; + + extraVirtualAliases = lib.mkOption { + type = let + account = lib.mkOptionType { + name = "Mail Account"; + check = account: builtins.elem account (builtins.attrNames cfg.accounts); + }; + in + with lib.types; attrsOf (either account (nonEmptyListOf account)); + example = { + "info@example.com" = "user1@example.com"; + "postmaster@example.com" = "user1@example.com"; + "abuse@example.com" = "user1@example.com"; + "multi@example.com" = ["user1@example.com" "user2@example.com"]; + }; + description = '' + Virtual account aliases. A virtual alias `"info@example.com" = "user1@example.com"` + means that all mail to `info@example.com` is forwarded to `user1@example.com`. + Furthermore, it also allows the user `user1@example.com` to send emails as + `info@example.com`. It is also possible to create an alias for multiple accounts. + In this example, all mails for `multi@example.com` will be forwarded to both + `user1@example.com` and `user2@example.com`. + ''; + default = {}; + }; }; imports = [ ./dovecot.nix ./nginx.nix + ./postfix.nix ./user.nix ]; } diff --git a/mailsystem/nginx.nix b/mailsystem/nginx.nix index e780b4a..7edd771 100644 --- a/mailsystem/nginx.nix +++ b/mailsystem/nginx.nix @@ -18,6 +18,7 @@ in { networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [80 443]; security.acme.certs."${cfg.fqdn}".reloadServices = [ + "postfix.service" "dovecot2.service" ]; }; diff --git a/mailsystem/postfix.nix b/mailsystem/postfix.nix new file mode 100644 index 0000000..ec42a1c --- /dev/null +++ b/mailsystem/postfix.nix @@ -0,0 +1,194 @@ +{ + config, + lib, + pkgs, + ... +}: +with (import ./common.nix {inherit config;}); let + cfg = config.mailsystem; + + mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; + + attrsToLookupTable = aliases: let + lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases; + in + mergeLookupTables lookupTables; + + lookupTableToString = attrs: let + valueToString = value: lib.concatStringsSep ", " value; + in + lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs); + + mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables; + + account_virtual_aliases = mergeLookupTables (lib.flatten (lib.mapAttrsToList + (name: value: let + to = name; + in + map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name)) + cfg.accounts)); + + extra_virtual_aliases = attrsToLookupTable cfg.extraVirtualAliases; + + all_virtual_aliases = mergeLookupTables [account_virtual_aliases extra_virtual_aliases]; + + aliases_file = let + content = lookupTableToString all_virtual_aliases; + in + builtins.toFile "virtual_aliases" content; + + # File containing all mappings of authenticated accounts and their sender mail addresses. + virtual_accounts_file = let + content = lookupTableToString all_virtual_aliases; + in + builtins.toFile "virtual_accounts" content; + + virtual_domains_file = builtins.toFile "virtual_domains" (lib.concatStringsSep "\n" cfg.domains); + + submission_header_cleanup_rules = 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 + + # Replace the user's submitted hostname with the server's FQDN to hide the + # user's host/network. + /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}> + ''; + + tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; + tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; +in { + config = lib.mkIf cfg.enable { + services.postfix = { + enable = true; + hostname = "${cfg.reverseFqdn}"; + networksStyle = "host"; + + sslCert = sslCertPath; + sslKey = sslKeyPath; + + enableSubmissions = true; + + # TODO: create function to simplify this? + mapFiles."virtual_aliases" = aliases_file; + mapFiles."virtual_accounts" = virtual_accounts_file; + virtual = lookupTableToString all_virtual_aliases; + + submissionsOptions = { + smtpd_tls_security_level = "encrypt"; + smtpd_sasl_auth_enable = "yes"; + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "/run/dovecot2/auth"; + smtpd_sasl_security_options = "noanonymous"; + smtpd_sasl_local_domain = "$myhostname"; + smtpd_client_restrictions = "permit_sasl_authenticated,reject"; + # use mappedFile -> different path? + smtpd_sender_login_maps = "hash:/etc/postfix/virtual_accounts"; + smtpd_sender_restrictions = "reject_sender_login_mismatch"; + smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; + cleanup_service_name = "submission-header-cleanup"; + }; + + config = { + mydestination = ""; + recipient_delimiter = "+"; + smtpd_banner = "${cfg.fqdn} ESMTP NO UCE"; + disable_vrfy_command = true; + message_size_limit = toString cfg.messageSizeLimit; + + virtual_uid_maps = "static:${toString cfg.vmailUID}"; + virtual_gid_maps = "static:${toString cfg.vmailUID}"; + virtual_mailbox_base = cfg.mailDirectory; + virtual_mailbox_domains = virtual_domains_file; + virtual_mailbox_maps = [ + (mappedFile "virtual_aliases") + ]; + 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 (enforce authentication via 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" + ]; + + # TLS settings, inspired by https://github.com/jeaye/nix-files + # Submission by mail clients is handled in submissionOptions + smtpd_tls_security_level = "may"; + + # Disable obsolete protocols + smtpd_tls_protocols = tls_protocols; + smtp_tls_protocols = tls_protocols; + smtpd_tls_mandatory_protocols = tls_protocols; + smtp_tls_mandatory_protocols = tls_protocols; + + smtp_tls_ciphers = "high"; + smtpd_tls_ciphers = "high"; + smtp_tls_mandatory_ciphers = "high"; + smtpd_tls_mandatory_ciphers = "high"; + + # Disable deprecated ciphers + smtpd_tls_mandatory_exclude_ciphers = tls_exclude_ciphers; + smtpd_tls_exclude_ciphers = tls_exclude_ciphers; + smtp_tls_mandatory_exclude_ciphers = tls_exclude_ciphers; + smtp_tls_exclude_ciphers = tls_exclude_ciphers; + + tls_preempt_cipherlist = true; + + # Allowing AUTH on a non-encrypted connection poses a security risk + smtpd_tls_auth_only = true; + # Log only a summary message on TLS handshake completion + smtpd_tls_loglevel = "1"; + + # Configure a non-blocking source of randomness + tls_random_source = "dev:/dev/urandom"; + + # Fix for https://www.postfix.org/smtp-smuggling.html + smtpd_forbid_bare_newline = "yes"; + smtpd_forbid_bare_newline_exclusions = "$mynetworks"; + }; + + 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:${submission_header_cleanup_rules}"]; + }; + }; + }; + + systemd.services.postfix = { + wants = sslCertService; + after = + ["dovecot2.service"] + ++ sslCertService; + requires = ["dovecot2.service"]; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ + 25 # smtp + 465 # submissions + ]; + }; + }; +}