diff options
Diffstat (limited to 'hosts/surtr/email')
8 files changed, 438 insertions, 78 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 00182523..45619fb0 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 | |||
| @@ -28,12 +28,14 @@ class PolicyHandler(StreamRequestHandler): | |||
| 28 | 28 | ||
| 29 | allowed = False | 29 | allowed = False |
| 30 | user = None | 30 | user = None |
| 31 | relay_eligible = False | ||
| 31 | if self.args['sasl_username']: | 32 | if self.args['sasl_username']: |
| 32 | user = self.args['sasl_username'] | 33 | user = self.args['sasl_username'] |
| 33 | if self.args['ccert_subject']: | 34 | if self.args['ccert_subject']: |
| 34 | user = self.args['ccert_subject'] | 35 | user = self.args['ccert_subject'] |
| 36 | relay_eligible = True | ||
| 35 | 37 | ||
| 36 | if user: | 38 | if user and '@' in self.args['sender']: |
| 37 | with self.server.db_pool.connection() as conn: | 39 | with self.server.db_pool.connection() as conn: |
| 38 | local, domain = self.args['sender'].split(sep='@', maxsplit=1) | 40 | local, domain = self.args['sender'].split(sep='@', maxsplit=1) |
| 39 | extension = None | 41 | extension = None |
| @@ -44,10 +46,16 @@ class PolicyHandler(StreamRequestHandler): | |||
| 44 | 46 | ||
| 45 | with conn.cursor() as cur: | 47 | with conn.cursor() as cur: |
| 46 | cur.row_factory = namedtuple_row | 48 | 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) | 49 | |
| 48 | for record in cur: | 50 | if relay_eligible: |
| 49 | logger.debug('Received result: %s', record) | 51 | cur.execute('SELECT EXISTS(SELECT true FROM "mailbox" INNER JOIN "relay_access" ON "mailbox".id = "relay_access"."mailbox" WHERE "mailbox"."mailbox" = %(user)s AND ("domain" = %(domain)s OR %(domain)s ilike CONCAT(\'%%_.\', "domain"))) as "exists"', params = {'user': user, 'domain': domain}) |
| 50 | allowed = True | 52 | if (row := cur.fetchone()) is not None: |
| 53 | allowed = row.exists | ||
| 54 | |||
| 55 | if not allowed: | ||
| 56 | cur.execute('SELECT EXISTS(SELECT true 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) as "exists"', params = {'user': user, 'local': local, 'extension': extension if extension is not None else '', 'domain': domain}, prepare=True) | ||
| 57 | if (row := cur.fetchone()) is not None: | ||
| 58 | allowed = row.exists | ||
| 51 | 59 | ||
| 52 | action = '550 5.7.0 Sender address not authorized for current user' | 60 | action = '550 5.7.0 Sender address not authorized for current user' |
| 53 | if allowed: | 61 | if allowed: |
diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix index 4666d1d6..b4b2b5c8 100644 --- a/hosts/surtr/email/default.nix +++ b/hosts/surtr/email/default.nix | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | { config, pkgs, lib, flakeInputs, ... }: | 1 | { config, pkgs, lib, flake, flakeInputs, ... }: |
| 2 | 2 | ||
| 3 | with lib; | 3 | with lib; |
| 4 | 4 | ||
| @@ -15,7 +15,7 @@ let | |||
| 15 | 15 | ||
| 16 | for file in $out/pipe/bin/*; do | 16 | for file in $out/pipe/bin/*; do |
| 17 | wrapProgram $file \ | 17 | wrapProgram $file \ |
| 18 | --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin" | 18 | --set PATH "${makeBinPath (with pkgs; [coreutils rspamd])}" |
| 19 | done | 19 | done |
| 20 | ''; | 20 | ''; |
| 21 | }; | 21 | }; |
| @@ -33,12 +33,28 @@ let | |||
| 33 | }); | 33 | }); |
| 34 | }); | 34 | }); |
| 35 | }; | 35 | }; |
| 36 | internal-policy-server = | ||
| 37 | let | ||
| 38 | workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./internal-policy-server; }; | ||
| 39 | pythonSet = flake.lib.pythonSet { | ||
| 40 | inherit pkgs; | ||
| 41 | python = pkgs.python312; | ||
| 42 | overlay = workspace.mkPyprojectOverlay { | ||
| 43 | sourcePreference = "wheel"; | ||
| 44 | }; | ||
| 45 | }; | ||
| 46 | virtualEnv = pythonSet.mkVirtualEnv "internal-policy-server-env" workspace.deps.default; | ||
| 47 | in virtualEnv.overrideAttrs (oldAttrs: { | ||
| 48 | meta = (oldAttrs.meta or {}) // { | ||
| 49 | mainProgram = "internal-policy-server"; | ||
| 50 | }; | ||
| 51 | }); | ||
| 36 | 52 | ||
| 37 | nftables-nologin-script = pkgs.writeScript "nftables-mail-nologin" '' | 53 | nftables-nologin-script = pkgs.resholve.writeScript "nftables-mail-nologin" { |
| 38 | #!${pkgs.zsh}/bin/zsh | 54 | inputs = with pkgs; [inetutils nftables gnugrep findutils]; |
| 39 | 55 | interpreter = lib.getExe pkgs.zsh; | |
| 56 | } '' | ||
| 40 | set -e | 57 | set -e |
| 41 | export PATH="${lib.makeBinPath (with pkgs; [inetutils nftables])}:$PATH" | ||
| 42 | 58 | ||
| 43 | typeset -a as_sets mnt_bys route route6 | 59 | typeset -a as_sets mnt_bys route route6 |
| 44 | as_sets=(${lib.escapeShellArgs config.services.email.nologin.ASSets}) | 60 | as_sets=(${lib.escapeShellArgs config.services.email.nologin.ASSets}) |
| @@ -51,7 +67,7 @@ let | |||
| 51 | elif [[ "''${line}" =~ "^route6:\s+(.+)$" ]]; then | 67 | elif [[ "''${line}" =~ "^route6:\s+(.+)$" ]]; then |
| 52 | route6+=($match[1]) | 68 | route6+=($match[1]) |
| 53 | fi | 69 | fi |
| 54 | done < <(whois -h whois.radb.net "!i''${as_set},1" | egrep -o 'AS[0-9]+' | xargs -- whois -h whois.radb.net -- -i origin) | 70 | done < <(whois -h whois.radb.net "!i''${as_set},1" | grep -Eo 'AS[0-9]+' | xargs whois -h whois.radb.net -- -i origin) |
| 55 | done | 71 | done |
| 56 | for mnt_by in $mnt_bys; do | 72 | for mnt_by in $mnt_bys; do |
| 57 | while IFS=$'\n' read line; do | 73 | while IFS=$'\n' read line; do |
| @@ -108,19 +124,20 @@ in { | |||
| 108 | services.postfix = { | 124 | services.postfix = { |
| 109 | enable = true; | 125 | enable = true; |
| 110 | enableSmtp = false; | 126 | enableSmtp = false; |
| 111 | hostname = "surtr.yggdrasil.li"; | ||
| 112 | recipientDelimiter = ""; | ||
| 113 | setSendmail = true; | 127 | setSendmail = true; |
| 114 | postmasterAlias = ""; rootAlias = ""; extraAliases = ""; | 128 | postmasterAlias = ""; rootAlias = ""; extraAliases = ""; |
| 115 | destination = []; | 129 | settings.main = { |
| 116 | sslCert = "/run/credentials/postfix.service/surtr.yggdrasil.li.pem"; | 130 | recpipient_delimiter = ""; |
| 117 | sslKey = "/run/credentials/postfix.service/surtr.yggdrasil.li.key.pem"; | 131 | mydestination = []; |
| 118 | networks = []; | 132 | mynetworks = []; |
| 119 | config = let | 133 | myhostname = "surtr.yggdrasil.li"; |
| 120 | relay_ccert = "texthash:${pkgs.writeText "relay_ccert" ""}"; | 134 | |
| 121 | in { | ||
| 122 | smtpd_tls_security_level = "may"; | 135 | smtpd_tls_security_level = "may"; |
| 123 | 136 | ||
| 137 | smtpd_tls_chain_files = [ | ||
| 138 | "/run/credentials/postfix.service/surtr.yggdrasil.li.full.pem" | ||
| 139 | ]; | ||
| 140 | |||
| 124 | #the dh params | 141 | #the dh params |
| 125 | smtpd_tls_dh1024_param_file = toString config.security.dhparams.params."postfix-1024".path; | 142 | smtpd_tls_dh1024_param_file = toString config.security.dhparams.params."postfix-1024".path; |
| 126 | smtpd_tls_dh512_param_file = toString config.security.dhparams.params."postfix-512".path; | 143 | smtpd_tls_dh512_param_file = toString config.security.dhparams.params."postfix-512".path; |
| @@ -155,21 +172,14 @@ in { | |||
| 155 | 172 | ||
| 156 | smtp_tls_connection_reuse = true; | 173 | smtp_tls_connection_reuse = true; |
| 157 | 174 | ||
| 158 | tls_server_sni_maps = ''texthash:${pkgs.writeText "sni" ( | 175 | tls_server_sni_maps = "inline:{${concatMapStringsSep ", " (domain: "{ ${domain} = /run/credentials/postfix.service/${removePrefix "." domain}.full.pem }") (concatMap (domain: [domain "mailin.${domain}" "mailsub.${domain}" ".${domain}"]) emailDomains)}}"; |
| 159 | concatMapStringsSep "\n\n" (domain: | ||
| 160 | concatMapStringsSep "\n" (subdomain: "${subdomain} /run/credentials/postfix.service/${removePrefix "." subdomain}.full.pem") | ||
| 161 | [domain "mailin.${domain}" "mailsub.${domain}" ".${domain}"] | ||
| 162 | ) emailDomains | ||
| 163 | )}''; | ||
| 164 | 176 | ||
| 165 | smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix"; | 177 | smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix"; |
| 166 | 178 | ||
| 167 | local_recipient_maps = ""; | 179 | local_recipient_maps = ""; |
| 168 | 180 | ||
| 169 | # 10 GiB | 181 | message_size_limit = 10 * 1024 * 1024 * 1024; |
| 170 | message_size_limit = "10737418240"; | 182 | mailbox_size_limit = 10 * 1024 * 1024 * 1024; |
| 171 | # 10 GiB | ||
| 172 | mailbox_size_limit = "10737418240"; | ||
| 173 | 183 | ||
| 174 | smtpd_delay_reject = true; | 184 | smtpd_delay_reject = true; |
| 175 | smtpd_helo_required = true; | 185 | smtpd_helo_required = true; |
| @@ -184,12 +194,12 @@ in { | |||
| 184 | dbname = email | 194 | dbname = email |
| 185 | query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' | 195 | query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' |
| 186 | ''}" | 196 | ''}" |
| 187 | "check_ccert_access ${relay_ccert}" | ||
| 188 | "reject_non_fqdn_helo_hostname" | 197 | "reject_non_fqdn_helo_hostname" |
| 189 | "reject_invalid_helo_hostname" | 198 | "reject_invalid_helo_hostname" |
| 190 | "reject_unauth_destination" | 199 | "reject_unauth_destination" |
| 191 | "reject_unknown_recipient_domain" | 200 | "reject_unknown_recipient_domain" |
| 192 | "reject_unverified_recipient" | 201 | "reject_unverified_recipient" |
| 202 | "check_policy_service unix:/run/postfix-internal-policy.sock" | ||
| 193 | ]; | 203 | ]; |
| 194 | unverified_recipient_reject_code = "550"; | 204 | unverified_recipient_reject_code = "550"; |
| 195 | unverified_recipient_reject_reason = "Recipient address lookup failed"; | 205 | unverified_recipient_reject_reason = "Recipient address lookup failed"; |
| @@ -204,7 +214,6 @@ in { | |||
| 204 | address_verify_sender_ttl = "30045s"; | 214 | address_verify_sender_ttl = "30045s"; |
| 205 | 215 | ||
| 206 | smtpd_relay_restrictions = [ | 216 | smtpd_relay_restrictions = [ |
| 207 | "check_ccert_access ${relay_ccert}" | ||
| 208 | "reject_unauth_destination" | 217 | "reject_unauth_destination" |
| 209 | ]; | 218 | ]; |
| 210 | 219 | ||
| @@ -227,6 +236,37 @@ in { | |||
| 227 | bounce_queue_lifetime = "20m"; | 236 | bounce_queue_lifetime = "20m"; |
| 228 | delay_warning_time = "10m"; | 237 | delay_warning_time = "10m"; |
| 229 | 238 | ||
| 239 | failure_template_file = toString (pkgs.writeText "failure.cf" '' | ||
| 240 | Charset: us-ascii | ||
| 241 | From: Mail Delivery System <MAILER-DAEMON> | ||
| 242 | Subject: Undelivered Mail Returned to Sender | ||
| 243 | Postmaster-Subject: Postmaster Copy: Undelivered Mail | ||
| 244 | |||
| 245 | This is the mail system at host $myhostname. | ||
| 246 | |||
| 247 | I'm sorry to have to inform you that your message could not | ||
| 248 | be delivered to one or more recipients. It's attached below. | ||
| 249 | |||
| 250 | The mail system | ||
| 251 | ''); | ||
| 252 | delay_template_file = toString (pkgs.writeText "delay.cf" '' | ||
| 253 | Charset: us-ascii | ||
| 254 | From: Mail Delivery System <MAILER-DAEMON> | ||
| 255 | Subject: Delayed Mail (still being retried) | ||
| 256 | Postmaster-Subject: Postmaster Warning: Delayed Mail | ||
| 257 | |||
| 258 | This is the mail system at host $myhostname. | ||
| 259 | |||
| 260 | #################################################################### | ||
| 261 | # THIS IS A WARNING ONLY. YOU DO NOT NEED TO RESEND YOUR MESSAGE. # | ||
| 262 | #################################################################### | ||
| 263 | |||
| 264 | Your message could not be delivered for more than $delay_warning_time_minutes minute(s). | ||
| 265 | It will be retried until it is $maximal_queue_lifetime_minutes minute(s) old. | ||
| 266 | |||
| 267 | The mail system | ||
| 268 | ''); | ||
| 269 | |||
| 230 | smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" '' | 270 | smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" '' |
| 231 | # Allow DSN requests from local subnet only | 271 | # Allow DSN requests from local subnet only |
| 232 | 192.168.0.0/16 silent-discard | 272 | 192.168.0.0/16 silent-discard |
| @@ -251,13 +291,26 @@ in { | |||
| 251 | virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp"; | 291 | virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp"; |
| 252 | smtputf8_enable = false; | 292 | smtputf8_enable = false; |
| 253 | 293 | ||
| 254 | authorized_submit_users = "inline:{ root= postfwd= }"; | 294 | authorized_submit_users = "inline:{ root= postfwd= ${config.services.dovecot2.user}= }"; |
| 295 | authorized_flush_users = "inline:{ root= }"; | ||
| 296 | authorized_mailq_users = "inline:{ root= }"; | ||
| 255 | 297 | ||
| 256 | postscreen_access_list = ""; | 298 | postscreen_access_list = ""; |
| 257 | postscreen_denylist_action = "drop"; | 299 | postscreen_denylist_action = "drop"; |
| 258 | postscreen_greet_action = "enforce"; | 300 | postscreen_greet_action = "enforce"; |
| 301 | |||
| 302 | sender_bcc_maps = ''pgsql:${pkgs.writeText "sender_bcc_maps.cf" '' | ||
| 303 | hosts = postgresql:///email | ||
| 304 | dbname = email | ||
| 305 | query = SELECT value FROM sender_bcc_maps WHERE key = '%s' | ||
| 306 | ''}''; | ||
| 307 | recipient_bcc_maps = ''pgsql:${pkgs.writeText "recipient_bcc_maps.cf" '' | ||
| 308 | hosts = postgresql:///email | ||
| 309 | dbname = email | ||
| 310 | query = SELECT value FROM recipient_bcc_maps WHERE key = '%s' | ||
| 311 | ''}''; | ||
| 259 | }; | 312 | }; |
| 260 | masterConfig = { | 313 | settings.master = { |
| 261 | "465" = { | 314 | "465" = { |
| 262 | type = "inet"; | 315 | type = "inet"; |
| 263 | private = false; | 316 | private = false; |
| @@ -280,7 +333,7 @@ in { | |||
| 280 | hosts = postgresql:///email | 333 | hosts = postgresql:///email |
| 281 | dbname = email | 334 | dbname = email |
| 282 | 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')) | 335 | 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')) |
| 283 | ''},permit_tls_all_clientcerts,reject}'' | 336 | ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_tls_all_clientcerts,reject}'' |
| 284 | "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" | 337 | "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" |
| 285 | "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" | 338 | "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" |
| 286 | "-o" "unverified_sender_reject_code=550" | 339 | "-o" "unverified_sender_reject_code=550" |
| @@ -310,7 +363,7 @@ in { | |||
| 310 | hosts = postgresql:///email | 363 | hosts = postgresql:///email |
| 311 | dbname = email | 364 | dbname = email |
| 312 | 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')) | 365 | 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 | ''},permit_sasl_authenticated,reject}'' | 366 | ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_sasl_authenticated,reject}'' |
| 314 | "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject" | 367 | "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject" |
| 315 | "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" | 368 | "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" |
| 316 | "-o" "unverified_sender_reject_code=550" | 369 | "-o" "unverified_sender_reject_code=550" |
| @@ -325,7 +378,10 @@ in { | |||
| 325 | maxproc = 0; | 378 | maxproc = 0; |
| 326 | args = [ | 379 | args = [ |
| 327 | "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" '' | 380 | "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" '' |
| 381 | if /^Received: / | ||
| 382 | !/by surtr\.yggdrasil\.li/ STRIP | ||
| 328 | /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1 | 383 | /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1 |
| 384 | endif | ||
| 329 | ''}" | 385 | ''}" |
| 330 | ]; | 386 | ]; |
| 331 | }; | 387 | }; |
| @@ -364,6 +420,7 @@ in { | |||
| 364 | domains = [ "surtr.yggdrasil.li" ] ++ concatMap (domain: [".${domain}" domain]) emailDomains; | 420 | domains = [ "surtr.yggdrasil.li" ] ++ concatMap (domain: [".${domain}" domain]) emailDomains; |
| 365 | separator = "+"; | 421 | separator = "+"; |
| 366 | extraConfig = '' | 422 | extraConfig = '' |
| 423 | socketmap = unix:/run/postsrsd/postsrsd-socketmap.sock | ||
| 367 | milter = unix:/run/postsrsd/postsrsd-milter.sock | 424 | milter = unix:/run/postsrsd/postsrsd-milter.sock |
| 368 | ''; | 425 | ''; |
| 369 | }; | 426 | }; |
| @@ -372,7 +429,7 @@ in { | |||
| 372 | enable = true; | 429 | enable = true; |
| 373 | user = "postfix"; group = "postfix"; | 430 | user = "postfix"; group = "postfix"; |
| 374 | socket = "local:/run/opendkim/opendkim.sock"; | 431 | socket = "local:/run/opendkim/opendkim.sock"; |
| 375 | domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li"] ++ emailDomains)}''; | 432 | domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li" "yggdrasil.li" "141.li" "kleen.li" "synapse.li" "praseodym.org"] ++ emailDomains)}''; |
| 376 | selector = "surtr"; | 433 | selector = "surtr"; |
| 377 | configFile = builtins.toFile "opendkim.conf" '' | 434 | configFile = builtins.toFile "opendkim.conf" '' |
| 378 | Syslog true | 435 | Syslog true |
| @@ -476,7 +533,7 @@ in { | |||
| 476 | }; | 533 | }; |
| 477 | }; | 534 | }; |
| 478 | 535 | ||
| 479 | users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user "dovecot2" ]; | 536 | users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user config.services.dovecot2.user ]; |
| 480 | 537 | ||
| 481 | services.redis.servers.rspamd.enable = true; | 538 | services.redis.servers.rspamd.enable = true; |
| 482 | 539 | ||
| @@ -486,22 +543,22 @@ in { | |||
| 486 | services.dovecot2 = { | 543 | services.dovecot2 = { |
| 487 | enable = true; | 544 | enable = true; |
| 488 | enablePAM = false; | 545 | enablePAM = false; |
| 489 | sslServerCert = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.pem"; | 546 | sslServerCert = "/run/credentials/dovecot.service/surtr.yggdrasil.li.pem"; |
| 490 | sslServerKey = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.key.pem"; | 547 | sslServerKey = "/run/credentials/dovecot.service/surtr.yggdrasil.li.key.pem"; |
| 491 | sslCACert = toString ./ca/ca.crt; | 548 | sslCACert = toString ./ca/ca.crt; |
| 492 | mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u"; | 549 | mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u"; |
| 493 | mailPlugins.globally.enable = [ "fts" "fts_xapian" ]; | 550 | mailPlugins.globally.enable = [ "fts" "fts_xapian" ]; |
| 494 | protocols = [ "lmtp" "sieve" ]; | 551 | protocols = [ "lmtp" "sieve" ]; |
| 495 | sieve = { | 552 | sieve = { |
| 496 | extensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"]; | 553 | extensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation" "vacation-seconds" "vnd.dovecot.debug"]; |
| 497 | globalExtensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"]; | 554 | globalExtensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation" "vacation-seconds" "vnd.dovecot.debug"]; |
| 498 | }; | 555 | }; |
| 499 | extraConfig = let | 556 | extraConfig = let |
| 500 | dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" '' | 557 | dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" '' |
| 501 | driver = pgsql | 558 | driver = pgsql |
| 502 | connect = dbname=email | 559 | connect = dbname=email |
| 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' | 560 | 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, '${config.services.dovecot2.user}' as uid, '${config.services.dovecot2.group}' 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' | 561 | user_query = SELECT "user", quota_rule, '${config.services.dovecot2.user}' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' |
| 505 | iterate_query = SELECT "user" FROM imap_user | 562 | iterate_query = SELECT "user" FROM imap_user |
| 506 | ''; | 563 | ''; |
| 507 | in '' | 564 | in '' |
| @@ -509,16 +566,16 @@ in { | |||
| 509 | 566 | ||
| 510 | mail_plugins = $mail_plugins quota | 567 | mail_plugins = $mail_plugins quota |
| 511 | 568 | ||
| 512 | first_valid_uid = ${toString config.users.users.dovecot2.uid} | 569 | first_valid_uid = ${toString config.users.users.${config.services.dovecot2.user}.uid} |
| 513 | last_valid_uid = ${toString config.users.users.dovecot2.uid} | 570 | last_valid_uid = ${toString config.users.users.${config.services.dovecot2.user}.uid} |
| 514 | first_valid_gid = ${toString config.users.groups.dovecot2.gid} | 571 | first_valid_gid = ${toString config.users.groups.${config.services.dovecot2.group}.gid} |
| 515 | last_valid_gid = ${toString config.users.groups.dovecot2.gid} | 572 | last_valid_gid = ${toString config.users.groups.${config.services.dovecot2.group}.gid} |
| 516 | 573 | ||
| 517 | ${concatMapStringsSep "\n\n" (domain: | 574 | ${concatMapStringsSep "\n\n" (domain: |
| 518 | concatMapStringsSep "\n" (subdomain: '' | 575 | concatMapStringsSep "\n" (subdomain: '' |
| 519 | local_name ${subdomain} { | 576 | local_name ${subdomain} { |
| 520 | ssl_cert = </run/credentials/dovecot2.service/${subdomain}.pem | 577 | ssl_cert = </run/credentials/dovecot.service/${subdomain}.pem |
| 521 | ssl_key = </run/credentials/dovecot2.service/${subdomain}.key.pem | 578 | ssl_key = </run/credentials/dovecot.service/${subdomain}.key.pem |
| 522 | } | 579 | } |
| 523 | '') ["imap.${domain}" domain] | 580 | '') ["imap.${domain}" domain] |
| 524 | ) emailDomains} | 581 | ) emailDomains} |
| @@ -539,10 +596,10 @@ in { | |||
| 539 | auth_debug = yes | 596 | auth_debug = yes |
| 540 | 597 | ||
| 541 | service auth { | 598 | service auth { |
| 542 | user = dovecot2 | 599 | user = ${config.services.dovecot2.user} |
| 543 | } | 600 | } |
| 544 | service auth-worker { | 601 | service auth-worker { |
| 545 | user = dovecot2 | 602 | user = ${config.services.dovecot2.user} |
| 546 | } | 603 | } |
| 547 | 604 | ||
| 548 | userdb { | 605 | userdb { |
| @@ -563,7 +620,7 @@ in { | |||
| 563 | args = ${pkgs.writeText "dovecot-sql.conf" '' | 620 | args = ${pkgs.writeText "dovecot-sql.conf" '' |
| 564 | driver = pgsql | 621 | driver = pgsql |
| 565 | connect = dbname=email | 622 | connect = dbname=email |
| 566 | 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 | 623 | user_query = SELECT DISTINCT ON (extension IS NULL, local IS NULL) "user", quota_rule, '${config.services.dovecot2.user}' as uid, '${config.services.dovecot2.group}' 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 |
| 567 | ''} | 624 | ''} |
| 568 | 625 | ||
| 569 | skip = never | 626 | skip = never |
| @@ -633,7 +690,7 @@ in { | |||
| 633 | quota_status_success = DUNNO | 690 | quota_status_success = DUNNO |
| 634 | quota_status_nouser = DUNNO | 691 | quota_status_nouser = DUNNO |
| 635 | quota_grace = 10%% | 692 | quota_grace = 10%% |
| 636 | quota_max_mail_size = ${config.services.postfix.config.message_size_limit} | 693 | quota_max_mail_size = ${toString config.services.postfix.settings.main.message_size_limit} |
| 637 | quota_vsizes = yes | 694 | quota_vsizes = yes |
| 638 | } | 695 | } |
| 639 | 696 | ||
| @@ -671,7 +728,7 @@ in { | |||
| 671 | plugin { | 728 | plugin { |
| 672 | plugin = fts fts_xapian | 729 | plugin = fts fts_xapian |
| 673 | fts = xapian | 730 | fts = xapian |
| 674 | fts_xapian = partial=2 full=20 attachments=1 verbose=1 | 731 | fts_xapian = partial=3 full=20 attachments=1 verbose=1 |
| 675 | 732 | ||
| 676 | fts_autoindex = yes | 733 | fts_autoindex = yes |
| 677 | 734 | ||
| @@ -686,12 +743,12 @@ in { | |||
| 686 | 743 | ||
| 687 | systemd.services.dovecot-fts-xapian-optimize = { | 744 | systemd.services.dovecot-fts-xapian-optimize = { |
| 688 | description = "Optimize dovecot indices for fts_xapian"; | 745 | description = "Optimize dovecot indices for fts_xapian"; |
| 689 | requisite = [ "dovecot2.service" ]; | 746 | requisite = [ "dovecot.service" ]; |
| 690 | after = [ "dovecot2.service" ]; | 747 | after = [ "dovecot.service" ]; |
| 691 | startAt = "*-*-* 22:00:00 Europe/Berlin"; | 748 | startAt = "*-*-* 22:00:00 Europe/Berlin"; |
| 692 | serviceConfig = { | 749 | serviceConfig = { |
| 693 | Type = "oneshot"; | 750 | Type = "oneshot"; |
| 694 | ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A"; | 751 | ExecStart = "${getExe' pkgs.dovecot "doveadm"} fts optimize -A"; |
| 695 | PrivateDevices = true; | 752 | PrivateDevices = true; |
| 696 | PrivateNetwork = true; | 753 | PrivateNetwork = true; |
| 697 | ProtectKernelTunables = true; | 754 | ProtectKernelTunables = true; |
| @@ -752,31 +809,29 @@ in { | |||
| 752 | 809 | ||
| 753 | security.acme.rfc2136Domains = { | 810 | security.acme.rfc2136Domains = { |
| 754 | "surtr.yggdrasil.li" = { | 811 | "surtr.yggdrasil.li" = { |
| 755 | restartUnits = [ "postfix.service" "dovecot2.service" ]; | 812 | restartUnits = [ "postfix.service" "dovecot.service" ]; |
| 756 | }; | 813 | }; |
| 757 | } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains) | 814 | } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains) |
| 758 | // listToAttrs (concatMap (domain: [ | 815 | // listToAttrs (concatMap (domain: [ |
| 759 | (nameValuePair domain { restartUnits = ["postfix.service" "dovecot2.service"]; }) | 816 | (nameValuePair domain { restartUnits = ["postfix.service" "dovecot.service"]; }) |
| 760 | (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; }) | 817 | (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; }) |
| 761 | (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; }) | 818 | (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; }) |
| 762 | (nameValuePair "imap.${domain}" { restartUnits = ["dovecot2.service"]; }) | 819 | (nameValuePair "imap.${domain}" { restartUnits = ["dovecot.service"]; }) |
| 763 | (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; }) | 820 | (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; }) |
| 764 | ]) emailDomains); | 821 | ]) emailDomains); |
| 765 | 822 | ||
| 766 | systemd.services.postfix = { | 823 | systemd.services.postfix = { |
| 767 | serviceConfig.LoadCredential = [ | 824 | serviceConfig.LoadCredential = let |
| 768 | "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" | 825 | tlsCredential = domain: "${domain}.full.pem:${config.security.acme.certs.${domain}.directory}/full.pem"; |
| 769 | "surtr.yggdrasil.li.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/fullchain.pem" | 826 | in [ |
| 770 | ] ++ concatMap (domain: | 827 | (tlsCredential "surtr.yggdrasil.li") |
| 771 | map (subdomain: "${subdomain}.full.pem:${config.security.acme.certs.${subdomain}.directory}/full.pem") | 828 | ] ++ concatMap (domain: map tlsCredential [domain "mailin.${domain}" "mailsub.${domain}"]) emailDomains; |
| 772 | [domain "mailin.${domain}" "mailsub.${domain}"] | ||
| 773 | ) emailDomains; | ||
| 774 | }; | 829 | }; |
| 775 | 830 | ||
| 776 | systemd.services.dovecot2 = { | 831 | systemd.services.dovecot = { |
| 777 | preStart = '' | 832 | preStart = '' |
| 778 | for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do | 833 | for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do |
| 779 | ${pkgs.dovecot_pigeonhole}/bin/sievec $f | 834 | ${getExe' pkgs.dovecot_pigeonhole "sievec"} $f |
| 780 | done | 835 | done |
| 781 | ''; | 836 | ''; |
| 782 | 837 | ||
| @@ -843,15 +898,16 @@ in { | |||
| 843 | charset utf-8; | 898 | charset utf-8; |
| 844 | source_charset utf-8; | 899 | source_charset utf-8; |
| 845 | ''; | 900 | ''; |
| 846 | root = pkgs.runCommand "mta-sts.${domain}" {} '' | 901 | root = pkgs.writeTextFile { |
| 847 | mkdir -p $out/.well-known | 902 | name = "mta-sts.${domain}"; |
| 848 | cp ${pkgs.writeText "mta-sts.${domain}.txt" '' | 903 | destination = "/.well-known/mta-sts.txt"; |
| 904 | text = '' | ||
| 849 | version: STSv1 | 905 | version: STSv1 |
| 850 | mode: enforce | 906 | mode: enforce |
| 851 | max_age: 2419200 | 907 | max_age: 2419200 |
| 852 | mx: mailin.${domain} | 908 | mx: mailin.${domain} |
| 853 | ''} $out/.well-known/mta-sts.txt | 909 | ''; |
| 854 | ''; | 910 | }; |
| 855 | }; | 911 | }; |
| 856 | }) emailDomains); | 912 | }) emailDomains); |
| 857 | }; | 913 | }; |
| @@ -868,7 +924,7 @@ in { | |||
| 868 | systemd.services.spm = { | 924 | systemd.services.spm = { |
| 869 | serviceConfig = { | 925 | serviceConfig = { |
| 870 | Type = "notify"; | 926 | Type = "notify"; |
| 871 | ExecStart = "${pkgs.spm}/bin/spm-server"; | 927 | ExecStart = getExe' pkgs.spm "spm-server"; |
| 872 | User = "spm"; | 928 | User = "spm"; |
| 873 | Group = "spm"; | 929 | Group = "spm"; |
| 874 | 930 | ||
| @@ -926,7 +982,7 @@ in { | |||
| 926 | serviceConfig = { | 982 | serviceConfig = { |
| 927 | Type = "notify"; | 983 | Type = "notify"; |
| 928 | 984 | ||
| 929 | ExecStart = "${ccert-policy-server}/bin/ccert-policy-server"; | 985 | ExecStart = getExe' ccert-policy-server "ccert-policy-server"; |
| 930 | 986 | ||
| 931 | Environment = [ | 987 | Environment = [ |
| 932 | "PGDATABASE=email" | 988 | "PGDATABASE=email" |
| @@ -959,6 +1015,53 @@ in { | |||
| 959 | }; | 1015 | }; |
| 960 | users.groups."postfix-ccert-sender-policy" = {}; | 1016 | users.groups."postfix-ccert-sender-policy" = {}; |
| 961 | 1017 | ||
| 1018 | systemd.sockets."postfix-internal-policy" = { | ||
| 1019 | requiredBy = ["postfix.service"]; | ||
| 1020 | wants = ["postfix-internal-policy.service"]; | ||
| 1021 | socketConfig = { | ||
| 1022 | ListenStream = "/run/postfix-internal-policy.sock"; | ||
| 1023 | }; | ||
| 1024 | }; | ||
| 1025 | systemd.services."postfix-internal-policy" = { | ||
| 1026 | after = [ "postgresql.service" ]; | ||
| 1027 | bindsTo = [ "postgresql.service" ]; | ||
| 1028 | |||
| 1029 | serviceConfig = { | ||
| 1030 | Type = "notify"; | ||
| 1031 | |||
| 1032 | ExecStart = lib.getExe internal-policy-server; | ||
| 1033 | |||
| 1034 | Environment = [ | ||
| 1035 | "PGDATABASE=email" | ||
| 1036 | ]; | ||
| 1037 | |||
| 1038 | DynamicUser = false; | ||
| 1039 | User = "postfix-internal-policy"; | ||
| 1040 | Group = "postfix-internal-policy"; | ||
| 1041 | ProtectSystem = "strict"; | ||
| 1042 | SystemCallFilter = "@system-service"; | ||
| 1043 | NoNewPrivileges = true; | ||
| 1044 | ProtectKernelTunables = true; | ||
| 1045 | ProtectKernelModules = true; | ||
| 1046 | ProtectKernelLogs = true; | ||
| 1047 | ProtectControlGroups = true; | ||
| 1048 | MemoryDenyWriteExecute = true; | ||
| 1049 | RestrictSUIDSGID = true; | ||
| 1050 | KeyringMode = "private"; | ||
| 1051 | ProtectClock = true; | ||
| 1052 | RestrictRealtime = true; | ||
| 1053 | PrivateDevices = true; | ||
| 1054 | PrivateTmp = true; | ||
| 1055 | ProtectHostname = true; | ||
| 1056 | ReadWritePaths = ["/run/postgresql"]; | ||
| 1057 | }; | ||
| 1058 | }; | ||
| 1059 | users.users."postfix-internal-policy" = { | ||
| 1060 | isSystemUser = true; | ||
| 1061 | group = "postfix-internal-policy"; | ||
| 1062 | }; | ||
| 1063 | users.groups."postfix-internal-policy" = {}; | ||
| 1064 | |||
| 962 | services.postfwd = { | 1065 | services.postfwd = { |
| 963 | enable = true; | 1066 | enable = true; |
| 964 | cache = false; | 1067 | cache = false; |
diff --git a/hosts/surtr/email/internal-policy-server/.envrc b/hosts/surtr/email/internal-policy-server/.envrc new file mode 100644 index 00000000..2c909235 --- /dev/null +++ b/hosts/surtr/email/internal-policy-server/.envrc | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | use flake | ||
| 2 | |||
| 3 | [[ -d ".venv" ]] || ( uv venv && uv sync ) | ||
| 4 | . .venv/bin/activate | ||
diff --git a/hosts/surtr/email/internal-policy-server/.gitignore b/hosts/surtr/email/internal-policy-server/.gitignore new file mode 100644 index 00000000..4ccfae70 --- /dev/null +++ b/hosts/surtr/email/internal-policy-server/.gitignore | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | .venv | ||
| 2 | **/__pycache__ | ||
diff --git a/hosts/surtr/email/internal-policy-server/internal_policy_server/__init__.py b/hosts/surtr/email/internal-policy-server/internal_policy_server/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/hosts/surtr/email/internal-policy-server/internal_policy_server/__init__.py | |||
diff --git a/hosts/surtr/email/internal-policy-server/internal_policy_server/__main__.py b/hosts/surtr/email/internal-policy-server/internal_policy_server/__main__.py new file mode 100644 index 00000000..04f1a59a --- /dev/null +++ b/hosts/surtr/email/internal-policy-server/internal_policy_server/__main__.py | |||
| @@ -0,0 +1,106 @@ | |||
| 1 | from systemd.daemon import listen_fds | ||
| 2 | from sdnotify import SystemdNotifier | ||
| 3 | from socketserver import StreamRequestHandler, ThreadingMixIn | ||
| 4 | from systemd_socketserver import SystemdSocketServer | ||
| 5 | import sys | ||
| 6 | from threading import Thread | ||
| 7 | from psycopg_pool import ConnectionPool | ||
| 8 | from psycopg.rows import namedtuple_row | ||
| 9 | |||
| 10 | import logging | ||
| 11 | |||
| 12 | |||
| 13 | class PolicyHandler(StreamRequestHandler): | ||
| 14 | def handle(self): | ||
| 15 | logger.debug('Handling new connection...') | ||
| 16 | |||
| 17 | self.args = dict() | ||
| 18 | |||
| 19 | line = None | ||
| 20 | while line := self.rfile.readline().removesuffix(b'\n'): | ||
| 21 | if b'=' not in line: | ||
| 22 | break | ||
| 23 | |||
| 24 | key, val = line.split(sep=b'=', maxsplit=1) | ||
| 25 | self.args[key.decode()] = val.decode() | ||
| 26 | |||
| 27 | logger.info('Connection parameters: %s', self.args) | ||
| 28 | |||
| 29 | allowed = False | ||
| 30 | user = None | ||
| 31 | if self.args['sasl_username']: | ||
| 32 | user = self.args['sasl_username'] | ||
| 33 | if self.args['ccert_subject']: | ||
| 34 | user = self.args['ccert_subject'] | ||
| 35 | |||
| 36 | with self.server.db_pool.connection() as conn: | ||
| 37 | local, domain = self.args['recipient'].split(sep='@', maxsplit=1) | ||
| 38 | extension = None | ||
| 39 | if '+' in local: | ||
| 40 | local, extension = local.split(sep='+', maxsplit=1) | ||
| 41 | |||
| 42 | logger.debug('Parsed recipient address: %s', {'local': local, 'extension': extension, 'domain': domain}) | ||
| 43 | |||
| 44 | with conn.cursor() as cur: | ||
| 45 | cur.row_factory = namedtuple_row | ||
| 46 | cur.execute('SELECT id, internal FROM "mailbox_mapping" WHERE ("local" = %(local)s OR "local" IS NULL) AND ("extension" = %(extension)s OR "extension" IS NULL) AND "domain" = %(domain)s', params = {'local': local, 'extension': extension if extension is not None else '', 'domain': domain}, prepare = True) | ||
| 47 | if (row := cur.fetchone()) is not None: | ||
| 48 | if not row.internal: | ||
| 49 | logger.debug('Recipient mailbox is not internal') | ||
| 50 | allowed = True | ||
| 51 | elif user: | ||
| 52 | cur.execute('SELECT EXISTS(SELECT true FROM "mailbox_mapping_access" INNER JOIN "mailbox" ON "mailbox".id = "mailbox_mapping_access"."mailbox" WHERE mailbox_mapping = %(mailbox_mapping)s AND "mailbox"."mailbox" = %(user)s) as "exists"', params = { 'mailbox_mapping': row.id, 'user': user }, prepare = True) | ||
| 53 | if (row := cur.fetchone()) is not None: | ||
| 54 | allowed = row.exists | ||
| 55 | else: | ||
| 56 | logger.debug('Recipient is not local') | ||
| 57 | allowed = True | ||
| 58 | |||
| 59 | action = '550 5.7.0 Recipient mailbox mapping not authorized for current user' | ||
| 60 | if allowed: | ||
| 61 | action = 'DUNNO' | ||
| 62 | |||
| 63 | logger.info('Reached verdict: %s', {'allowed': allowed, 'action': action}) | ||
| 64 | self.wfile.write(f'action={action}\n\n'.encode()) | ||
| 65 | |||
| 66 | class ThreadedSystemdSocketServer(ThreadingMixIn, SystemdSocketServer): | ||
| 67 | def __init__(self, fd, RequestHandlerClass): | ||
| 68 | super().__init__(fd, RequestHandlerClass) | ||
| 69 | |||
| 70 | self.db_pool = ConnectionPool(min_size=1) | ||
| 71 | self.db_pool.wait() | ||
| 72 | |||
| 73 | def main(): | ||
| 74 | global logger | ||
| 75 | logger = logging.getLogger(__name__) | ||
| 76 | console_handler = logging.StreamHandler() | ||
| 77 | console_handler.setFormatter( logging.Formatter('[%(levelname)s](%(name)s): %(message)s') ) | ||
| 78 | if sys.stderr.isatty(): | ||
| 79 | console_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s](%(name)s): %(message)s') ) | ||
| 80 | logger.addHandler(console_handler) | ||
| 81 | logger.setLevel(logging.DEBUG) | ||
| 82 | |||
| 83 | # log uncaught exceptions | ||
| 84 | def log_exceptions(type, value, tb): | ||
| 85 | global logger | ||
| 86 | |||
| 87 | logger.error(value) | ||
| 88 | sys.__excepthook__(type, value, tb) # calls default excepthook | ||
| 89 | |||
| 90 | sys.excepthook = log_exceptions | ||
| 91 | |||
| 92 | fds = listen_fds() | ||
| 93 | servers = [ThreadedSystemdSocketServer(fd, PolicyHandler) for fd in fds] | ||
| 94 | |||
| 95 | if servers: | ||
| 96 | for server in servers: | ||
| 97 | Thread(name=f'Server for fd{server.fileno()}', target=server.serve_forever).start() | ||
| 98 | else: | ||
| 99 | return 2 | ||
| 100 | |||
| 101 | SystemdNotifier().notify('READY=1') | ||
| 102 | |||
| 103 | return 0 | ||
| 104 | |||
| 105 | if __name__ == '__main__': | ||
| 106 | sys.exit(main()) | ||
diff --git a/hosts/surtr/email/internal-policy-server/pyproject.toml b/hosts/surtr/email/internal-policy-server/pyproject.toml new file mode 100644 index 00000000..c697cd01 --- /dev/null +++ b/hosts/surtr/email/internal-policy-server/pyproject.toml | |||
| @@ -0,0 +1,18 @@ | |||
| 1 | [project] | ||
| 2 | name = "internal-policy-server" | ||
| 3 | version = "0.1.0" | ||
| 4 | requires-python = ">=3.12" | ||
| 5 | dependencies = [ | ||
| 6 | "psycopg>=3.2.9", | ||
| 7 | "psycopg-binary>=3.2.9", | ||
| 8 | "psycopg-pool>=3.2.6", | ||
| 9 | "sdnotify>=0.3.2", | ||
| 10 | "systemd-socketserver>=1.0", | ||
| 11 | ] | ||
| 12 | |||
| 13 | [project.scripts] | ||
| 14 | internal-policy-server = "internal_policy_server.__main__:main" | ||
| 15 | |||
| 16 | [build-system] | ||
| 17 | requires = ["hatchling"] | ||
| 18 | build-backend = "hatchling.build" | ||
diff --git a/hosts/surtr/email/internal-policy-server/uv.lock b/hosts/surtr/email/internal-policy-server/uv.lock new file mode 100644 index 00000000..f7a4e729 --- /dev/null +++ b/hosts/surtr/email/internal-policy-server/uv.lock | |||
| @@ -0,0 +1,119 @@ | |||
| 1 | version = 1 | ||
| 2 | revision = 2 | ||
| 3 | requires-python = ">=3.12" | ||
| 4 | |||
| 5 | [[package]] | ||
| 6 | name = "internal-policy-server" | ||
| 7 | version = "0.1.0" | ||
| 8 | source = { editable = "." } | ||
| 9 | dependencies = [ | ||
| 10 | { name = "psycopg" }, | ||
| 11 | { name = "psycopg-binary" }, | ||
| 12 | { name = "psycopg-pool" }, | ||
| 13 | { name = "sdnotify" }, | ||
| 14 | { name = "systemd-socketserver" }, | ||
| 15 | ] | ||
| 16 | |||
| 17 | [package.metadata] | ||
| 18 | requires-dist = [ | ||
| 19 | { name = "psycopg", specifier = ">=3.2.9" }, | ||
| 20 | { name = "psycopg-binary", specifier = ">=3.2.9" }, | ||
| 21 | { name = "psycopg-pool", specifier = ">=3.2.6" }, | ||
| 22 | { name = "sdnotify", specifier = ">=0.3.2" }, | ||
| 23 | { name = "systemd-socketserver", specifier = ">=1.0" }, | ||
| 24 | ] | ||
| 25 | |||
| 26 | [[package]] | ||
| 27 | name = "psycopg" | ||
| 28 | version = "3.2.9" | ||
| 29 | source = { registry = "https://pypi.org/simple" } | ||
| 30 | dependencies = [ | ||
| 31 | { name = "typing-extensions", marker = "python_full_version < '3.13'" }, | ||
| 32 | { name = "tzdata", marker = "sys_platform == 'win32'" }, | ||
| 33 | ] | ||
| 34 | sdist = { url = "https://files.pythonhosted.org/packages/27/4a/93a6ab570a8d1a4ad171a1f4256e205ce48d828781312c0bbaff36380ecb/psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700", size = 158122, upload-time = "2025-05-13T16:11:15.533Z" } | ||
| 35 | wheels = [ | ||
| 36 | { url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705, upload-time = "2025-05-13T16:06:26.584Z" }, | ||
| 37 | ] | ||
| 38 | |||
| 39 | [[package]] | ||
| 40 | name = "psycopg-binary" | ||
| 41 | version = "3.2.9" | ||
| 42 | source = { registry = "https://pypi.org/simple" } | ||
| 43 | wheels = [ | ||
| 44 | { url = "https://files.pythonhosted.org/packages/29/6f/ec9957e37a606cd7564412e03f41f1b3c3637a5be018d0849914cb06e674/psycopg_binary-3.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be7d650a434921a6b1ebe3fff324dbc2364393eb29d7672e638ce3e21076974e", size = 4022205, upload-time = "2025-05-13T16:07:48.195Z" }, | ||
| 45 | { url = "https://files.pythonhosted.org/packages/6b/ba/497b8bea72b20a862ac95a94386967b745a472d9ddc88bc3f32d5d5f0d43/psycopg_binary-3.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76b4722a529390683c0304501f238b365a46b1e5fb6b7249dbc0ad6fea51a0", size = 4083795, upload-time = "2025-05-13T16:07:50.917Z" }, | ||
| 46 | { url = "https://files.pythonhosted.org/packages/42/07/af9503e8e8bdad3911fd88e10e6a29240f9feaa99f57d6fac4a18b16f5a0/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a551e4683f1c307cfc3d9a05fec62c00a7264f320c9962a67a543e3ce0d8ff", size = 4655043, upload-time = "2025-05-13T16:07:54.857Z" }, | ||
| 47 | { url = "https://files.pythonhosted.org/packages/28/ed/aff8c9850df1648cc6a5cc7a381f11ee78d98a6b807edd4a5ae276ad60ad/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61d0a6ceed8f08c75a395bc28cb648a81cf8dee75ba4650093ad1a24a51c8724", size = 4477972, upload-time = "2025-05-13T16:07:57.925Z" }, | ||
| 48 | { url = "https://files.pythonhosted.org/packages/5c/bd/8e9d1b77ec1a632818fe2f457c3a65af83c68710c4c162d6866947d08cc5/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad280bbd409bf598683dda82232f5215cfc5f2b1bf0854e409b4d0c44a113b1d", size = 4737516, upload-time = "2025-05-13T16:08:01.616Z" }, | ||
| 49 | { url = "https://files.pythonhosted.org/packages/46/ec/222238f774cd5a0881f3f3b18fb86daceae89cc410f91ef6a9fb4556f236/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76eddaf7fef1d0994e3d536ad48aa75034663d3a07f6f7e3e601105ae73aeff6", size = 4436160, upload-time = "2025-05-13T16:08:04.278Z" }, | ||
| 50 | { url = "https://files.pythonhosted.org/packages/37/78/af5af2a1b296eeca54ea7592cd19284739a844974c9747e516707e7b3b39/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52e239cd66c4158e412318fbe028cd94b0ef21b0707f56dcb4bdc250ee58fd40", size = 3753518, upload-time = "2025-05-13T16:08:07.567Z" }, | ||
| 51 | { url = "https://files.pythonhosted.org/packages/ec/ac/8a3ed39ea069402e9e6e6a2f79d81a71879708b31cc3454283314994b1ae/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:08bf9d5eabba160dd4f6ad247cf12f229cc19d2458511cab2eb9647f42fa6795", size = 3313598, upload-time = "2025-05-13T16:08:09.999Z" }, | ||
| 52 | { url = "https://files.pythonhosted.org/packages/da/43/26549af068347c808fbfe5f07d2fa8cef747cfff7c695136172991d2378b/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1b2cf018168cad87580e67bdde38ff5e51511112f1ce6ce9a8336871f465c19a", size = 3407289, upload-time = "2025-05-13T16:08:12.66Z" }, | ||
| 53 | { url = "https://files.pythonhosted.org/packages/67/55/ea8d227c77df8e8aec880ded398316735add8fda5eb4ff5cc96fac11e964/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14f64d1ac6942ff089fc7e926440f7a5ced062e2ed0949d7d2d680dc5c00e2d4", size = 3472493, upload-time = "2025-05-13T16:08:15.672Z" }, | ||
| 54 | { url = "https://files.pythonhosted.org/packages/3c/02/6ff2a5bc53c3cd653d281666728e29121149179c73fddefb1e437024c192/psycopg_binary-3.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:7a838852e5afb6b4126f93eb409516a8c02a49b788f4df8b6469a40c2157fa21", size = 2927400, upload-time = "2025-05-13T16:08:18.652Z" }, | ||
| 55 | { url = "https://files.pythonhosted.org/packages/28/0b/f61ff4e9f23396aca674ed4d5c9a5b7323738021d5d72d36d8b865b3deaf/psycopg_binary-3.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:98bbe35b5ad24a782c7bf267596638d78aa0e87abc7837bdac5b2a2ab954179e", size = 4017127, upload-time = "2025-05-13T16:08:21.391Z" }, | ||
| 56 | { url = "https://files.pythonhosted.org/packages/bc/00/7e181fb1179fbfc24493738b61efd0453d4b70a0c4b12728e2b82db355fd/psycopg_binary-3.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72691a1615ebb42da8b636c5ca9f2b71f266be9e172f66209a361c175b7842c5", size = 4080322, upload-time = "2025-05-13T16:08:24.049Z" }, | ||
| 57 | { url = "https://files.pythonhosted.org/packages/58/fd/94fc267c1d1392c4211e54ccb943be96ea4032e761573cf1047951887494/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ab464bfba8c401f5536d5aa95f0ca1dd8257b5202eede04019b4415f491351", size = 4655097, upload-time = "2025-05-13T16:08:27.376Z" }, | ||
| 58 | { url = "https://files.pythonhosted.org/packages/41/17/31b3acf43de0b2ba83eac5878ff0dea5a608ca2a5c5dd48067999503a9de/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8aeefebe752f46e3c4b769e53f1d4ad71208fe1150975ef7662c22cca80fab", size = 4482114, upload-time = "2025-05-13T16:08:30.781Z" }, | ||
| 59 | { url = "https://files.pythonhosted.org/packages/85/78/b4d75e5fd5a85e17f2beb977abbba3389d11a4536b116205846b0e1cf744/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e4e4dd177a8665c9ce86bc9caae2ab3aa9360b7ce7ec01827ea1baea9ff748", size = 4737693, upload-time = "2025-05-13T16:08:34.625Z" }, | ||
| 60 | { url = "https://files.pythonhosted.org/packages/3b/95/7325a8550e3388b00b5e54f4ced5e7346b531eb4573bf054c3dbbfdc14fe/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc2915949e5c1ea27a851f7a472a7da7d0a40d679f0a31e42f1022f3c562e87", size = 4437423, upload-time = "2025-05-13T16:08:37.444Z" }, | ||
| 61 | { url = "https://files.pythonhosted.org/packages/1a/db/cef77d08e59910d483df4ee6da8af51c03bb597f500f1fe818f0f3b925d3/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1fa38a4687b14f517f049477178093c39c2a10fdcced21116f47c017516498f", size = 3758667, upload-time = "2025-05-13T16:08:40.116Z" }, | ||
| 62 | { url = "https://files.pythonhosted.org/packages/95/3e/252fcbffb47189aa84d723b54682e1bb6d05c8875fa50ce1ada914ae6e28/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5be8292d07a3ab828dc95b5ee6b69ca0a5b2e579a577b39671f4f5b47116dfd2", size = 3320576, upload-time = "2025-05-13T16:08:43.243Z" }, | ||
| 63 | { url = "https://files.pythonhosted.org/packages/1c/cd/9b5583936515d085a1bec32b45289ceb53b80d9ce1cea0fef4c782dc41a7/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9", size = 3411439, upload-time = "2025-05-13T16:08:47.321Z" }, | ||
| 64 | { url = "https://files.pythonhosted.org/packages/45/6b/6f1164ea1634c87956cdb6db759e0b8c5827f989ee3cdff0f5c70e8331f2/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b", size = 3477477, upload-time = "2025-05-13T16:08:51.166Z" }, | ||
| 65 | { url = "https://files.pythonhosted.org/packages/7b/1d/bf54cfec79377929da600c16114f0da77a5f1670f45e0c3af9fcd36879bc/psycopg_binary-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb", size = 2928009, upload-time = "2025-05-13T16:08:53.67Z" }, | ||
| 66 | ] | ||
| 67 | |||
| 68 | [[package]] | ||
| 69 | name = "psycopg-pool" | ||
| 70 | version = "3.2.6" | ||
| 71 | source = { registry = "https://pypi.org/simple" } | ||
| 72 | dependencies = [ | ||
| 73 | { name = "typing-extensions" }, | ||
| 74 | ] | ||
| 75 | sdist = { url = "https://files.pythonhosted.org/packages/cf/13/1e7850bb2c69a63267c3dbf37387d3f71a00fd0e2fa55c5db14d64ba1af4/psycopg_pool-3.2.6.tar.gz", hash = "sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5", size = 29770, upload-time = "2025-02-26T12:03:47.129Z" } | ||
| 76 | wheels = [ | ||
| 77 | { url = "https://files.pythonhosted.org/packages/47/fd/4feb52a55c1a4bd748f2acaed1903ab54a723c47f6d0242780f4d97104d4/psycopg_pool-3.2.6-py3-none-any.whl", hash = "sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7", size = 38252, upload-time = "2025-02-26T12:03:45.073Z" }, | ||
| 78 | ] | ||
| 79 | |||
| 80 | [[package]] | ||
| 81 | name = "sdnotify" | ||
| 82 | version = "0.3.2" | ||
| 83 | source = { registry = "https://pypi.org/simple" } | ||
| 84 | sdist = { url = "https://files.pythonhosted.org/packages/ce/d8/9fdc36b2a912bf78106de4b3f0de3891ff8f369e7a6f80be842b8b0b6bd5/sdnotify-0.3.2.tar.gz", hash = "sha256:73977fc746b36cc41184dd43c3fe81323e7b8b06c2bb0826c4f59a20c56bb9f1", size = 2459, upload-time = "2017-08-02T20:03:44.395Z" } | ||
| 85 | |||
| 86 | [[package]] | ||
| 87 | name = "systemd-python" | ||
| 88 | version = "235" | ||
| 89 | source = { registry = "https://pypi.org/simple" } | ||
| 90 | sdist = { url = "https://files.pythonhosted.org/packages/10/9e/ab4458e00367223bda2dd7ccf0849a72235ee3e29b36dce732685d9b7ad9/systemd-python-235.tar.gz", hash = "sha256:4e57f39797fd5d9e2d22b8806a252d7c0106c936039d1e71c8c6b8008e695c0a", size = 61677, upload-time = "2023-02-11T13:42:16.588Z" } | ||
| 91 | |||
| 92 | [[package]] | ||
| 93 | name = "systemd-socketserver" | ||
| 94 | version = "1.0" | ||
| 95 | source = { registry = "https://pypi.org/simple" } | ||
| 96 | dependencies = [ | ||
| 97 | { name = "systemd-python" }, | ||
| 98 | ] | ||
| 99 | wheels = [ | ||
| 100 | { url = "https://files.pythonhosted.org/packages/d8/4f/b28b7f08880120a26669b080ca74487c8c67e8b54dcb0467a8f0c9f38ed6/systemd_socketserver-1.0-py3-none-any.whl", hash = "sha256:987a8bfbf28d959e7c2966c742ad7bad482f05e121077defcf95bb38267db9a8", size = 3248, upload-time = "2020-04-26T05:26:40.661Z" }, | ||
| 101 | ] | ||
| 102 | |||
| 103 | [[package]] | ||
| 104 | name = "typing-extensions" | ||
| 105 | version = "4.13.2" | ||
| 106 | source = { registry = "https://pypi.org/simple" } | ||
| 107 | sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } | ||
| 108 | wheels = [ | ||
| 109 | { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, | ||
| 110 | ] | ||
| 111 | |||
| 112 | [[package]] | ||
| 113 | name = "tzdata" | ||
| 114 | version = "2025.2" | ||
| 115 | source = { registry = "https://pypi.org/simple" } | ||
| 116 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } | ||
| 117 | wheels = [ | ||
| 118 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, | ||
| 119 | ] | ||
