{ config, lib, pkgs, ... }: let cfg = config.sbruder.mailserver; certDir = config.security.acme.certs."${cfg.fqdn}".directory; in { options.sbruder.mailserver = with lib; with lib.types; { enable = mkEnableOption "simple mail server"; fqdn = mkOption { type = str; description = '' FQDN of the mail server It needs to have a matching reverse DNS record. Also, an acme certificate with this name has to be present. ''; example = "mail.example.com"; }; storage = mkOption { type = path; description = "Location of the storage for mails"; default = "/var/vmail"; }; domains = mkOption { type = listOf str; description = "Domains to serve"; example = [ "example.com" "example.org" ]; }; users = mkOption { type = listOf (submodule { options = { address = mkOption { type = str; description = "Primary e-mail address of the user"; example = "jdoe@example.com"; }; passwordHash = mkOption { type = str; description = '' Bcrypt hash of the user’s password. Please note that it will be world-readable in the nix store. You can generate a password with `nix run nixpkgs.apacheHttpd -c htpasswd -nBC 12 "" | cut -d: -f2` ''; example = "$2y$05$SHxhwVGx.XCd19HAcb1NKuidUxW1BwU7GeO0ZIcMTc5t2uZoYLVRK"; }; aliases = mkOption { type = listOf str; description = '' A list of aliases for the user. If multiple users have the same alias defined, mail will be delivered to both of them. ''; default = [ ]; example = [ "j.doe@example.com" "jane.doe@example.com" "postmaster@example.com" ]; }; }; }); description = "Users of the mail server"; }; cleanHeaders = mkOption { type = listOf str; description = "A list of regular expressions that define what headers are filtered"; default = [ "/^\\s*Received:/" "/^\\s*User-Agent:/" "/^\\s*X-Mailer:/" "/^\\s*X-Originating-IP:/" ]; }; rejectSenders = mkOption { type = listOf str; description = "A list of senders to reject mails from"; default = [ ]; example = [ "newsletter@example.com" "spammer@example.com" ]; }; spam = { enable = (lib.mkEnableOption "spam filtering") // { default = true; }; }; }; config = lib.mkIf cfg.enable { # Users and groups users.users.vmail = { uid = 10000; group = "vmail"; home = cfg.storage; createHome = true; }; users.groups.vmail.gid = 10000; # Firewall networking.firewall.allowedTCPPorts = [ 143 # IMAP 25 # SMTP 587 # SMTP submission ]; # Service dependencies systemd.services.dovecot2 = { wants = [ "acme-finished-${cfg.fqdn}.target" ]; after = [ "acme-finished-${cfg.fqdn}.target" ]; }; systemd.services.postfix = { wants = [ "acme-finished-${cfg.fqdn}.target" ]; requires = [ "dovecot2.service" ]; after = [ "acme-finished-${cfg.fqdn}.target" "dovecot2.service" ]; }; # Reload on certificate renewal security.acme.certs."${cfg.fqdn}".postRun = '' if systemctl is-active dovecot2; then systemctl --no-block reload dovecot2 fi if systemctl is-active postfix; then systemctl --no-block reload postfix fi ''; # Postfix security.dhparams.params.postfix = { }; services.postfix = let listToString = lib.concatStringsSep ","; valiases = let # List of attribute sets with single key-value pair plainAliases = (lib.flatten (map ({ address, aliases, ... }: map (alias: { "${alias}" = address; }) (aliases ++ lib.singleton address)) cfg.users)); # Attribute set with every alias mapped to a list of receivers mergedAliases = (lib.attrsets.foldAttrs (val: col: lib.singleton val ++ col) [ ] plainAliases); # Contents of the aliases file aliasesString = (lib.concatStringsSep "\n" (lib.mapAttrsToList (alias: addresses: "${alias} ${listToString addresses}") mergedAliases)); in pkgs.writeText "valiases" aliasesString; access_sender = pkgs.writeText "access_sender" (lib.concatMapStringsSep "\n" (sender: "${sender} REJECT") cfg.rejectSenders); submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (lib.concatMapStringsSep "\n" (regex: "${regex} IGNORE") cfg.cleanHeaders); in { enable = true; enableSubmission = true; hostname = cfg.fqdn; networksStyle = "host"; sslCert = "${certDir}/fullchain.pem"; sslKey = "${certDir}/key.pem"; recipientDelimiter = "+"; mapFiles = { inherit access_sender valiases; }; config = { # General smtpd_banner = "${cfg.fqdn} ESMTP NO UCE"; disable_vrfy_command = true; # disable check if mailbox exists enable_long_queue_ids = true; # better for debugging strict_rfc821_envelopes = true; # only accept properly formatted envelope message_size_limit = "50331648"; # 48 MiB virtual_mailbox_domains = listToString cfg.domains; virtual_mailbox_maps = "hash:/var/lib/postfix/conf/valiases"; virtual_alias_maps = "hash:/var/lib/postfix/conf/valiases"; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; smtpd_recipient_restrictions = listToString [ "reject_non_fqdn_recipient" "reject_rbl_client ix.dnsbl.manitu.net" "reject_unknown_recipient_domain" "reject_unverified_recipient" ]; smtpd_client_restrictions = listToString [ "reject_rbl_client ix.dnsbl.manitu.net" "reject_unknown_client_hostname" ]; smtpd_sender_restrictions = listToString [ "check_sender_access hash:/var/lib/postfix/conf/access_sender" "reject_non_fqdn_sender" "reject_unknown_sender_domain" ]; # generated 2021-02-04, Mozilla Guideline v5.6, Postfix 3.5.6, OpenSSL 1.1.1i, intermediate configuration # https://ssl-config.mozilla.org/#server=postfix&version=3.5.6&config=intermediate&openssl=1.1.1i&guideline=5.6 smtpd_tls_security_level = "may"; smtpd_tls_auth_only = "yes"; smtpd_tls_mandatory_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1"; smtpd_tls_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1"; smtpd_tls_mandatory_ciphers = "medium"; smtpd_tls_loglevel = "1"; tls_medium_cipherlist = listToString [ "ECDHE-ECDSA-AES128-GCM-SHA256" "ECDHE-RSA-AES128-GCM-SHA256" "ECDHE-ECDSA-AES256-GCM-SHA384" "ECDHE-RSA-AES256-GCM-SHA384" "ECDHE-ECDSA-CHACHA20-POLY1305" "ECDHE-RSA-CHACHA20-POLY1305" "DHE-RSA-AES128-GCM-SHA256" "DHE-RSA-AES256-GCM-SHA384" ]; tls_preempt_cipherlist = "no"; smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix.path; }; submissionOptions = { smtpd_tls_security_level = "encrypt"; smtpd_sasl_auth_enable = "yes"; smtpd_sasl_type = "dovecot"; smtpd_sasl_path = "/run/dovecot2/auth"; smtpd_sender_login_maps = "hash:/etc/postfix/valiases"; smtpd_recipient_restrictions = listToString [ ]; smtpd_client_restrictions = listToString [ "permit_sasl_authenticated" "reject" ]; smtpd_sender_restrictions = listToString [ "reject_sender_login_mismatch" ]; cleanup_service_name = "submission-header-cleanup"; }; masterConfig = { submission-header-cleanup = { private = false; maxproc = 0; command = "cleanup"; args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ]; }; }; }; # Dovecot services.dovecot2 = let postfixCfg = config.services.postfix; passdb = pkgs.writeText "dovecot-users" (lib.concatMapStringsSep "\n" ({ address, passwordHash, ... }: "${address}:{BLF-CRYPT}${passwordHash}") cfg.users); in { enable = true; enableLmtp = true; enablePAM = false; mailUser = "vmail"; mailGroup = "vmail"; mailLocation = "maildir:${cfg.storage}/%d/%n"; sslServerCert = "${certDir}/fullchain.pem"; sslServerKey = "${certDir}/key.pem"; extraConfig = '' # generated 2021-02-04, Mozilla Guideline v5.6, Dovecot 2.3.13, OpenSSL 1.1.1i, intermediate configuration # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.13&config=intermediate&openssl=1.1.1i&guideline=5.6 ssl = required ssl_min_protocol = TLSv1.2 ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 ssl_prefer_server_ciphers = no service imap-login { inet_listener imap { } } service lmtp { unix_listener dovecot-lmtp { mode = 0600 user = ${postfixCfg.user} group = ${postfixCfg.group} } } passdb { driver = passwd-file args = username_format=%u ${passdb} } userdb { driver = static args = uid=vmail gid=vmail home=${cfg.storage}/%d/%n } service auth { unix_listener auth { mode = 0660 user = ${postfixCfg.user} group = ${postfixCfg.group} } } lda_mailbox_autosubscribe = yes lda_mailbox_autocreate = yes ''; }; # DNS (recursor for DNSBLs etc.) services.resolved.enable = false; services.unbound = { enable = true; settings = { server = { interface = [ "127.0.0.53" ]; }; }; resolveLocalQueries = false; # does not work with .53 }; networking.resolvconf.extraConfig = '' name_servers='127.0.0.53' ''; # rspamd sops.secrets.rspamd-worker-controller = lib.mkIf cfg.spam.enable { owner = config.users.users.rspamd.name; sopsFile = ../machines + "/${config.networking.hostName}/secrets.yaml"; }; services.rspamd = { enable = cfg.spam.enable; postfix.enable = true; workers = { normal = { includes = [ "$CONFDIR/worker-normal.inc" ]; bindSockets = lib.singleton { socket = "/run/rspamd/rspamd.sock"; mode = "0660"; owner = "${config.services.rspamd.user}"; group = "${config.services.rspamd.group}"; }; }; controller = { includes = [ "$CONFDIR/worker-controller.inc" ]; bindSockets = [ "127.0.0.1:11334" ] ++ lib.optional config.sbruder.wireguard.home.enable "${config.sbruder.wireguard.home.address}:11334"; }; }; locals = { "dkim_signing.conf".text = '' enabled = false; ''; "logging.inc".text = '' # starts at info, drops to notice once started up level = "silent"; ''; "milter_headers.conf".text = '' extended_spam_headers = true; ''; "redis.conf".text = '' servers = "127.0.0.1:${toString config.services.redis.servers.rspamd.port}" ''; "worker-controller.inc".source = config.sops.secrets.rspamd-worker-controller.path; # includes password }; }; services.redis = lib.mkIf cfg.spam.enable { vmOverCommit = true; servers.rspamd = { enable = true; port = 6379; }; }; }; }