From aefbe2d5a0cd10daa555433b14230ede07225372 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Thu, 5 May 2022 19:13:41 +0200 Subject: surtr: ... --- hosts/surtr/email/default.nix | 225 +++++++++++++++++++++++++++++++++++++++++- hosts/surtr/postgresql.nix | 12 ++- hosts/surtr/ruleset.nft | 10 +- 3 files changed, 240 insertions(+), 7 deletions(-) diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix index 68690d55..5f4d9725 100644 --- a/hosts/surtr/email/default.nix +++ b/hosts/surtr/email/default.nix @@ -2,7 +2,14 @@ with lib; -{ +let + compileSieve = name: text: pkgs.runCommand name {} '' + mkdir $out + cp ${pkgs.writeText name '' + ''} $out/${name} + ${pkgs.dovecot_pigeonhole}/bin/sievec $out/${name} + ''}; +in { config = { services.postfix = { enable = true; @@ -40,6 +47,8 @@ with lib; #enable TLS logging to see the ciphers for outbound connections smtp_tls_loglevel = "1"; + smtpd_tls_received_header = true; + smtpd_tls_ask_ccert = true; smtpd_tls_CAfile = toString ./ca/ca.crt; @@ -88,8 +97,8 @@ with lib; authorized_verp_clients = "$mynetworks"; milter_default_action = "accept"; - smtpd_milters = [config.services.opendkim.socket]; - non_smtpd_milters = [config.services.opendkim.socket]; + 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 = ""; @@ -113,6 +122,19 @@ with lib; 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" '' + dbname = emails + table = virtual_mailbox_domain + select_field = domain + where_field = domain + ''}''; + virtual_mailbox_maps = ''pgsql:${pkgs.writeText "virtual_mailbox_maps.cf" '' + dbname = emails + table = virtual_mailbox_mapping + select_field = mailbox + where_field = lookup + ''}''; }; masterConfig = { smtps = { @@ -155,6 +177,187 @@ with lib; ''; }; + services.rspamd = { + enable = true; + workers = { + controller = {}; + 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; + + 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 ]; + + services.redis.servers.rspamd = { + enable = true; + vmOverCommit = true; + }; + + users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ]; + + services.dovecot2 = { + enable = true; + enableImap = false; + sslServerCert = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.pem"; + sslServerKey = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.key.pem"; + sslCACert = ./ca/ca.crt; + mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8"; + modules = with pkgs; [ dovecot_pigeonhole ]; + protocols = [ "imaps" "lmtp" "sieve" ]; + extraConfig = '' + mail_home = /var/lib/mail/%u + + local_name imap.bouncy.email { + ssl_cert = <$/run/credentials/dovecot2.service/imap.bouncy.email.pem + ssl_key = <$/run/credentials/dovecot2.service/imap.bouncy.email.key.pem + } + local_name bouncy.email { + ssl_cert = <$/run/credentials/dovecot2.service/bouncy.email.pem + ssl_key = <$/run/credentials/dovecot2.service/bouncy.email.key.pem + } + + ssl_require_crl = yes + ssl_verify_client_cert = yes + auth_ssl_username_from_cert = yes + auth_mechanisms = external + + userdb sql { + args = ${pkgs.writeText "dovecot-sql.conf" '' + driver = pgsql + connect = host=localhost dbname=email + user_query = SELECT mailbox AS user, quota_rule FROM mailbox WHERE mailbox = '%u' + ''} + default_fields = uid=dovecot2 gid=dovecot2 + } + + mail_plugins = $mail_plugins quota + mailbox_list_index = yes + postmaster_address = postmaster@yggdrasil.li + recipient_delimiter = + + + sieve_plugins = $sieve_plugins sieve_imapsieve + + service lmtp { + vsz_limit = 1G + + unix_listener /run/postfix/dovecot-lmtp { + mode = 0600 + user = postfix + group = postfix + } + } + + namespace inbox { + separator = / + inbox = yes + prefix = + } + + plugin { + quota = maildir + 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%% + } + + protocol imap { + mail_max_userip_connections = 50 + mail_plugins = $mail_plugins imap_quota imap_sieve + } + + service managesieve-login { + inet_listener sieve { + port = 4190 + ssl = yes + } + } + + plugin { + sieve_redirect_envelope_from = orig_recipient + sieve_before = ${compileSieve "tag-junk.sieve" '' + require ["imap4flags"]; + + if header :contains "X-Spam-Flag" "YES" { + addflag ["\\Junk"]; + } + ''} + } + ''; + }; + security.dhparams = { params = { "postfix-512".bits = 512; @@ -166,6 +369,7 @@ with lib; "bouncy.email" = {}; "mailin.bouncy.email" = {}; "mailsub.bouncy.email" = {}; + "imap.bouncy.email" = {}; "surtr.yggdrasil.li" = {}; }; @@ -178,5 +382,20 @@ with lib; "mailsub.bouncy.email.full.pem:${config.security.acme.certs."mailsub.bouncy.email".directory}/full.pem" ]; }; + + systemd.services.dovecot2 = { + 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" + "bouncy.email.key.pem:${config.security.acme.certs."bouncy.email".directory}/key.pem" + "bouncy.email.pem:${config.security.acme.certs."bouncy.email".directory}/fullchain.pem" + "imap.bouncy.email.key.pem:${config.security.acme.certs."imap.bouncy.email".directory}/key.pem" + "imap.bouncy.email.pem:${config.security.acme.certs."imap.bouncy.email".directory}/fullchain.pem" + ]; + + StateDirectory = "mail"; + }; + }; }; } diff --git a/hosts/surtr/postgresql.nix b/hosts/surtr/postgresql.nix index 7b3b8c74..d8f66fcc 100644 --- a/hosts/surtr/postgresql.nix +++ b/hosts/surtr/postgresql.nix @@ -30,17 +30,23 @@ in { BEGIN; SELECT _v.register_patch('000-base', null, null); - CREATE TABLE virtual_mailbox_mapping ( + CREATE TABLE mailbox ( + id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), + mailbox text NOT NULL CONSTRAINT mailbox_non_empty CHECK (mailbox <> '''), + quota_bytes bigint CONSTRAINT quota_bytes_positive CHECK (CASE WHEN quota_bytes IS NOT NULL THEN quota_bytes > 0 ELSE true), + quota_rule text GENERATED ALWAYS AS (CASE WHEN quota_bytes IS NULL THEN '*:ignore' ELSE '*:bytes=' || quota_bytes) STORED + ) + CREATE TABLE mailbox_mapping ( id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), local text CONSTRAINT local_non_empty CHECK (local IS DISTINCT FROM '''), domain text NOT NULL CONSTRAINT domain_non_empty CHECK (domain <> '''), - mailbox text NOT NULL CONSTRAINT mailbox_non_empty CHECK (mailbox <> '''), + mailbox uuid REFERENCES virtual_mailbox(id) CONSTRAINT local_domain_unique UNIQUE (local, domain) ); CREATE UNIQUE INDEX domain_unique ON virtual_mailbox_mapping (domain) WHERE local IS NULL; CREATE VIEW virtual_mailbox_domain (domain) AS SELECT DISTINCT domain FROM virtual_mailbox_mapping; - CREATE VIEW virtual_mailbox (mailbox) AS SELECT DISTINCT mailbox FROM virtual_mailbox_mapping; + CREATE VIEW virtual_mailbox_mapping (mailbox, lookup) AS SELECT mailbox.mailbox as mailbox, (CASE WHEN local IS NULL THEN ''' ELSE local) || '@' || domain AS lookup FROM mailbox_mapping INNER JOIN mailbox on mailbox.id = mailbox_mapping.mailbox; COMMIT; ''} ''; diff --git a/hosts/surtr/ruleset.nft b/hosts/surtr/ruleset.nft index f5ad5769..b9f83487 100644 --- a/hosts/surtr/ruleset.nft +++ b/hosts/surtr/ruleset.nft @@ -80,6 +80,8 @@ table inet filter { counter turn-rx {} counter smtp-rx {} counter submissions-rx {} + counter imap-rx {} + counter managesieve-rx {} counter established-rx {} @@ -105,6 +107,8 @@ table inet filter { counter turn-tx {} counter smtp-tx {} counter submissions-tx {} + counter imap-tx {} + counter managesieve-tx {} counter tx {} @@ -170,8 +174,10 @@ table inet filter { udp dport {3478, 5349} counter name stun-rx accept udp dport 49000-50000 counter name turn-rx accept - # tcp dport 25 counter name smtp-rx accept + tcp dport 25 counter name smtp-rx accept tcp dport 465 counter name submissions-rx accept + tcp dport 993 counter name imaps-rx accept + tcp dport 4190 counter name managesieve-rx accept ct state {established, related} counter name established-rx accept @@ -214,6 +220,8 @@ table inet filter { tcp sport 25 counter name smtp-tx accept tcp sport 465 counter name submissions-tx accept + tcp sport 993 counter name imaps-tx accept + tcp sport 4190 counter name managesieve-tx accept counter name tx -- cgit v1.2.3