{ config, pkgs, lib, flakeInputs, ... }: with lib; let dovecotSievePipeBin = pkgs.stdenv.mkDerivation { name = "dovecot-sieve-pipe-bin"; src = ./dovecot-pipe-bin; buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ]; buildCommand = '' mkdir -p $out/pipe/bin cp $src/* $out/pipe/bin/ chmod a+x $out/pipe/bin/* patchShebangs $out/pipe/bin for file in $out/pipe/bin/*; do wrapProgram $file \ --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin" done ''; }; ccert-policy-server = flakeInputs.mach-nix.lib.${config.nixpkgs.system}.buildPythonPackage { src = ./ccert-policy-server; pname = "ccert-policy-server"; version = "0.0.0"; python = "python39"; ignoreDataOutdated = true; requirements = '' sdnotify systemd-socketserver psycopg >=3.0.0 psycopg-pool >=3.0.0 psycopg-binary >=3.0.0 ''; overridesPre = [ (self: super: { systemd-python = super.systemd.overrideAttrs (oldAttrs: { pname = "systemd-python"; }); }) ]; }; spmDomains = ["bouncy.email"]; emailDomains = spmDomains ++ ["kleen.consulting"]; in { config = { nixpkgs.overlays = [ (final: prev: { postfix = prev.postfix.override { withLDAP = false; withPgSQL = true; }; dovecot = prev.dovecot.override { withSQLite = false; withPgSQL = true; }; }) ]; services.postfix = { enable = true; enableSmtp = false; hostname = "surtr.yggdrasil.li"; recipientDelimiter = ""; setSendmail = true; postmasterAlias = ""; rootAlias = ""; extraAliases = ""; destination = []; sslCert = "/run/credentials/postfix.service/surtr.yggdrasil.li.pem"; sslKey = "/run/credentials/postfix.service/surtr.yggdrasil.li.key.pem"; networks = []; config = let relay_ccert = "texthash:${pkgs.writeText "relay_ccert" ""}"; in { smtpd_tls_security_level = "may"; #the dh params smtpd_tls_dh1024_param_file = toString config.security.dhparams.params."postfix-1024".path; smtpd_tls_dh512_param_file = toString config.security.dhparams.params."postfix-512".path; #enable ECDH smtpd_tls_eecdh_grade = "strong"; #enabled SSL protocols, don't allow SSLv2 and SSLv3 smtpd_tls_protocols = ["!SSLv2" "!SSLv3" "!TLSv1" "!TLSv1.1"]; smtpd_tls_mandatory_protocols = ["!SSLv2" "!SSLv3" "!TLSv1" "!TLSv1.1"]; #allowed ciphers for smtpd_tls_security_level=encrypt smtpd_tls_mandatory_ciphers = "medium"; #allowed ciphers for smtpd_tls_security_level=may #smtpd_tls_ciphers = high #enforce the server cipher preference tls_preempt_cipherlist = true; #disable following ciphers for smtpd_tls_security_level=encrypt smtpd_tls_mandatory_exclude_ciphers = ["aNULL" "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL"]; #disable following ciphers for smtpd_tls_security_level=may smtpd_tls_exclude_ciphers = ["aNULL" "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL"]; #enable TLS logging to see the ciphers for inbound connections smtpd_tls_loglevel = "1"; #enable TLS logging to see the ciphers for outbound connections smtp_tls_loglevel = "1"; tls_medium_cipherlist = "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"; smtpd_tls_received_header = true; smtpd_tls_ask_ccert = true; smtpd_tls_CAfile = toString ./ca/ca.crt; smtp_tls_security_level = "dane"; smtp_dns_support_level = "dnssec"; smtp_tls_connection_reuse = true; tls_server_sni_maps = ''texthash:${pkgs.writeText "sni" ( concatMapStringsSep "\n\n" (domain: concatMapStringsSep "\n" (subdomain: "${subdomain} /run/credentials/postfix.service/${removePrefix "." subdomain}.full.pem") [domain "mailin.${domain}" "mailsub.${domain}" ".${domain}"] ) emailDomains )}''; smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix"; local_recipient_maps = ""; # 10 GiB message_size_limit = "10737418240"; # 10 GiB mailbox_size_limit = "10737418240"; smtpd_delay_reject = true; smtpd_helo_required = true; smtpd_helo_restrictions = "permit"; smtpd_recipient_restrictions = [ "reject_unauth_pipelining" "reject_non_fqdn_recipient" "reject_unknown_recipient_domain" "check_recipient_access pgsql:${pkgs.writeText "check_recipient_access.cf" '' hosts = postgresql:///email dbname = email query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' ''}" "check_ccert_access ${relay_ccert}" "reject_non_fqdn_helo_hostname" "reject_invalid_helo_hostname" "reject_unauth_destination" "reject_unknown_recipient_domain" "reject_unverified_recipient" ]; unverified_recipient_reject_code = "550"; unverified_recipient_reject_reason = "Recipient address lookup failed"; address_verify_map = "internal:address_verify_map"; address_verify_positive_expire_time = "1h"; address_verify_positive_refresh_time = "15m"; address_verify_negative_expire_time = "15s"; address_verify_negative_refresh_time = "5s"; address_verify_cache_cleanup_interval = "5s"; address_verify_poll_delay = "1s"; smtpd_relay_restrictions = [ "check_ccert_access ${relay_ccert}" "reject_unauth_destination" ]; propagate_unmatched_extensions = ["canonical" "virtual" "alias"]; smtpd_authorized_verp_clients = ""; authorized_verp_clients = ""; smtpd_client_event_limit_exceptions = ""; milter_default_action = "accept"; smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; non_smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; alias_maps = ""; queue_run_delay = "10s"; minimal_backoff_time = "1m"; maximal_backoff_time = "10m"; maximal_queue_lifetime = "100m"; bounce_queue_lifetime = "20m"; smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" '' # Allow DSN requests from local subnet only 192.168.0.0/16 silent-discard 172.16.0.0/12 silent-discard 10.0.0.0/8 silent-discard 0.0.0.0/0 silent-discard, dsn fd00::/8 silent-discard ::/0 silent-discard, dsn ''}"; sender_canonical_maps = "tcp:localhost:${toString config.services.postsrsd.forwardPort}"; sender_canonical_classes = "envelope_sender"; recipient_canonical_maps = "tcp:localhost:${toString config.services.postsrsd.reversePort}"; recipient_canonical_classes = ["envelope_recipient" "header_recipient"]; virtual_mailbox_domains = ''pgsql:${pkgs.writeText "virtual_mailbox_domains.cf" '' hosts = postgresql:///email dbname = email query = SELECT 1 FROM virtual_mailbox_domain WHERE domain = '%s' ''}''; virtual_mailbox_maps = ''pgsql:${pkgs.writeText "virtual_mailbox_maps.cf" '' hosts = postgresql:///email dbname = email query = SELECT 1 FROM virtual_mailbox_mapping WHERE lookup = '%s' OR (lookup = regexp_replace('%s', '\+[^@]*@', '@') AND NOT EXISTS (SELECT 1 FROM virtual_mailbox_mapping WHERE lookup = '%s')) ''}''; dvlmtp_destination_recipient_limit = "1"; virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp"; smtputf8_enable = false; authorized_submit_users = "inline:{ root= postfwd= }"; postscreen_access_list = ""; postscreen_denylist_action = "drop"; postscreen_greet_action = "enforce"; }; masterConfig = { smtps = { type = "inet"; private = false; command = "smtpd"; args = [ "-o" "smtpd_tls_security_level=encrypt" "-o" "{smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2}" "-o" "{smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2}" "-o" "smtpd_tls_mandatory_ciphers=high" "-o" "smtpd_tls_dh1024_param_file=${toString config.security.dhparams.params."postfix-smtps-1024".path}" "-o" "smtpd_tls_dh512_param_file=${toString config.security.dhparams.params."postfix-smtps-512".path}" "-o" "{tls_eecdh_auto_curves = X25519 X448}" "-o" "smtpd_tls_wrappermode=yes" "-o" "smtpd_tls_ask_ccert=yes" "-o" "smtpd_tls_req_ccert=yes" "-o" "smtpd_tls_received_header=no" "-o" "cleanup_service_name=subcleanup" "-o" "smtpd_client_restrictions=permit_tls_all_clientcerts,reject" "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" "-o" "{smtpd_sender_restrictions = reject_unknown_sender_domain,reject_unverified_sender,check_policy_service unix:/run/postfix-ccert-sender-policy.sock}" "-o" "unverified_sender_reject_code=550" "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}" "-o" ''{smtpd_recipient_restrictions=reject_unauth_pipelining,reject_non_fqdn_recipient,reject_unknown_recipient_domain,check_recipient_access pgsql:${pkgs.writeText "check_recipient_access.cf" '' hosts = postgresql:///email dbname = email query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' OR (lookup = regexp_replace('%s', '\+[^@]*@', '@') AND NOT EXISTS (SELECT 1 FROM virtual_mailbox_access WHERE lookup = '%s')) ''},permit_tls_all_clientcerts,reject}'' "-o" "milter_macro_daemon_name=surtr.yggdrasil.li" "-o" ''smtpd_milters=${config.services.opendkim.socket}'' ]; }; subcleanup = { command = "cleanup"; private = false; maxproc = 0; args = [ "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" '' /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1 ''}" ]; }; dvlmtp = { command = "lmtp"; args = [ "flags=DORX" ]; }; smtp_pass = { name = "smtpd"; type = "pass"; command = "smtpd"; }; postscreen = { name = "smtp"; type = "inet"; private = false; command = "postscreen"; maxproc = 1; }; smtp = {}; relay = { command = "smtp"; args = [ "-o" "smtp_fallback_relay=" ]; }; tlsproxy = { maxproc = 0; }; dnsblog = {}; }; }; services.postsrsd = { enable = true; domain = "surtr.yggdrasil.li"; separator = "+"; excludeDomains = [ "surtr.yggdrasil.li" ] ++ concatMap (domain: [".${domain}" domain]) emailDomains; }; services.opendkim = { enable = true; user = "postfix"; group = "postfix"; socket = "local:/run/opendkim/opendkim.sock"; domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li"] ++ emailDomains)}''; selector = "surtr"; configFile = builtins.toFile "opendkim.conf" '' Syslog true MTA surtr.yggdrasil.li MTACommand ${config.security.wrapperDir}/sendmail LogResults true ''; }; services.rspamd = { enable = true; workers = { controller = { type = "controller"; count = 1; bindSockets = [ { mode = "0660"; socket = "/run/rspamd/worker-controller.sock"; owner = config.services.rspamd.user; group = config.services.rspamd.group; } ]; includes = []; extraConfig = '' static_dir = "''${WWWDIR}"; # Serve the web UI static assets ''; }; external = { type = "rspamd_proxy"; bindSockets = [ { mode = "0660"; socket = "/run/rspamd/rspamd-milter.sock"; owner = config.services.rspamd.user; group = config.services.rspamd.group; } ]; extraConfig = '' milter = yes; timeout = 120s; upstream "local" { default = yes; self_scan = yes; } ''; }; }; locals = { "milter_headers.conf".text = '' use = ["authentication-results", "x-spamd-result", "x-rspamd-queue-id", "x-rspamd-server", "x-spam-level", "x-spam-status"]; extended_headers_rcpt = []; ''; "actions.conf".text = '' reject = 15; add_header = 10; greylist = 5; ''; "groups.conf".text = '' symbols { "BAYES_SPAM" { weight = 2.0; } } ''; "dmarc.conf".text = '' reporting = true; send_reports = true; report_settings { org_name = "Yggdrasil.li"; domain = "yggdrasil.li"; email = "postmaster@yggdrasil.li"; } ''; "redis.conf".text = '' servers = "${config.services.redis.servers.rspamd.unixSocket}"; ''; "dkim_signing.conf".text = "enabled = false;"; "neural.conf".text = "enabled = false;"; "classifier-bayes.conf".text = '' enable = true; expire = 8640000; new_schema = true; backend = "redis"; per_user = true; min_learns = 0; autolearn = [0, 10]; statfile { symbol = "BAYES_HAM"; spam = false; } statfile { symbol = "BAYES_SPAM"; spam = true; } ''; # "redirectors.inc".text = '' # visit.creeper.host # ''; }; }; users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user "dovecot2" ]; services.redis.servers.rspamd.enable = true; users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ]; services.dovecot2 = { enable = true; enablePAM = false; sslServerCert = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.pem"; sslServerKey = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.key.pem"; sslCACert = toString ./ca/ca.crt; mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u"; modules = with pkgs; [ dovecot_pigeonhole dovecot_fts_xapian ]; mailPlugins.globally.enable = [ "fts" "fts_xapian" ]; protocols = [ "lmtp" "sieve" ]; extraConfig = let dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" '' driver = pgsql connect = dbname=email password_query = SELECT NULL as password, 'Y' as nopassword, "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' user_query = SELECT "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' iterate_query = SELECT "user" FROM imap_user ''; in '' mail_home = /var/lib/mail/%u mail_plugins = $mail_plugins quota first_valid_uid = ${toString config.users.users.dovecot2.uid} last_valid_uid = ${toString config.users.users.dovecot2.uid} first_valid_gid = ${toString config.users.groups.dovecot2.gid} last_valid_gid = ${toString config.users.groups.dovecot2.gid} ${concatMapStringsSep "\n\n" (domain: concatMapStringsSep "\n" (subdomain: '' local_name ${subdomain} { ssl_cert = </run/credentials/dovecot2.service/${subdomain}.pem ssl_key = </run/credentials/dovecot2.service/${subdomain}.key.pem } '') ["imap.${domain}" domain] ) emailDomains} ssl_require_crl = no ssl_verify_client_cert = yes 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 auth_ssl_username_from_cert = yes ssl_cert_username_field = commonName auth_mechanisms = external auth_verbose = yes verbose_ssl = yes auth_debug = yes service auth { user = dovecot2 } service auth-worker { user = dovecot2 } userdb { driver = prefetch } userdb { driver = sql args = ${dovecotSqlConf} } passdb { driver = sql args = ${dovecotSqlConf} } protocol lmtp { userdb { driver = sql args = ${pkgs.writeText "dovecot-sql.conf" '' driver = pgsql connect = dbname=email user_query = SELECT DISTINCT ON (extension IS NULL, local IS NULL) "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM lmtp_mapping WHERE CASE WHEN extension IS NOT NULL AND local IS NOT NULL THEN ('%n' :: citext) = local || '+' || extension AND domain = ('%d' :: citext) WHEN local IS NOT NULL THEN (local = ('%n' :: citext) OR ('%n' :: citext) ILIKE local || '+%%') AND domain = ('%d' :: citext) WHEN extension IS NOT NULL THEN ('%n' :: citext) ILIKE '%%+' || extension AND domain = ('%d' :: citext) ELSE domain = ('%d' :: citext) END ORDER BY (extension IS NULL) ASC, (local IS NULL) ASC ''} skip = never result_failure = return-fail result_internalfail = return-fail } mail_plugins = $mail_plugins sieve } mailbox_list_index = yes postmaster_address = postmaster@yggdrasil.li recipient_delimiter = auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-+_@ service lmtp { vsz_limit = 1G unix_listener /run/dovecot-lmtp { mode = 0600 user = postfix group = postfix } } namespace inbox { separator = / inbox = yes prefix = mailbox Trash { auto = no special_use = \Trash } mailbox Junk { auto = no special_use = \Junk } mailbox Drafts { auto = no special_use = \Drafts } mailbox Sent { auto = subscribe special_use = \Sent } mailbox "Sent Messages" { auto = no special_use = \Sent } } plugin { quota = count quota_rule = *:storage=1GB quota_rule2 = Trash:storage=+10%% quota_status_overquota = "552 5.2.2 Mailbox is full" quota_status_success = DUNNO quota_status_nouser = DUNNO quota_grace = 10%% quota_max_mail_size = ${config.services.postfix.config.message_size_limit} quota_vsizes = yes } protocol imap { mail_max_userip_connections = 50 mail_plugins = $mail_plugins imap_quota imap_sieve } service imap-login { inet_listener imap { port = 0 } } service managesieve-login { inet_listener sieve { port = 4190 } } plugin { sieve_plugins = sieve_imapsieve sieve_extprograms sieve = file:~/sieve;active=~/dovecot.sieve sieve_redirect_envelope_from = orig_recipient sieve_before = /etc/dovecot/sieve_before.d sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment sieve_pipe_bin_dir = ${dovecotSievePipeBin}/pipe/bin imapsieve_mailbox1_name = * imapsieve_mailbox1_causes = FLAG imapsieve_mailbox1_before = /etc/dovecot/sieve_flag.d/learn-junk.sieve } plugin { plugin = fts fts_xapian fts = xapian fts_xapian = partial=2 full=20 attachments=1 verbose=1 fts_autoindex = yes fts_enforced = no } service indexer-worker { vsz_limit = ${toString (1024 * 1024 * 1024)} } ''; }; systemd.services.dovecot-fts-xapian-optimize = { description = "Optimize dovecot indices for fts_xapian"; requisite = [ "dovecot2.service" ]; after = [ "dovecot2.service" ]; startAt = "*-*-* 22:00:00 Europe/Berlin"; serviceConfig = { Type = "oneshot"; ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A"; PrivateDevices = true; PrivateNetwork = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; ProtectHome = true; ProtectSystem = true; PrivateTmp = true; }; }; systemd.timers.dovecot-fts-xapian-optimize = { timerConfig = { RandomizedDelaySec = 4 * 3600; }; }; environment.etc = { "dovecot/sieve_before.d/tag-junk.sieve".text = '' require ["imap4flags"]; if header :contains "X-Spam-Flag" "YES" { addflag ["\\Junk"]; } ''; "dovecot/sieve_flag.d/learn-junk.sieve".text = '' require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables", "imap4flags"]; if environment :matches "imap.user" "*" { set "username" "''${1}"; } if environment :contains "imap.changedflags" "\\Junk" { if hasflag "\\Junk" { pipe :copy "learn_spam.sh" [ "''${username}" ]; } else { if environment :matches "imap.mailbox" "*" { set "mailbox" "''${1}"; } if not anyof(string "''${mailbox}" "Trash", string "''${mailbox}" "Junk") { pipe :copy "learn_ham.sh" [ "''${username}" ]; } } } ''; }; security.dhparams = { params = { "postfix-512".bits = 512; "postfix-1024".bits = 2048; "postfix-smtps-512".bits = 512; "postfix-smtps-1024".bits = 2048; }; }; security.acme.rfc2136Domains = { "surtr.yggdrasil.li" = { restartUnits = [ "postfix.service" "dovecot2.service" ]; }; } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains) // listToAttrs (concatMap (domain: [ (nameValuePair domain { restartUnits = ["postfix.service" "dovecot2.service"]; }) (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; }) (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; }) (nameValuePair "imap.${domain}" { restartUnits = ["dovecot2.service"]; }) (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; }) ]) emailDomains); systemd.services.postfix = { serviceConfig.LoadCredential = [ "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" "surtr.yggdrasil.li.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/fullchain.pem" ] ++ concatMap (domain: map (subdomain: "${subdomain}.full.pem:${config.security.acme.certs.${subdomain}.directory}/full.pem") [domain "mailin.${domain}" "mailsub.${domain}"] ) emailDomains; }; systemd.services.dovecot2 = { preStart = '' for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do ${pkgs.dovecot_pigeonhole}/bin/sievec $f done ''; serviceConfig = { LoadCredential = [ "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" "surtr.yggdrasil.li.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/fullchain.pem" ] ++ concatMap (domain: concatMap (subdomain: [ "${subdomain}.key.pem:${config.security.acme.certs.${subdomain}.directory}/key.pem" "${subdomain}.pem:${config.security.acme.certs.${subdomain}.directory}/fullchain.pem" ]) [domain "imap.${domain}"] ) emailDomains; }; }; services.nginx = { upstreams.spm = { servers = { "unix:/run/spm/server.sock" = {}; }; }; virtualHosts = listToAttrs (map (domain: nameValuePair "spm.${domain}" { forceSSL = true; kTLS = true; http3 = false; sslCertificate = "/run/credentials/nginx.service/spm.${domain}.pem"; sslCertificateKey = "/run/credentials/nginx.service/spm.${domain}.key.pem"; extraConfig = '' ssl_stapling off; ssl_verify_client on; ssl_client_certificate ${toString ./ca/ca.crt}; ''; locations."/" = { proxyPass = "http://spm"; extraConfig = '' proxy_set_header SSL-CLIENT-VERIFY $ssl_client_verify; proxy_set_header SSL-CLIENT-S-DN $ssl_client_s_dn; proxy_set_header SPM-DOMAIN "${domain}"; ''; }; }) spmDomains) // listToAttrs (map (domain: nameValuePair "mta-sts.${domain}" { forceSSL = true; kTLS = true; http3 = true; sslCertificate = "/run/credentials/nginx.service/mta-sts.${domain}.pem"; sslCertificateKey = "/run/credentials/nginx.service/mta-sts.${domain}.key.pem"; sslTrustedCertificate = "/run/credentials/nginx.service/mta-sts.${domain}.chain.pem"; extraConfig = '' add_header Strict-Transport-Security "max-age=63072000" always; add_header Access-Control-Allow-Origin '*'; add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS'; add_header Access-Control-Allow-Headers 'X-Requested-With, Content-Type, Authorization'; add_header Access-Control-Max-Age 7200; ''; locations."/" = { extraConfig = '' charset utf-8; source_charset utf-8; ''; root = pkgs.runCommand "mta-sts.${domain}" {} '' mkdir -p $out/.well-known cp ${pkgs.writeText "mta-sts.${domain}.txt" '' version: STSv1 mode: enforce max_age: 2419200 mx: mailin.${domain} ''} $out/.well-known/mta-sts.txt ''; }; }) emailDomains); }; systemd.services.nginx.serviceConfig.LoadCredential = concatMap (domain: [ "spm.${domain}.key.pem:${config.security.acme.certs."spm.${domain}".directory}/key.pem" "spm.${domain}.pem:${config.security.acme.certs."spm.${domain}".directory}/fullchain.pem" ]) spmDomains ++ concatMap (domain: [ "mta-sts.${domain}.key.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/key.pem" "mta-sts.${domain}.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/fullchain.pem" "mta-sts.${domain}.chain.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/chain.pem" ]) emailDomains; systemd.services.spm = { serviceConfig = { Type = "notify"; ExecStart = "${pkgs.spm}/bin/spm-server"; User = "spm"; Group = "spm"; Environment = [ "SPM_INSTANCE=ed1c0e1d-7be4-4dd5-b51a-291bad3ac9c9" "PGCONNSTR=dbname=email" ]; LoadCredential = [ "spm-keys.json:${config.sops.secrets."spm-keys.json".path}" ]; }; }; systemd.sockets.spm = { wantedBy = [ "nginx.service" ]; socketConfig = { ListenStream = "/run/spm/server.sock"; SocketUser = "spm"; SocketGroup = "spm"; SocketMode = 0660; }; }; users.users.spm = { isSystemUser = true; group = "spm"; }; users.groups.spm = { members = [ config.services.nginx.user ]; }; sops.secrets."spm-keys.json" = { format = "binary"; sopsFile = ./spm-keys.json; }; services.postfix-mta-sts-resolver = { enable = true; loglevel = "debug"; }; systemd.sockets."postfix-ccert-sender-policy" = { requiredBy = ["postfix.service"]; wants = ["postfix-ccert-sender-policy.service"]; socketConfig = { ListenStream = "/run/postfix-ccert-sender-policy.sock"; }; }; systemd.services."postfix-ccert-sender-policy" = { after = [ "postgresql.service" ]; bindsTo = [ "postgresql.service" ]; serviceConfig = { Type = "notify"; ExecStart = "${ccert-policy-server}/bin/ccert-policy-server"; Environment = [ "PGDATABASE=email" ]; DynamicUser = false; User = "postfix-ccert-sender-policy"; Group = "postfix-ccert-sender-policy"; ProtectSystem = "strict"; SystemCallFilter = "@system-service"; NoNewPrivileges = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectKernelLogs = true; ProtectControlGroups = true; MemoryDenyWriteExecute = true; RestrictSUIDSGID = true; KeyringMode = "private"; ProtectClock = true; RestrictRealtime = true; PrivateDevices = true; PrivateTmp = true; ProtectHostname = true; ReadWritePaths = ["/run/postgresql"]; }; }; users.users."postfix-ccert-sender-policy" = { isSystemUser = true; group = "postfix-ccert-sender-policy"; }; users.groups."postfix-ccert-sender-policy" = {}; services.postfwd = { enable = true; rules = '' id=RCPT01; protocol_state=DATA; protocol_state=END-OF-MESSAGE; action=rcpt(ccert_subject/100/3600/set(HIT_RATELIMIT=1,HIT_RATECOUNT=$$ratecount,HIT_RATELIMIT_LIMIT=100,HIT_RATELIMIT_INTERVAL=3600)) id=RCPT02; protocol_state=DATA; protocol_state=END-OF-MESSAGE; action=rcpt(ccert_subject/1000/86400/set(HIT_RATELIMIT=1,HIT_RATECOUNT=$$ratecount,HIT_RATELIMIT_LIMIT=1000,HIT_RATELIMIT_INTERVAL=86400)) id=JUMP_REJECT_RL; HIT_RATELIMIT=="1"; action=jump(REJECT_RL) id=EOF; action=DUNNO id=REJECT_RL; action=450 4.7.1 Exceeding maximum of $$HIT_RATELIMIT_LIMIT recipients per $$HIT_RATELIMIT_INTERVAL seconds [$$HIT_RATECOUNT] ''; }; }; }