diff options
Diffstat (limited to 'hosts/surtr')
| -rw-r--r-- | hosts/surtr/email/ccert-policy-server/ccert_policy_server/__init__.py | 0 | ||||
| -rw-r--r-- | hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py | 92 | ||||
| -rw-r--r-- | hosts/surtr/email/ccert-policy-server/setup.py | 12 | ||||
| -rw-r--r-- | hosts/surtr/email/default.nix | 88 | ||||
| -rw-r--r-- | hosts/surtr/postgresql.nix | 11 |
5 files changed, 195 insertions, 8 deletions
diff --git a/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__init__.py b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__init__.py | |||
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 new file mode 100644 index 00000000..f481090c --- /dev/null +++ b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py | |||
| @@ -0,0 +1,92 @@ | |||
| 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 | with self.server.db_pool.connection() as conn: | ||
| 31 | local, domain = self.args['sender'].split(sep='@', maxsplit=1) | ||
| 32 | extension = None | ||
| 33 | if '+' in local: | ||
| 34 | local, extension = local.split(sep='+', maxsplit=1) | ||
| 35 | |||
| 36 | logger.debug('Parsed address: %s', {'local': local, 'extension': extension, 'domain': domain}) | ||
| 37 | |||
| 38 | with conn.cursor() as cur: | ||
| 39 | cur.row_factory = namedtuple_row | ||
| 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) | ||
| 41 | for record in cur: | ||
| 42 | logger.debug('Received result: %s', record) | ||
| 43 | allowed = True | ||
| 44 | |||
| 45 | action = '550 5.7.0 Sender address not authorized for current user' | ||
| 46 | if allowed: | ||
| 47 | action = 'DUNNO' | ||
| 48 | |||
| 49 | logger.info('Reached verdict: %s', {'allowed': allowed, 'action': action}) | ||
| 50 | self.wfile.write(f'action={action}\n\n'.encode()) | ||
| 51 | |||
| 52 | class ThreadedSystemdSocketServer(ThreadingMixIn, SystemdSocketServer): | ||
| 53 | def __init__(self, fd, RequestHandlerClass): | ||
| 54 | super().__init__(fd, RequestHandlerClass) | ||
| 55 | |||
| 56 | self.db_pool = ConnectionPool(min_size=1) | ||
| 57 | self.db_pool.wait() | ||
| 58 | |||
| 59 | def main(): | ||
| 60 | global logger | ||
| 61 | logger = logging.getLogger(__name__) | ||
| 62 | console_handler = logging.StreamHandler() | ||
| 63 | console_handler.setFormatter( logging.Formatter('[%(levelname)s](%(name)s): %(message)s') ) | ||
| 64 | if sys.stderr.isatty(): | ||
| 65 | console_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s](%(name)s): %(message)s') ) | ||
| 66 | logger.addHandler(console_handler) | ||
| 67 | logger.setLevel(logging.DEBUG) | ||
| 68 | |||
| 69 | # log uncaught exceptions | ||
| 70 | def log_exceptions(type, value, tb): | ||
| 71 | global logger | ||
| 72 | |||
| 73 | logger.error(value) | ||
| 74 | sys.__excepthook__(type, value, tb) # calls default excepthook | ||
| 75 | |||
| 76 | sys.excepthook = log_exceptions | ||
| 77 | |||
| 78 | fds = listen_fds() | ||
| 79 | servers = [ThreadedSystemdSocketServer(fd, PolicyHandler) for fd in fds] | ||
| 80 | |||
| 81 | if servers: | ||
| 82 | for server in servers: | ||
| 83 | Thread(name=f'Server for fd{server.fileno()}', target=server.serve_forever).start() | ||
| 84 | else: | ||
| 85 | return 2 | ||
| 86 | |||
| 87 | SystemdNotifier().notify('READY=1') | ||
| 88 | |||
| 89 | return 0 | ||
| 90 | |||
| 91 | if __name__ == '__main__': | ||
| 92 | sys.exit(main()) | ||
diff --git a/hosts/surtr/email/ccert-policy-server/setup.py b/hosts/surtr/email/ccert-policy-server/setup.py new file mode 100644 index 00000000..d8eb415a --- /dev/null +++ b/hosts/surtr/email/ccert-policy-server/setup.py | |||
| @@ -0,0 +1,12 @@ | |||
| 1 | from setuptools import setup, find_packages | ||
| 2 | |||
| 3 | setup( | ||
| 4 | name = 'ccert-policy-server', | ||
| 5 | version = '0.0.0', | ||
| 6 | packages = ['ccert_policy_server'], | ||
| 7 | entry_points = { | ||
| 8 | 'console_scripts': [ | ||
| 9 | 'ccert-policy-server=ccert_policy_server.__main__:main' | ||
| 10 | ], | ||
| 11 | }, | ||
| 12 | ) | ||
diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix index 83bf02f5..9cfba1f1 100644 --- a/hosts/surtr/email/default.nix +++ b/hosts/surtr/email/default.nix | |||
| @@ -1,4 +1,4 @@ | |||
| 1 | { config, pkgs, lib, ... }: | 1 | { config, pkgs, lib, flakeInputs, ... }: |
| 2 | 2 | ||
| 3 | with lib; | 3 | with lib; |
| 4 | 4 | ||
| @@ -20,6 +20,27 @@ let | |||
| 20 | ''; | 20 | ''; |
| 21 | }; | 21 | }; |
| 22 | 22 | ||
| 23 | ccert-policy-server = flakeInputs.mach-nix.lib.${config.nixpkgs.system}.buildPythonPackage { | ||
| 24 | src = ./ccert-policy-server; | ||
| 25 | pname = "ccert-policy-server"; | ||
| 26 | version = "0.0.0"; | ||
| 27 | |||
| 28 | python = "python39"; | ||
| 29 | ignoreDataOutdated = true; | ||
| 30 | |||
| 31 | requirements = '' | ||
| 32 | sdnotify | ||
| 33 | systemd-socketserver | ||
| 34 | psycopg >=3.0.0 | ||
| 35 | psycopg-pool >=3.0.0 | ||
| 36 | psycopg-binary >=3.0.0 | ||
| 37 | ''; | ||
| 38 | |||
| 39 | overridesPre = [ | ||
| 40 | (self: super: { systemd-python = super.systemd.overrideAttrs (oldAttrs: { pname = "systemd-python"; }); }) | ||
| 41 | ]; | ||
| 42 | }; | ||
| 43 | |||
| 23 | spmDomains = ["bouncy.email"]; | 44 | spmDomains = ["bouncy.email"]; |
| 24 | in { | 45 | in { |
| 25 | config = { | 46 | config = { |
| @@ -35,7 +56,7 @@ in { | |||
| 35 | }; | 56 | }; |
| 36 | }) | 57 | }) |
| 37 | ]; | 58 | ]; |
| 38 | 59 | ||
| 39 | services.postfix = { | 60 | services.postfix = { |
| 40 | enable = true; | 61 | enable = true; |
| 41 | hostname = "surtr.yggdrasil.li"; | 62 | hostname = "surtr.yggdrasil.li"; |
| @@ -187,8 +208,9 @@ in { | |||
| 187 | "-o" "smtpd_tls_ask_ccert=yes" | 208 | "-o" "smtpd_tls_ask_ccert=yes" |
| 188 | "-o" "smtpd_tls_req_ccert=yes" | 209 | "-o" "smtpd_tls_req_ccert=yes" |
| 189 | "-o" "smtpd_client_restrictions=permit_tls_all_clientcerts,reject" | 210 | "-o" "smtpd_client_restrictions=permit_tls_all_clientcerts,reject" |
| 211 | "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" | ||
| 190 | "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" | 212 | "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" |
| 191 | "-o" "smtpd_sender_restrictions=reject_unknown_sender_domain,reject_unverified_sender" | 213 | "-o" "{smtpd_sender_restrictions = reject_unknown_sender_domain,reject_unverified_sender,check_policy_service unix:/run/postfix-ccert-sender-policy.sock}" |
| 192 | "-o" "unverified_sender_reject_code=550" | 214 | "-o" "unverified_sender_reject_code=550" |
| 193 | "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}" | 215 | "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}" |
| 194 | "-o" "smtpd_recipient_restrictions=reject_unauth_pipelining,reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_tls_all_clientcerts,reject" | 216 | "-o" "smtpd_recipient_restrictions=reject_unauth_pipelining,reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_tls_all_clientcerts,reject" |
| @@ -415,7 +437,7 @@ in { | |||
| 415 | mail_plugins = $mail_plugins quota | 437 | mail_plugins = $mail_plugins quota |
| 416 | mailbox_list_index = yes | 438 | mailbox_list_index = yes |
| 417 | postmaster_address = postmaster@yggdrasil.li | 439 | postmaster_address = postmaster@yggdrasil.li |
| 418 | recipient_delimiter = | 440 | recipient_delimiter = |
| 419 | auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-+_@ | 441 | auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-+_@ |
| 420 | 442 | ||
| 421 | service lmtp { | 443 | service lmtp { |
| @@ -431,7 +453,7 @@ in { | |||
| 431 | namespace inbox { | 453 | namespace inbox { |
| 432 | separator = / | 454 | separator = / |
| 433 | inbox = yes | 455 | inbox = yes |
| 434 | prefix = | 456 | prefix = |
| 435 | 457 | ||
| 436 | mailbox Trash { | 458 | mailbox Trash { |
| 437 | auto = no | 459 | auto = no |
| @@ -602,7 +624,7 @@ in { | |||
| 602 | ${pkgs.dovecot_pigeonhole}/bin/sievec $f | 624 | ${pkgs.dovecot_pigeonhole}/bin/sievec $f |
| 603 | done | 625 | done |
| 604 | ''; | 626 | ''; |
| 605 | 627 | ||
| 606 | serviceConfig = { | 628 | serviceConfig = { |
| 607 | LoadCredential = [ | 629 | LoadCredential = [ |
| 608 | "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" | 630 | "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" |
| @@ -703,7 +725,7 @@ in { | |||
| 703 | }; | 725 | }; |
| 704 | systemd.sockets.spm = { | 726 | systemd.sockets.spm = { |
| 705 | wantedBy = [ "nginx.service" ]; | 727 | wantedBy = [ "nginx.service" ]; |
| 706 | 728 | ||
| 707 | socketConfig = { | 729 | socketConfig = { |
| 708 | ListenStream = "/run/spm/server.sock"; | 730 | ListenStream = "/run/spm/server.sock"; |
| 709 | SocketUser = "spm"; | 731 | SocketUser = "spm"; |
| @@ -730,5 +752,57 @@ in { | |||
| 730 | enable = true; | 752 | enable = true; |
| 731 | loglevel = "debug"; | 753 | loglevel = "debug"; |
| 732 | }; | 754 | }; |
| 755 | |||
| 756 | systemd.sockets."postfix-ccert-sender-policy" = { | ||
| 757 | requiredBy = ["postfix.service"]; | ||
| 758 | wants = ["postfix-ccert-sender-policy.service"]; | ||
| 759 | socketConfig = { | ||
| 760 | ListenStream = "/run/postfix-ccert-sender-policy.sock"; | ||
| 761 | }; | ||
| 762 | }; | ||
| 763 | systemd.services."postfix-ccert-sender-policy" = { | ||
| 764 | serviceConfig = { | ||
| 765 | Type = "notify"; | ||
| 766 | |||
| 767 | ExecStart = "${ccert-policy-server}/bin/ccert-policy-server"; | ||
| 768 | |||
| 769 | Environment = [ | ||
| 770 | "PGDATABASE=email" | ||
| 771 | ]; | ||
| 772 | |||
| 773 | DynamicUser = false; | ||
| 774 | User = "postfix-ccert-sender-policy"; | ||
| 775 | Group = "postfix-ccert-sender-policy"; | ||
| 776 | ProtectSystem = "strict"; | ||
| 777 | SystemCallFilter = "@system-service"; | ||
| 778 | NoNewPrivileges = true; | ||
| 779 | ProtectKernelTunables = true; | ||
| 780 | ProtectKernelModules = true; | ||
| 781 | ProtectKernelLogs = true; | ||
| 782 | ProtectControlGroups = true; | ||
| 783 | MemoryDenyWriteExecute = true; | ||
| 784 | RestrictSUIDSGID = true; | ||
| 785 | KeyringMode = "private"; | ||
| 786 | ProtectClock = true; | ||
| 787 | RestrictRealtime = true; | ||
| 788 | PrivateDevices = true; | ||
| 789 | PrivateTmp = true; | ||
| 790 | ProtectHostname = true; | ||
| 791 | ReadWritePaths = ["/run/postgresql"]; | ||
| 792 | }; | ||
| 793 | }; | ||
| 794 | users.users."postfix-ccert-sender-policy" = { | ||
| 795 | isSystemUser = true; | ||
| 796 | group = "postfix-ccert-sender-policy"; | ||
| 797 | }; | ||
| 798 | users.groups."postfix-ccert-sender-policy" = {}; | ||
| 799 | |||
| 800 | services.postfwd = { | ||
| 801 | enable = true; | ||
| 802 | rules = '' | ||
| 803 | id=RCPT01; protocol_state=DATA; protocol_state=END-OF-MESSAGE; action=rcpt(ccert_subject/100/3600/450 4.7.1 Exceeding maximum of 100 recipients per hour [$$ratecount]) | ||
| 804 | id=RCPT02; protocol_state=DATA; protocol_state=END-OF-MESSAGE; action=rcpt(ccert_subject/1000/86400/450 4.7.1 Exceeding maximum of 1000 recipients per day [$$ratecount]) | ||
| 805 | ''; | ||
| 806 | }; | ||
| 733 | }; | 807 | }; |
| 734 | } | 808 | } |
diff --git a/hosts/surtr/postgresql.nix b/hosts/surtr/postgresql.nix index 66ce60eb..7013ae97 100644 --- a/hosts/surtr/postgresql.nix +++ b/hosts/surtr/postgresql.nix | |||
| @@ -104,7 +104,7 @@ in { | |||
| 104 | ALTER TABLE mailbox_mapping ALTER local TYPE citext; | 104 | ALTER TABLE mailbox_mapping ALTER local TYPE citext; |
| 105 | ALTER TABLE mailbox_mapping ALTER domain TYPE citext; | 105 | ALTER TABLE mailbox_mapping ALTER domain TYPE citext; |
| 106 | 106 | ||
| 107 | CREATE VIEW mailbox_quota_rule (id, mailbox, quota_rule) AS SELECT id, mailbox, (CASE WHEN quota_bytes IS NULL THEN '*:ignore' ELSE '*:bytes=' || quota_bytes END) AS quota_rule FROM mailbox; | 107 | CREATE VIEW mailbox_quota_rule (id, mailbox, quota_rule) AS SELECT id, mailbox, (CASE WHEN quota_bytes IS NULL THEN '*:ignore' ELSE '*:bytes=' || quota_bytes END) AS quota_rule FROM mailbox; |
| 108 | 108 | ||
| 109 | CREATE VIEW virtual_mailbox_domain (domain) AS SELECT DISTINCT domain FROM mailbox_mapping; | 109 | CREATE VIEW virtual_mailbox_domain (domain) AS SELECT DISTINCT domain FROM mailbox_mapping; |
| 110 | CREATE VIEW virtual_mailbox_mapping (lookup) AS SELECT (CASE WHEN local IS NULL THEN ''' ELSE local END) || '@' || domain AS lookup FROM mailbox_mapping; | 110 | CREATE VIEW virtual_mailbox_mapping (lookup) AS SELECT (CASE WHEN local IS NULL THEN ''' ELSE local END) || '@' || domain AS lookup FROM mailbox_mapping; |
| @@ -143,6 +143,15 @@ in { | |||
| 143 | 143 | ||
| 144 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO "spm"; | 144 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO "spm"; |
| 145 | COMMIT; | 145 | COMMIT; |
| 146 | |||
| 147 | BEGIN; | ||
| 148 | SELECT _v.register_patch('007-ccert-sender-policy', ARRAY['000-base'], null); | ||
| 149 | |||
| 150 | CREATE USER "postfix-ccert-sender-policy"; | ||
| 151 | GRANT CONNECT ON DATABASE "email" TO "postfix-ccert-sender-policy"; | ||
| 152 | ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "postfix-ccert-sender-policy"; | ||
| 153 | GRANT SELECT ON ALL TABLES IN SCHEMA public TO "postfix-ccert-sender-policy"; | ||
| 154 | COMMIT; | ||
| 146 | ''} | 155 | ''} |
| 147 | ''; | 156 | ''; |
| 148 | }; | 157 | }; |
