diff --git a/hosts/iron/configuration.nix b/hosts/iron/configuration.nix index ff5cd67..7f1580a 100644 --- a/hosts/iron/configuration.nix +++ b/hosts/iron/configuration.nix @@ -36,7 +36,6 @@ with lib; { ./services ]; config = { - system.stateVersion = "22.11"; security.sudo.wheelNeedsPassword = false; diff --git a/hosts/iron/secrets.yaml b/hosts/iron/secrets.yaml index 2d0a306..bb95bba 100644 --- a/hosts/iron/secrets.yaml +++ b/hosts/iron/secrets.yaml @@ -2,6 +2,10 @@ duckdns-secret: ENC[AES256_GCM,data:SAf/xZ28tgmvqcVKC2tMNRm838AVMMNCC3fpYLXBEIoT sturzbach-htpasswd: ENC[AES256_GCM,data:qqBwu6mASnRqjy65knU4uIvBNXXgrfcmvWnbmOH4tVQ7vRbpEhe/GQDwAg==,iv:OQnDOzezjajGl35m/u5StQeMRR+1sNDD5u1my1wTngQ=,tag:7zjVRWI1IzZ5iS3sFHLubg==,type:str] navidrome-password-encryption-key: ENC[AES256_GCM,data:ynQsFyGDEBnlWhTlv0mF7mLiXOjijq9ixWWEa1OXsTOYAd74dU0dp3Fo532WtD4fPvIWEf8Y2dYmY7zPVLuydQ==,iv:GJqPVL5OIFPLMcCVOjWvMjyFR4iTXo3uGE8R0keTzG0=,tag:RTERQgYRxBBevlL2H1lIWA==,type:str] wireguard_key_hetzner-ha: ENC[AES256_GCM,data:ak/KpQIHBNRPriJ1IeKYXIp4CcnygRHSj5MzZNnuxQnVunmmtzGu0lBEajA=,iv:aNw3EooT6XE1zC+g37WSJasRCfnNUaKQrYCDBMTxRrg=,tag:KXc70tVFc7xDLlefk1Hzow==,type:str] +hetzner-api-key: ENC[AES256_GCM,data:7eWYncujkEytQzhRdNRItPgpz1eUvcyp2PVLJtHbqd8=,iv:AxoKJUuor32kC3ZdpkDPUEUlPRosY6cKoWx0TIGK9wA=,tag:SVtXMraGxnJnx/j3zMQnQw==,type:str] +rspamd-worker-controller: ENC[AES256_GCM,data:7tS8bEr9i5F+YZoj3uPQa6Xd2SCsuC+jE531AbKEmPHNeL3qMyO0pQZ/P1ONaPHTVMOPQHYABihDJcZv0BKW,iv:pFBVi4F661fnYPcCPwuetiGL1H+RAnJiFQhTUqGNwjU=,tag:xQoHIEQpnrMOnXqsH8anxQ==,type:str] +dkim-keys: + jalr.de.default: ENC[AES256_GCM,data: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,iv:2NBiTTW9slOH9BvM+kVbMB/+8EiS/Dc/eaqrtiwn4HY=,tag:0rc2+ZWy9XZYE7RK/oSo3g==,type:str] sops: kms: [] gcp_kms: [] @@ -17,8 +21,8 @@ sops: TjdZRldhSzVtMkVoTzY1NjdGbCswRVUK0pi+8UuLqRmytcR2ikxOAM02iccl8P1y ixv0PKPLd+vQ23QeeQy/TfoGx16XttaDUnUrPLZR3TUKtAcld8+m6w== -----END AGE ENCRYPTED FILE----- - lastmodified: "2023-06-22T12:41:01Z" - mac: ENC[AES256_GCM,data:OBzeE4XsdyrmW+U9nFLizAiNpdr7rXaBIa6q8PCjMMrGEi5C2Sg+1wHzgOqB3ACYc4gjv5W3s9rAVX3YOBEJ34eu+hcRWjLlK9tmKBdSZm1nP0gkfCmbMGw1DkPdkNRufX5FrIHEG0xzLN3Wo/C9LnDO+Qwn88OVq1+TYQHH3nY=,iv:OU+Xmmqsa03oRclRw/TCIXjroA/9YOtB07R9+1caUes=,tag:ZHEXxwz6NOzsA+jGT3oe4g==,type:str] + lastmodified: "2023-06-28T01:59:15Z" + mac: ENC[AES256_GCM,data:s/b3TPwtRDYb2QDZZXTD1QKuEDDFucFq6Ec2tHA1t7+20xS9NdhLb7rZkPTmOw/UJi+FF5+H63V9K1dIZr06FSofNCLidRJTV2q9TUKQeSyguIuu25xd47udiJ53sZeEwikw8iQx0bdkUKox9VQfw3+Eqqz/088XN5fNYGKq4ow=,iv:ZwRjSg2fWZCs3YOJhPkv2rha4UOaDAi6+vHc/LHWIPo=,tag:05/0W4cy/xSgsFBj+xtckQ==,type:str] pgp: - created_at: "2023-05-02T19:30:42Z" enc: | diff --git a/hosts/iron/secrets/mail-users.nix b/hosts/iron/secrets/mail-users.nix new file mode 100644 index 0000000..4caac02 Binary files /dev/null and b/hosts/iron/secrets/mail-users.nix differ diff --git a/hosts/iron/services/default.nix b/hosts/iron/services/default.nix index 9568d28..d48d55a 100644 --- a/hosts/iron/services/default.nix +++ b/hosts/iron/services/default.nix @@ -3,6 +3,7 @@ ./dnsmasq.nix ./dyndns.nix ./jellyfin.nix + ./mail.nix ./navidrome.nix ./nginx.nix ./public-ip-tunnel.nix diff --git a/hosts/iron/services/mail.nix b/hosts/iron/services/mail.nix new file mode 100644 index 0000000..315f46a --- /dev/null +++ b/hosts/iron/services/mail.nix @@ -0,0 +1,38 @@ +{ config, pkgs, ... }: +{ + sops.secrets.hetzner-api-key = { + sopsFile = ../secrets.yaml; + owner = "acme"; + }; + #sops.secrets."domain_key_jalr.de" = { + # sopsFile = ../secrets.yaml; + # owner = "rspamd"; + #}; + jalr = { + mailserver = { + enable = true; + fqdn = "hha.jalr.de"; + domains = [ + { + domain = "jalr.de"; + enableDKIM = true; + } + { + domain = "fablab-nea.de"; + enableDKIM = false; + } + ]; + users = import ../secrets/mail-users.nix; + messageSizeLimit = 50 * 1024 * 1024; + }; + }; + services.postfix.config = { + smtp_bind_address = "159.69.103.126"; + smtp_bind_address_enforce = true; + }; + + security.acme.certs."hha.jalr.de" = { + dnsProvider = "hetzner"; + credentialsFile = pkgs.writeText "certbotCredentialsFile" "HETZNER_API_KEY_FILE=${config.sops.secrets.hetzner-api-key.path}"; + }; +} diff --git a/hosts/iron/services/nginx.nix b/hosts/iron/services/nginx.nix index bb9d306..dd381d3 100644 --- a/hosts/iron/services/nginx.nix +++ b/hosts/iron/services/nginx.nix @@ -11,8 +11,4 @@ 80 443 ]; - security.acme = { - acceptTerms = true; - defaults.email = "mail@jalr.de"; - }; } diff --git a/modules/default.nix b/modules/default.nix index 5c2a08e..7ecebb6 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -20,6 +20,7 @@ ./kvm-switch-enable-screen.nix ./libvirt.nix ./localization.nix + ./mailserver ./mute-indicator.nix ./nix.nix ./obs.nix @@ -44,5 +45,12 @@ boot.tmp.cleanOnBoot = true; security.polkit.enable = true; + + security.acme = { + acceptTerms = true; + defaults = { + email = "security@jalr.de"; + }; + }; }; } diff --git a/modules/mailserver/default.nix b/modules/mailserver/default.nix new file mode 100644 index 0000000..353d14a --- /dev/null +++ b/modules/mailserver/default.nix @@ -0,0 +1,107 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.jalr.mailserver; +in +{ + options.jalr.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. + By default, an acme certificate with this name has to be present. + See `certDir` for more details. + ''; + example = "mail.example.com"; + }; + storageDir = mkOption { + type = path; + description = "Location of the storage directory for mails"; + default = "/var/vmail"; + }; + domains = mkOption { + type = listOf (submodule { + options = { + domain = mkOption { + type = str; + description = "Domain to serve"; + example = [ "example.com" "example.org" ]; + }; + enableDKIM = (lib.mkEnableOption "Enable DKIM signing") // { default = false; }; + DKIMSelector = mkOption { + type = str; + description = "DKIM selector to use when signing"; + default = "default"; + }; + }; + }); + description = "Domains of the mail server"; + }; + certDir = mkOption { + type = path; + description = "Directory with `fullchain.pem` and `key.pem` for the FQDN. Defaults to the ACME directory of the FQDN."; + default = config.security.acme.certs."${cfg.fqdn}".directory; + }; + 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 = '' + Argon2id hash of the user’s password. Please note that it will be + world-readable in the nix store. + ''; + example = "$argon2id$v=19$m=2097152,t=9,p=4$ycAnTa3lq5EAPTNJVpZ3+A$dIJ0CHVNn3vRUUso3IveHlrzTURoudrkxU92P5Q9/P4"; + }; + 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:/" + ]; + }; + messageSizeLimit = mkOption { + type = int; + description = '' + Message size limit, in bytes. + ''; + default = 10485760; + }; + }; + + imports = [ + ./dovecot.nix + ./postfix.nix + ./rspamd.nix + ./users.nix + ]; +} diff --git a/modules/mailserver/dovecot.nix b/modules/mailserver/dovecot.nix new file mode 100644 index 0000000..353474d --- /dev/null +++ b/modules/mailserver/dovecot.nix @@ -0,0 +1,162 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.jalr.mailserver; + postfixCfg = config.services.postfix; + + passdb = pkgs.writeText "dovecot-users" + (lib.concatMapStringsSep + "\n" + ({ address, passwordHash, ... }: "${address}:${passwordHash}") + cfg.users); +in +lib.mkIf cfg.enable { + services.dovecot2 = { + enable = true; + + modules = with pkgs; [ dovecot_pigeonhole ]; + + enableLmtp = true; + enablePAM = false; + + mailUser = "vmail"; + mailGroup = "vmail"; + mailLocation = "maildir:${cfg.storageDir}/%d/%n"; + + sslServerCert = "${cfg.certDir}/fullchain.pem"; + sslServerKey = "${cfg.certDir}/key.pem"; + + mailboxes = { + Archive = { specialUse = "Archive"; auto = "subscribe"; }; + Sent = { specialUse = "Sent"; auto = "subscribe"; }; + Drafts = { specialUse = "Drafts"; auto = "subscribe"; }; + Trash = { specialUse = "Trash"; auto = "subscribe"; }; + Spam = { specialUse = "Junk"; auto = "subscribe"; }; + }; + + sieveScripts = { + before = pkgs.writeText "spam.sieve" '' + require "fileinto"; + + if header :is "X-Spam" "Yes" { + fileinto "Spam"; + } + ''; + }; + + 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-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384 + ssl_prefer_server_ciphers = no + + protocol imap { + mail_plugins = $mail_plugins imap_sieve + } + + protocol lmtp { + mail_plugins = $mail_plugins sieve + } + + 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 = scheme=argon2id username_format=%u ${passdb} + auth_verbose = yes + } + + userdb { + driver = static + args = uid=vmail gid=vmail home=${cfg.storageDir}/%d/%n + } + + service auth { + vsz_limit = 4G # needed for argon2. + unix_listener auth { + mode = 0660 + user = ${postfixCfg.user} + group = ${postfixCfg.group} + } + } + + service auth-worker { + vsz_limit = 4G # needed for argon2. + } + + lda_mailbox_autosubscribe = yes + lda_mailbox_autocreate = yes + + plugin { + sieve_plugins = sieve_imapsieve sieve_extprograms + + ${lib.optionalString cfg.spam.enable '' + imapsieve_mailbox1_name = Spam + imapsieve_mailbox1_causes = COPY + imapsieve_mailbox1_before = file:/var/lib/dovecot/sieve/learn-spam.sieve + + imapsieve_mailbox2_name = * + imapsieve_mailbox2_from = Spam + imapsieve_mailbox2_causes = COPY + imapsieve_mailbox2_before = file:/var/lib/dovecot/sieve/learn-ham.sieve + sieve_pipe_bin_dir = ${pkgs.symlinkJoin { name = "sieve-pipe-bin-dir"; paths = with pkgs; [ rspamd ]; } }/bin + ''} + + sieve_global_extensions = +vnd.dovecot.pipe + } + ''; + }; + systemd.services.dovecot2 = { + wants = [ "acme-finished-${cfg.fqdn}.target" ]; + after = [ "acme-finished-${cfg.fqdn}.target" ]; + + preStart = lib.mkIf cfg.spam.enable + (lib.mkAfter + (lib.concatStrings + (lib.mapAttrsToList + (name: content: '' + cp ${pkgs.writeText name content} /var/lib/dovecot/sieve/${name} + '') + { + "learn-spam.sieve" = '' + require ["vnd.dovecot.pipe", "copy", "imapsieve"]; + pipe :copy "rspamc" ["learn_spam"]; + ''; + "learn-ham.sieve" = '' + require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + + if environment :matches "imap.mailbox" "*" { + set "mailbox" "''${1}"; + } + + if string "''${mailbox}" "Trash" { + stop; + } + + pipe :copy "rspamc" ["learn_ham"]; + ''; + }))); + }; + + networking.firewall.allowedTCPPorts = [ + 143 # IMAP + ]; + + security.acme.certs."${cfg.fqdn}".postRun = '' + if systemctl is-active dovecot2; then + systemctl --no-block reload dovecot2 + fi + ''; +} diff --git a/modules/mailserver/postfix.nix b/modules/mailserver/postfix.nix new file mode 100644 index 0000000..e5a8831 --- /dev/null +++ b/modules/mailserver/postfix.nix @@ -0,0 +1,162 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.jalr.mailserver; + + listToString = lib.concatStringsSep ","; + + # 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)); + + valiases = pkgs.writeText "valiases" aliasesString; + + submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" + (lib.concatMapStringsSep + "\n" + (regex: "${regex} IGNORE") + cfg.cleanHeaders); +in +lib.mkIf cfg.enable { + security.dhparams.params.postfix = { }; + services.postfix = { + enable = true; + + enableSubmission = true; # plain/STARTTLS (latter is forced in submissionOptions) + enableSubmissions = true; # submission with implicit TLS (TCP/465) + + hostname = cfg.fqdn; + networksStyle = "host"; + sslCert = "${cfg.certDir}/fullchain.pem"; + sslKey = "${cfg.certDir}/key.pem"; + + recipientDelimiter = "+"; + + mapFiles = { + inherit valiases; + }; + + config = { + # General + smtpd_banner = "${cfg.fqdn} ESMTP"; + 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 = toString cfg.messageSizeLimit; + + virtual_mailbox_domains = listToString (map (x: x.domain) 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 [ + "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; + }; + + # plain/STARTTLS (forced with smtpd_tls_security_level) + submissionOptions = { + 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, forward_secrecy" + + 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"; + }; + # implicit TLS + submissionsOptions = config.services.postfix.submissionOptions; + + masterConfig = { + submission-header-cleanup = { + private = false; + maxproc = 0; + command = "cleanup"; + args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ]; + }; + }; + }; + + networking.firewall.allowedTCPPorts = [ + 25 # SMTP + 587 # SMTP submission + ]; + + systemd.services.postfix = { + wants = [ "acme-finished-${cfg.fqdn}.target" ]; + requires = [ "dovecot2.service" ]; + after = [ "acme-finished-${cfg.fqdn}.target" "dovecot2.service" ]; + }; + + security.acme.certs."${cfg.fqdn}".postRun = '' + if systemctl is-active postfix; then + systemctl --no-block reload postfix + fi + ''; +} diff --git a/modules/mailserver/rspamd.nix b/modules/mailserver/rspamd.nix new file mode 100644 index 0000000..a7e0992 --- /dev/null +++ b/modules/mailserver/rspamd.nix @@ -0,0 +1,129 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.jalr.mailserver; + + # Generate DKIM keys: + # nix shell nixpkgs#rspamd -c \ + # rspamadm dkim_keygen -s default -d example.com -b 4096 -k /dev/shm/dkim.key > dkim.txt + + dkimEnabledDomains = (lib.filter (d: d.enableDKIM) cfg.domains); + dkimSignatureDir = pkgs.stdenvNoCC.mkDerivation { + name = "dkim-signatures"; + dontUnpack = true; + installPhase = "mkdir $out" + "\n" + lib.concatStringsSep "\n" ( + map + ( + x: "ln -s " + config.sops.secrets."dkim-keys/${x.domain}.${x.DKIMSelector}".path + " $out/${x.domain}.${x.DKIMSelector}.key" + ) + dkimEnabledDomains + ); + }; +in +{ + options.jalr.mailserver.spam = { + enable = (lib.mkEnableOption "spam filtering") // { default = true; }; + }; + + config = lib.mkIf (cfg.enable && cfg.spam.enable) { + sops.secrets = lib.attrsets.listToAttrs + ( + map + (x: + { + name = "dkim-keys/${x.domain}.${x.DKIMSelector}"; + value = { + owner = config.users.users.rspamd.name; + sopsFile = ../../hosts + "/${config.networking.hostName}/secrets.yaml"; + }; + } + ) + dkimEnabledDomains + ) // { + rspamd-worker-controller = { + owner = config.users.users.rspamd.name; + sopsFile = ../../hosts + "/${config.networking.hostName}/secrets.yaml"; + }; + }; + + services.rspamd = { + enable = true; + 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" ]; + }; + }; + locals = { + "dkim_signing.conf".text = '' + enabled = true; + path = "${dkimSignatureDir}/$domain.$selector.key" + selector = "default"; + allow_envfrom_empty = true; + allow_hdrfrom_mismatch = false; + allow_hdrfrom_multiple = false; + allow_username_mismatch = false; + sign_authenticated = true; + sign_local = true; + symbol = "DKIM_SIGNED"; + try_fallback = true; + use_domain = "header"; + use_esld = true; + use_redis = false; + key_prefix = "DKIM_KEYS"; + check_pubkey = true; + allow_pubkey_mismatch = false; + ''; + "logging.inc".text = '' + # starts at info, drops to notice once started up + level = "silent"; + #debug_modules = ["dkim_signing"]; + ''; + "milter_headers.conf".text = '' + extended_spam_headers = true; + ''; + "multimap.conf".text = '' + SENDER_BLOCKED { + type = "from"; + filter = "email:addr"; + map = "/var/lib/rspamd/blocked_senders.map"; + symbol = "SENDER_BLOCKED"; + description = "Sender’s address is manually blocked"; + prefilter = true; + action = "reject"; + score = 30.0; + } + SENDER_DOMAIN_BLOCKED { + type = "from"; + filter = "email:domain:tld"; + map = "/var/lib/rspamd/blocked_sender_domains.map"; + symbol = "SENDER_DOMAIN_BLOCKED"; + description = "Sender’s effective second level domain is manually blocked"; + score = 8.0; + } + ''; + "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 = { + vmOverCommit = true; + servers.rspamd = { + enable = true; + port = 6379; + }; + }; + }; +} diff --git a/modules/mailserver/users.nix b/modules/mailserver/users.nix new file mode 100644 index 0000000..33e2beb --- /dev/null +++ b/modules/mailserver/users.nix @@ -0,0 +1,12 @@ +{ config, lib, ... }: + +lib.mkIf config.jalr.mailserver.enable { + users.users.vmail = { + uid = 10000; + group = "vmail"; + home = config.jalr.mailserver.storageDir; + createHome = true; + }; + + users.groups.vmail.gid = 10000; +}