summaryrefslogtreecommitdiff
path: root/hosts/surtr
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/surtr')
-rw-r--r--hosts/surtr/email/ccert-policy-server/ccert_policy_server/__init__.py0
-rw-r--r--hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py92
-rw-r--r--hosts/surtr/email/ccert-policy-server/setup.py12
-rw-r--r--hosts/surtr/email/default.nix88
-rw-r--r--hosts/surtr/postgresql.nix11
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 @@
1from systemd.daemon import listen_fds
2from sdnotify import SystemdNotifier
3from socketserver import StreamRequestHandler, ThreadingMixIn
4from systemd_socketserver import SystemdSocketServer
5import sys
6from threading import Thread
7from psycopg_pool import ConnectionPool
8from psycopg.rows import namedtuple_row
9
10import logging
11
12
13class 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
52class 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
59def 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
91if __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 @@
1from setuptools import setup, find_packages
2
3setup(
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
3with lib; 3with 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"];
24in { 45in {
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 };