summaryrefslogtreecommitdiff
path: root/hosts/surtr/email
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2024-08-08 14:36:50 +0200
committerGregor Kleen <gkleen@yggdrasil.li>2024-08-08 14:36:50 +0200
commitbe06f04babc12fb60366c24a22561c1d46895c80 (patch)
tree8df057a3605a7272cb048043be7593b5c944a67c /hosts/surtr/email
parenta6754d729f2d16cfdcb3570891c038a14718de1f (diff)
parentbc90ef66903e78713db1fd3a700785572b794cde (diff)
downloadnixos-be06f04babc12fb60366c24a22561c1d46895c80.tar
nixos-be06f04babc12fb60366c24a22561c1d46895c80.tar.gz
nixos-be06f04babc12fb60366c24a22561c1d46895c80.tar.bz2
nixos-be06f04babc12fb60366c24a22561c1d46895c80.tar.xz
nixos-be06f04babc12fb60366c24a22561c1d46895c80.zip
Merge commit 'bc90ef66' into flakes
Diffstat (limited to 'hosts/surtr/email')
-rw-r--r--hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py35
-rw-r--r--hosts/surtr/email/default.nix144
2 files changed, 151 insertions, 28 deletions
diff --git a/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py
index f481090c..00182523 100644
--- a/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py
+++ b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py
@@ -27,20 +27,27 @@ class PolicyHandler(StreamRequestHandler):
27 logger.info('Connection parameters: %s', self.args) 27 logger.info('Connection parameters: %s', self.args)
28 28
29 allowed = False 29 allowed = False
30 with self.server.db_pool.connection() as conn: 30 user = None
31 local, domain = self.args['sender'].split(sep='@', maxsplit=1) 31 if self.args['sasl_username']:
32 extension = None 32 user = self.args['sasl_username']
33 if '+' in local: 33 if self.args['ccert_subject']:
34 local, extension = local.split(sep='+', maxsplit=1) 34 user = self.args['ccert_subject']
35 35
36 logger.debug('Parsed address: %s', {'local': local, 'extension': extension, 'domain': domain}) 36 if user:
37 37 with self.server.db_pool.connection() as conn:
38 with conn.cursor() as cur: 38 local, domain = self.args['sender'].split(sep='@', maxsplit=1)
39 cur.row_factory = namedtuple_row 39 extension = None
40 cur.execute('SELECT "mailbox"."mailbox" as "user", "local", "extension", "domain" FROM "mailbox" INNER JOIN "mailbox_mapping" ON "mailbox".id = "mailbox_mapping"."mailbox" WHERE "mailbox"."mailbox" = %(user)s AND ("local" = %(local)s OR "local" IS NULL) AND ("extension" = %(extension)s OR "extension" IS NULL) AND "domain" = %(domain)s', params = {'user': self.args['ccert_subject'], 'local': local, 'extension': extension if extension is not None else '', 'domain': domain}, prepare=True) 40 if '+' in local:
41 for record in cur: 41 local, extension = local.split(sep='+', maxsplit=1)
42 logger.debug('Received result: %s', record) 42
43 allowed = True 43 logger.debug('Parsed address: %s', {'local': local, 'extension': extension, 'domain': domain})
44
45 with conn.cursor() as cur:
46 cur.row_factory = namedtuple_row
47 cur.execute('SELECT "mailbox"."mailbox" as "user", "local", "extension", "domain" FROM "mailbox" INNER JOIN "mailbox_mapping" ON "mailbox".id = "mailbox_mapping"."mailbox" WHERE "mailbox"."mailbox" = %(user)s AND ("local" = %(local)s OR "local" IS NULL) AND ("extension" = %(extension)s OR "extension" IS NULL) AND "domain" = %(domain)s', params = {'user': user, 'local': local, 'extension': extension if extension is not None else '', 'domain': domain}, prepare=True)
48 for record in cur:
49 logger.debug('Received result: %s', record)
50 allowed = True
44 51
45 action = '550 5.7.0 Sender address not authorized for current user' 52 action = '550 5.7.0 Sender address not authorized for current user'
46 if allowed: 53 if allowed:
diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix
index bb0f6e20..c10f611f 100644
--- a/hosts/surtr/email/default.nix
+++ b/hosts/surtr/email/default.nix
@@ -32,9 +32,63 @@ let
32 }); 32 });
33 }; 33 };
34 34
35 nftables-nologin-script = pkgs.writeScript "nftables-mail-nologin" ''
36 #!${pkgs.zsh}/bin/zsh
37
38 set -e
39 export PATH="${lib.makeBinPath (with pkgs; [inetutils nftables])}:$PATH"
40
41 typeset -a as_sets mnt_bys route route6
42 as_sets=(${lib.escapeShellArgs config.services.email.nologin.ASSets})
43 mnt_bys=(${lib.escapeShellArgs config.services.email.nologin.MNTBys})
44
45 for as_set in $as_sets; do
46 while IFS=$'\n' read line; do
47 if [[ "''${line}" =~ "^route:\s+(.+)$" ]]; then
48 route+=($match[1])
49 elif [[ "''${line}" =~ "^route6:\s+(.+)$" ]]; then
50 route6+=($match[1])
51 fi
52 done < <(whois -h whois.radb.net "!i''${as_set},1" | egrep -o 'AS[0-9]+' | xargs -- whois -h whois.radb.net -- -i origin)
53 done
54 for mnt_by in $mnt_bys; do
55 while IFS=$'\n' read line; do
56 if [[ "''${line}" =~ "^route:\s+(.+)$" ]]; then
57 route+=($match[1])
58 elif [[ "''${line}" =~ "^route6:\s+(.+)$" ]]; then
59 route6+=($match[1])
60 fi
61 done < <(whois -h whois.radb.net "!o''${mnt_by}")
62 done
63
64 printf -v elements4 '%s,' "''${route[@]}"
65 elements4=''${elements4%,}
66 printf -v elements6 '%s,' "''${route6[@]}"
67 elements6=''${elements6%,}
68 nft -f - <<EOF
69 flush set inet filter mail_nologin4
70 flush set inet filter mail_nologin6
71 add element inet filter mail_nologin4 {''${elements4}}
72 add element inet filter mail_nologin6 {''${elements6}}
73 EOF
74 '';
75
35 spmDomains = ["bouncy.email"]; 76 spmDomains = ["bouncy.email"];
36 emailDomains = spmDomains ++ ["kleen.consulting"]; 77 emailDomains = spmDomains ++ ["kleen.consulting"];
37in { 78in {
79 options = {
80 services.email.nologin = {
81 ASSets = mkOption {
82 type = types.listOf types.str;
83 default = [];
84 };
85 MNTBys = mkOption {
86 type = types.listOf types.str;
87 default = [];
88 };
89 };
90 };
91
38 config = { 92 config = {
39 nixpkgs.overlays = [ 93 nixpkgs.overlays = [
40 (final: prev: { 94 (final: prev: {
@@ -167,6 +221,7 @@ in {
167 maximal_backoff_time = "10m"; 221 maximal_backoff_time = "10m";
168 maximal_queue_lifetime = "100m"; 222 maximal_queue_lifetime = "100m";
169 bounce_queue_lifetime = "20m"; 223 bounce_queue_lifetime = "20m";
224 delay_warning_time = "10m";
170 225
171 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" '' 226 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" ''
172 # Allow DSN requests from local subnet only 227 # Allow DSN requests from local subnet only
@@ -204,17 +259,15 @@ in {
204 postscreen_greet_action = "enforce"; 259 postscreen_greet_action = "enforce";
205 }; 260 };
206 masterConfig = { 261 masterConfig = {
207 smtps = { 262 "465" = {
208 type = "inet"; 263 type = "inet";
209 private = false; 264 private = false;
210 command = "smtpd"; 265 command = "smtpd -v";
211 args = [ 266 args = [
212 "-o" "smtpd_tls_security_level=encrypt" 267 "-o" "smtpd_tls_security_level=encrypt"
213 "-o" "{smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2}" 268 "-o" "{smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2}"
214 "-o" "{smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2}" 269 "-o" "{smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1, !TLSv1.2}"
215 "-o" "smtpd_tls_mandatory_ciphers=high" 270 "-o" "smtpd_tls_mandatory_ciphers=high"
216 "-o" "smtpd_tls_dh1024_param_file=${toString config.security.dhparams.params."postfix-smtps-1024".path}"
217 "-o" "smtpd_tls_dh512_param_file=${toString config.security.dhparams.params."postfix-smtps-512".path}"
218 "-o" "{tls_eecdh_auto_curves = X25519 X448}" 271 "-o" "{tls_eecdh_auto_curves = X25519 X448}"
219 272
220 "-o" "smtpd_tls_wrappermode=yes" 273 "-o" "smtpd_tls_wrappermode=yes"
@@ -223,22 +276,52 @@ in {
223 "-o" "smtpd_tls_received_header=no" 276 "-o" "smtpd_tls_received_header=no"
224 "-o" "cleanup_service_name=subcleanup" 277 "-o" "cleanup_service_name=subcleanup"
225 "-o" "smtpd_client_restrictions=permit_tls_all_clientcerts,reject" 278 "-o" "smtpd_client_restrictions=permit_tls_all_clientcerts,reject"
226 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
227 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject"
228 "-o" "{smtpd_sender_restrictions = reject_unknown_sender_domain,reject_unverified_sender,check_policy_service unix:/run/postfix-ccert-sender-policy.sock}" 279 "-o" "{smtpd_sender_restrictions = reject_unknown_sender_domain,reject_unverified_sender,check_policy_service unix:/run/postfix-ccert-sender-policy.sock}"
280 "-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" ''
281 hosts = postgresql:///email
282 dbname = email
283 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'))
284 ''},permit_tls_all_clientcerts,reject}''
285 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject"
286 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
229 "-o" "unverified_sender_reject_code=550" 287 "-o" "unverified_sender_reject_code=550"
230 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}" 288 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}"
289 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li"
290 "-o" ''smtpd_milters=${config.services.opendkim.socket}''
291 ];
292 };
293 "466" = {
294 type = "inet";
295 private = false;
296 command = "smtpd -v";
297 args = [
298 "-o" "smtpd_tls_security_level=encrypt"
299
300 "-o" "smtpd_tls_wrappermode=yes"
301 "-o" "smtpd_tls_ask_ccert=no"
302 "-o" "smtpd_tls_req_ccert=no"
303 "-o" "smtpd_sasl_type=dovecot"
304 "-o" "smtpd_sasl_path=/run/dovecot-sasl"
305 "-o" "smtpd_sasl_auth_enable=yes"
306 "-o" "smtpd_tls_received_header=no"
307 "-o" "cleanup_service_name=subcleanup"
308 "-o" "smtpd_client_restrictions=permit_sasl_authenticated,reject"
309 "-o" "{smtpd_sender_restrictions = reject_unknown_sender_domain,reject_unverified_sender,check_policy_service unix:/run/postfix-ccert-sender-policy.sock}"
231 "-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" '' 310 "-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" ''
232 hosts = postgresql:///email 311 hosts = postgresql:///email
233 dbname = email 312 dbname = email
234 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')) 313 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'))
235 ''},permit_tls_all_clientcerts,reject}'' 314 ''},permit_sasl_authenticated,reject}''
315 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject"
316 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
317 "-o" "unverified_sender_reject_code=550"
318 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}"
236 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li" 319 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li"
237 "-o" ''smtpd_milters=${config.services.opendkim.socket}'' 320 "-o" ''smtpd_milters=${config.services.opendkim.socket}''
238 ]; 321 ];
239 }; 322 };
240 subcleanup = { 323 subcleanup = {
241 command = "cleanup"; 324 command = "cleanup -v";
242 private = false; 325 private = false;
243 maxproc = 0; 326 maxproc = 0;
244 args = [ 327 args = [
@@ -256,13 +339,13 @@ in {
256 smtp_pass = { 339 smtp_pass = {
257 name = "smtpd"; 340 name = "smtpd";
258 type = "pass"; 341 type = "pass";
259 command = "smtpd"; 342 command = "smtpd -v";
260 }; 343 };
261 postscreen = { 344 postscreen = {
262 name = "smtp"; 345 name = "smtp";
263 type = "inet"; 346 type = "inet";
264 private = false; 347 private = false;
265 command = "postscreen"; 348 command = "postscreen -v";
266 maxproc = 1; 349 maxproc = 1;
267 }; 350 };
268 smtp = {}; 351 smtp = {};
@@ -417,7 +500,7 @@ in {
417 dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" '' 500 dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" ''
418 driver = pgsql 501 driver = pgsql
419 connect = dbname=email 502 connect = dbname=email
420 password_query = SELECT NULL as password, 'Y' as nopassword, "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' 503 password_query = SELECT (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN NULL ELSE "password" END) as password, (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN true WHEN password IS NULL THEN true ELSE NULL END) as nopassword, "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n'
421 user_query = SELECT "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' 504 user_query = SELECT "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n'
422 iterate_query = SELECT "user" FROM imap_user 505 iterate_query = SELECT "user" FROM imap_user
423 ''; 506 '';
@@ -449,7 +532,7 @@ in {
449 532
450 auth_ssl_username_from_cert = yes 533 auth_ssl_username_from_cert = yes
451 ssl_cert_username_field = commonName 534 ssl_cert_username_field = commonName
452 auth_mechanisms = external 535 auth_mechanisms = plain login external
453 536
454 auth_verbose = yes 537 auth_verbose = yes
455 verbose_ssl = yes 538 verbose_ssl = yes
@@ -505,6 +588,15 @@ in {
505 group = postfix 588 group = postfix
506 } 589 }
507 } 590 }
591 service auth {
592 vsz_limit = 2G
593
594 unix_listener /run/dovecot-sasl {
595 mode = 0600
596 user = postfix
597 group = postfix
598 }
599 }
508 600
509 namespace inbox { 601 namespace inbox {
510 separator = / 602 separator = /
@@ -869,9 +961,13 @@ in {
869 961
870 services.postfwd = { 962 services.postfwd = {
871 enable = true; 963 enable = true;
964 cache = false;
872 rules = '' 965 rules = ''
873 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)) 966 id=RCPT_SASL01; protocol_state=DATA; protocol_state=END-OF-MESSAGE; sasl_username!=; action=rcpt(sasl_username/100/3600/set(HIT_RATELIMIT=1,HIT_RATECOUNT=$$ratecount,HIT_RATELIMIT_LIMIT=100,HIT_RATELIMIT_INTERVAL=3600))
874 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)) 967 id=RCPT_SASL02; protocol_state=DATA; protocol_state=END-OF-MESSAGE; sasl_username!=; action=rcpt(sasl_username/1000/86400/set(HIT_RATELIMIT=1,HIT_RATECOUNT=$$ratecount,HIT_RATELIMIT_LIMIT=1000,HIT_RATELIMIT_INTERVAL=86400))
968
969 id=RCPT_CCERT01; protocol_state=DATA; protocol_state=END-OF-MESSAGE; ccert_subject!=; action=rcpt(ccert_subject/100/3600/set(HIT_RATELIMIT=1,HIT_RATECOUNT=$$ratecount,HIT_RATELIMIT_LIMIT=100,HIT_RATELIMIT_INTERVAL=3600))
970 id=RCPT_CCERT02; protocol_state=DATA; protocol_state=END-OF-MESSAGE; ccert_subject!=; action=rcpt(ccert_subject/1000/86400/set(HIT_RATELIMIT=1,HIT_RATECOUNT=$$ratecount,HIT_RATELIMIT_LIMIT=1000,HIT_RATELIMIT_INTERVAL=86400))
875 971
876 id=JUMP_REJECT_RL; HIT_RATELIMIT=="1"; action=jump(REJECT_RL) 972 id=JUMP_REJECT_RL; HIT_RATELIMIT=="1"; action=jump(REJECT_RL)
877 973
@@ -880,5 +976,25 @@ in {
880 id=REJECT_RL; action=450 4.7.1 Exceeding maximum of $$HIT_RATELIMIT_LIMIT recipients per $$HIT_RATELIMIT_INTERVAL seconds [$$HIT_RATECOUNT] 976 id=REJECT_RL; action=450 4.7.1 Exceeding maximum of $$HIT_RATELIMIT_LIMIT recipients per $$HIT_RATELIMIT_INTERVAL seconds [$$HIT_RATECOUNT]
881 ''; 977 '';
882 }; 978 };
979
980 services.email.nologin.MNTBys = ["MICROSOFT-MAINT"];
981 systemd.services.nftables.serviceConfig = {
982 ExecStart = lib.mkAfter [ nftables-nologin-script ];
983 ExecReload = lib.mkAfter [ nftables-nologin-script ];
984 };
985 systemd.services."nftables-mail-nologin" = {
986 serviceConfig = {
987 Type = "oneshot";
988 ExecStart = nftables-nologin-script;
989 };
990 };
991 systemd.timers."nftables-mail-nologin" = {
992 wantedBy = [ "nftables.service" ];
993
994 timerConfig = {
995 OnActiveSec = "20h";
996 RandomizedDelaySec = "8h";
997 };
998 };
883 }; 999 };
884} 1000}