summaryrefslogtreecommitdiff
path: root/hosts/surtr
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/surtr')
-rw-r--r--hosts/surtr/bifrost/default.nix4
-rw-r--r--hosts/surtr/changedetection-io.nix66
-rw-r--r--hosts/surtr/default.nix25
-rw-r--r--hosts/surtr/dns/default.nix9
-rw-r--r--hosts/surtr/dns/keys/changedetection.yggdrasil.li_acme18
-rw-r--r--hosts/surtr/dns/keys/kimai.yggdrasil.li_acme19
-rw-r--r--hosts/surtr/dns/keys/online.yggdrasil.li_acme18
-rw-r--r--hosts/surtr/dns/keys/pw.bouncy.email_acme18
-rw-r--r--hosts/surtr/dns/keys/vikunja.yggdrasil.li_acme18
-rw-r--r--hosts/surtr/dns/zones/consulting.kleen.soa4
-rw-r--r--hosts/surtr/dns/zones/email.bouncy.soa12
-rw-r--r--hosts/surtr/dns/zones/email.nights.soa8
-rw-r--r--hosts/surtr/dns/zones/li.141.soa9
-rw-r--r--hosts/surtr/dns/zones/li.kleen.soa9
-rw-r--r--hosts/surtr/dns/zones/li.synapse.soa2
-rw-r--r--hosts/surtr/dns/zones/li.yggdrasil.soa41
-rw-r--r--hosts/surtr/dns/zones/org.praseodym.soa9
-rw-r--r--hosts/surtr/email/ccert-policy-server/.envrc4
-rw-r--r--hosts/surtr/email/ccert-policy-server/.gitignore2
-rw-r--r--hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py18
-rw-r--r--hosts/surtr/email/ccert-policy-server/poetry.lock169
-rw-r--r--hosts/surtr/email/ccert-policy-server/pyproject.toml36
-rw-r--r--hosts/surtr/email/ccert-policy-server/uv.lock130
-rw-r--r--hosts/surtr/email/default.nix802
-rw-r--r--hosts/surtr/email/email-password-server-secret18
-rw-r--r--hosts/surtr/email/internal-policy-server/.envrc4
-rw-r--r--hosts/surtr/email/internal-policy-server/.gitignore2
-rw-r--r--hosts/surtr/email/internal-policy-server/internal_policy_server/__init__.py0
-rw-r--r--hosts/surtr/email/internal-policy-server/internal_policy_server/__main__.py106
-rw-r--r--hosts/surtr/email/internal-policy-server/pyproject.toml18
-rw-r--r--hosts/surtr/email/internal-policy-server/uv.lock119
-rw-r--r--hosts/surtr/email/password-server/.envrc4
-rw-r--r--hosts/surtr/email/password-server/.gitignore2
-rw-r--r--hosts/surtr/email/password-server/password_server/__init__.py0
-rw-r--r--hosts/surtr/email/password-server/password_server/__main__.py268
-rw-r--r--hosts/surtr/email/password-server/pyproject.toml28
-rw-r--r--hosts/surtr/email/password-server/uv.lock334
-rw-r--r--hosts/surtr/http/default.nix5
-rw-r--r--hosts/surtr/http/online.nix29
-rw-r--r--hosts/surtr/kimai.nix68
-rw-r--r--hosts/surtr/matrix/default.nix1
-rw-r--r--hosts/surtr/postgresql/default.nix79
-rw-r--r--hosts/surtr/tls/default.nix13
-rw-r--r--hosts/surtr/tls/tsig_keys/changedetection.yggdrasil.li18
-rw-r--r--hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li19
-rw-r--r--hosts/surtr/tls/tsig_keys/online.yggdrasil.li18
-rw-r--r--hosts/surtr/tls/tsig_keys/pw.bouncy.email18
-rw-r--r--hosts/surtr/tls/tsig_keys/vikunja.yggdrasil.li18
-rw-r--r--hosts/surtr/vikunja.nix66
49 files changed, 2139 insertions, 566 deletions
diff --git a/hosts/surtr/bifrost/default.nix b/hosts/surtr/bifrost/default.nix
index fbfde757..52ab43f5 100644
--- a/hosts/surtr/bifrost/default.nix
+++ b/hosts/surtr/bifrost/default.nix
@@ -18,7 +18,7 @@ in {
18 ListenPort = 51822; 18 ListenPort = 51822;
19 }; 19 };
20 wireguardPeers = [ 20 wireguardPeers = [
21 { AllowedIPs = [ "2a03:4000:52:ada:4:1::/96" ]; 21 { AllowedIPs = [ "2a03:4000:52:ada:4:1::/96" "2a03:4000:52:ada:6::/80" ];
22 PublicKey = trim (readFile ../../vidhar/network/bifrost/vidhar.pub); 22 PublicKey = trim (readFile ../../vidhar/network/bifrost/vidhar.pub);
23 } 23 }
24 ]; 24 ];
@@ -34,6 +34,8 @@ in {
34 routes = [ 34 routes = [
35 { Destination = "2a03:4000:52:ada:4::/80"; 35 { Destination = "2a03:4000:52:ada:4::/80";
36 } 36 }
37 { Destination = "2a03:4000:52:ada:6::/80";
38 }
37 ]; 39 ];
38 linkConfig = { 40 linkConfig = {
39 RequiredForOnline = false; 41 RequiredForOnline = false;
diff --git a/hosts/surtr/changedetection-io.nix b/hosts/surtr/changedetection-io.nix
new file mode 100644
index 00000000..bfdedee1
--- /dev/null
+++ b/hosts/surtr/changedetection-io.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "changedetection.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."changedetection-io" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:5001" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "changedetection.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/changedetection.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/changedetection.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/changedetection.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://changedetection-io;
35
36 proxy_http_version 1.1;
37 proxy_set_header Upgrade $http_upgrade;
38 proxy_set_header Connection "upgrade";
39
40 proxy_redirect off;
41 proxy_set_header Host $host;
42 proxy_set_header X-Real-IP $remote_addr;
43 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44 proxy_set_header X-Forwarded-Host $server_name;
45 proxy_set_header X-Forwarded-Proto $scheme;
46
47 client_max_body_size 0;
48 proxy_request_buffering off;
49 proxy_buffering off;
50 '';
51 };
52 };
53 };
54 };
55
56 systemd.services.nginx = {
57 serviceConfig = {
58 LoadCredential = [
59 "changedetection.yggdrasil.li.key.pem:${config.security.acme.certs."changedetection.yggdrasil.li".directory}/key.pem"
60 "changedetection.yggdrasil.li.pem:${config.security.acme.certs."changedetection.yggdrasil.li".directory}/fullchain.pem"
61 "changedetection.yggdrasil.li.chain.pem:${config.security.acme.certs."changedetection.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}
diff --git a/hosts/surtr/default.nix b/hosts/surtr/default.nix
index d420040a..fb2d5b62 100644
--- a/hosts/surtr/default.nix
+++ b/hosts/surtr/default.nix
@@ -4,10 +4,11 @@ with lib;
4 4
5{ 5{
6 imports = with flake.nixosModules.systemProfiles; [ 6 imports = with flake.nixosModules.systemProfiles; [
7 tmpfs-root qemu-guest openssh rebuild-machines zfs 7 tmpfs-root qemu-guest openssh rebuild-machines zfs zswap
8 ./zfs.nix ./dns ./tls ./http ./bifrost ./matrix ./postgresql 8 ./zfs.nix ./dns ./tls ./http ./bifrost ./matrix ./postgresql
9 ./prometheus ./email ./vpn ./borg.nix ./etebase ./immich.nix 9 ./prometheus ./email ./vpn ./borg.nix ./etebase ./immich.nix
10 ./paperless.nix ./hledger.nix ./audiobookshelf.nix 10 ./paperless.nix ./hledger.nix ./audiobookshelf.nix ./kimai.nix
11 ./changedetection-io.nix ./vikunja.nix
11 ]; 12 ];
12 13
13 config = { 14 config = {
@@ -22,21 +23,11 @@ with lib;
22 device = "/dev/vda"; 23 device = "/dev/vda";
23 }; 24 };
24 25
25
26 tmp.useTmpfs = true; 26 tmp.useTmpfs = true;
27 27
28 zfs.devNodes = "/dev"; # /dev/vda2 does not show up in /dev/disk/by-id 28 zfs.devNodes = "/dev"; # /dev/vda2 does not show up in /dev/disk/by-id
29 29
30 kernelModules = ["ptp_kvm"]; 30 kernelModules = ["ptp_kvm"];
31 kernelPatches = [
32 { name = "zswap-default";
33 patch = null;
34 extraStructuredConfig = with lib.kernel; {
35 ZSWAP_DEFAULT_ON = yes;
36 ZSWAP_SHRINKER_DEFAULT_ON = yes;
37 };
38 }
39 ];
40 }; 31 };
41 32
42 fileSystems = { 33 fileSystems = {
@@ -163,15 +154,7 @@ with lib;
163 options = "--delete-older-than 30d"; 154 options = "--delete-older-than 30d";
164 }; 155 };
165 156
166 security.dhparams = { 157 services.dbus.implementation = "dbus";
167 enable = true;
168 defaultBitSize = 4096;
169 params = {
170 nginx = {};
171 coturn = {};
172 };
173 stateful = true;
174 };
175 158
176 systemd.sysusers.enable = false; 159 systemd.sysusers.enable = false;
177 system.etc.overlay.mutable = true; 160 system.etc.overlay.mutable = true;
diff --git a/hosts/surtr/dns/default.nix b/hosts/surtr/dns/default.nix
index 7aa3fb00..4bb7059a 100644
--- a/hosts/surtr/dns/default.nix
+++ b/hosts/surtr/dns/default.nix
@@ -46,7 +46,12 @@ in {
46 46
47 systemd.services.knot = { 47 systemd.services.knot = {
48 unitConfig.RequiresMountsFor = [ "/var/lib/knot" ]; 48 unitConfig.RequiresMountsFor = [ "/var/lib/knot" ];
49 serviceConfig.LoadCredential = map ({name, ...}: "${name}.yaml:${config.sops.secrets.${name}.path}") knotKeys; 49 serviceConfig = {
50 LoadCredential = map ({name, ...}: "${name}.yaml:${config.sops.secrets.${name}.path}") knotKeys;
51 BindPaths = let
52 dkimBindPath = domain: "/var/lib/rspamd/dkim/${domain}.txt:/var/lib/dkim/${domain}.txt";
53 in map dkimBindPath ["yggdrasil.li" "141.li" "kleen.li" "praseodym.org" "kleen.consulting" "bouncy.email" "surtr.yggdrasil.li"];
54 };
50 }; 55 };
51 56
52 services.knot = { 57 services.knot = {
@@ -157,7 +162,7 @@ in {
157 ${concatMapStringsSep "\n" mkZone [ 162 ${concatMapStringsSep "\n" mkZone [
158 { domain = "yggdrasil.li"; 163 { domain = "yggdrasil.li";
159 addACLs = { "yggdrasil.li" = ["ymir_acme_acl"]; }; 164 addACLs = { "yggdrasil.li" = ["ymir_acme_acl"]; };
160 acmeDomains = ["surtr.yggdrasil.li" "yggdrasil.li" "etesync.yggdrasil.li" "immich.yggdrasil.li" "app.etesync.yggdrasil.li" "paperless.yggdrasil.li" "hledger.yggdrasil.li" "audiobookshelf.yggdrasil.li"]; 165 acmeDomains = ["surtr.yggdrasil.li" "yggdrasil.li" "etesync.yggdrasil.li" "immich.yggdrasil.li" "app.etesync.yggdrasil.li" "paperless.yggdrasil.li" "hledger.yggdrasil.li" "audiobookshelf.yggdrasil.li" "kimai.yggdrasil.li" "changedetection.yggdrasil.li" "vikunja.yggdrasil.li" "online.yggdrasil.li" "pw.bouncy.email"];
161 } 166 }
162 { domain = "nights.email"; 167 { domain = "nights.email";
163 addACLs = { "nights.email" = ["ymir_acme_acl"]; }; 168 addACLs = { "nights.email" = ["ymir_acme_acl"]; };
diff --git a/hosts/surtr/dns/keys/changedetection.yggdrasil.li_acme b/hosts/surtr/dns/keys/changedetection.yggdrasil.li_acme
new file mode 100644
index 00000000..dcc7f85b
--- /dev/null
+++ b/hosts/surtr/dns/keys/changedetection.yggdrasil.li_acme
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:QWZAer8xKZvAGl3HxaIdsOT1n3os4EDyQoZOU3YzwDpPfweVRhhBfyAg7M6rMRj8K8ffkkRWatDmgyHV1R43GfNyb1sLjqdqPysYXxC8KlP22WlT+1xstQ2q1KYmeN6VEKF0q+QOMMPRvwQbSQ0eC4mXcE+WgQSTVywjab9hQuc8vin69RbFxbhepxYLXT1rzQpLlxFmUNZBcLpSqsHkSDa2B0d4j2kIvSl2BuUgb3QJwgyNS5pGbnfyVfmus7p5+/pVFCe5EwTVjwgpn/cpIB0mu1Bbt9r0EvCkYXI6wKcLDVbfdV7KsA==,iv:wjHpcClpybzCIi3JhxgXTd5nW9y223pJn2rBde/2cy8=,tag:etfnmj+HhIKeZMGjxE5jiw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlWkk2Z0FUNlNoaFVaSFEr\nSE54eUpHaTNzTTdjaytJemtQUW1mWjJNUGhzCkU1OGlNb3pidVFKalVCZUhBODNi\nMjcxR0xLUDRwYkZ5V2I1Q3Z6Y2pmRWMKLS0tIGNzaGNiTEMvdEhBMDRZY2pDQzNu\nWkpkcVNYVjJZQS9QRHEyOUx6RVpQVjAKKGjVrfeovCIml2hExydC9Cd7PyungtpJ\nCdXfrvzP/OtoBSiEDQGC2VafwKkZ98dQqVRnfVApDoxdVQ8vIrxmKQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTbUNBV1NYZEtKaDlzUVBF\nU2IrZ3dSWS9keVE1ak9qWkppc1hOY3JpMjEwCmNUbVJJMFNObmptVGQ0K1ovanVm\nc1dzN0VRVThyVWxPWTFLbldPQnE3Z2sKLS0tIEdWRENRRFROSm5SQ04yTG1wZWJ2\nNDMxK1ArYmdiQWJZV0d2TElZcFZLNVEKtmVrSIOcP4Ek1WW85f2/dNVYQMz9XqZ3\n0J04kqvkHZuM8PiBDg2l2rSh0xhHz3xb1iBhAddLXEjeEfy6o9HKyg==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-12-08T12:46:12Z",
15 "mac": "ENC[AES256_GCM,data:waY6IDfabZ8B8069liXh7RXjgUTpOdr4U9VQK5xYRujAlI//Ea5lM2ODHJ7PrAkZsK0TGB9ezN8SA5QpxYZwOcpxg45jNbTALxZsZMEzrtCy4wSiBdiLvRoTXvwMZsnsaQEGk2ij2rEqNEOYYBFapBoIz2w5kbEZrrhVRHSkNME=,iv:tpU++qliONinepku/gdPJQ/h2NdyNw3GY+RV+6UM07U=,tag:yieMw3BOC134zAIqb1Fvjg==,type:str]",
16 "version": "3.11.0"
17 }
18}
diff --git a/hosts/surtr/dns/keys/kimai.yggdrasil.li_acme b/hosts/surtr/dns/keys/kimai.yggdrasil.li_acme
new file mode 100644
index 00000000..bdfb135a
--- /dev/null
+++ b/hosts/surtr/dns/keys/kimai.yggdrasil.li_acme
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:sKFt4pH0Xn7Qm6JFMg/2N7Ht7jtMJukfN+U3dQaoYXPbhRJ+heEtDpXV/WP4AlfbfpIOgTPW3mcmQCwKFNhS00vEsQA4728FfXZzDDmZCa3hwg51wDbL7XUOr0OePgzi86lt0Q193K6CkGqEAa1vFIb//ElEfBYIwdATbmcoAsM3mHhz58X7c1qf8LNuB93o/1N2xXXZI3NWOhOjlviTc2DAhffXDwlMJSYUhldnwtDKmLM1mooJzLgm2p9w7gRD7WPqEqZFq9uFDK69P9uX5T9hFHg=,iv:rAE4sYxxLou4tyD4RWTp3LjQP0cya95coy1MvwfEK/U=,tag:u4SSk8SZFlj0ks7d6tDocw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2KzdNUWhEcDB6QmtUTnVh\nNS9Nc2I4UjAzekxhRXo1UmY3SklPejV1TURJCm9NY2lVOERoMDFKTU56Mmh1NHEr\naGV4M1RoVldHV0xyc3Z0MnVqakpjMFUKLS0tIEYxSk9OUm9kMkdtcG5POWRGQVkx\nY1FEaXYwMGo0L0Z0aTVTZDA5aUFDWEUKJ+e/7lR/rNPNVnIy+wkiKiAYMxWp4L7q\nwnSTx451vSnxv9j3JWB43Y7XQC08cisWDj06ULw8FnEbKYOvTYj9mQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwOTU3dUEzaXM5T1VkbDRO\nMm14OG1mUkk2bDRhdnBsMHBkc3kvUzlyNlQwCktFSHJhMnhoQ2J6bC9vUHNLWTRC\nRFpYeHo3N2xjWUhjQnRwQ2Nrc1pRUmsKLS0tIDdPeFBVdkxDd1JWSmcxQ0tLMTBD\ncHU3VExZOUhYUlJvbGNoK3FMK2VIbGMKFk94P9aBY04CPIi983f3Aalgh4fnU+/K\n2mxawSMf9jz8704N5XJfmr2hwNy8hqLIn8bjsEMAPTfE1YBGga4w0g==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-24T09:42:23Z",
15 "mac": "ENC[AES256_GCM,data:diCeJGvBmM0Ng722eKoFwDe7pqZrdLPSLn5j9LfdaFI64BAbSbA5bAq4NFXqdJ1vttarD2A5rEafYoXUxP8228x2GhNyWUGW5AWgBjVPUc59gjs4wYKR5HlkVMIadhTwNheEyoEjrxX40GNBgCG7X3ocOtOYKbKECp433gdAPDg=,iv:d+yJMWj2RyFnveo2ZNrpNeV+amXM+H7vdC0A2F7mwjA=,tag:yjibG2iusdprp0ORghYWhw==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
diff --git a/hosts/surtr/dns/keys/online.yggdrasil.li_acme b/hosts/surtr/dns/keys/online.yggdrasil.li_acme
new file mode 100644
index 00000000..9c166f38
--- /dev/null
+++ b/hosts/surtr/dns/keys/online.yggdrasil.li_acme
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:Ic4cQrF3bU55P8q4lFDL0fe0NJyoJ2K8VN1U7Lgq/y9+W2PcaGM0AjuPGP3K5SOMyVU6xUiN5T9vfRA+c7ztRBmEZYWu5/p7l9tajqMyCozzZ5ctYGEFAk5bwfuXf9Y4cGP+vh6dA6ktbewN8HzajQwHAd5ADcICzwXVD4CATcTPOgaUG9pDZZRqmRnfDzC22j6nKQQN494+5xj8cRFnx96LYN0nZUyVO5F5XxaWV+SkT0tJBhHuGNF85r/k3pVXmqRYgaQl6y4LNkzOSXGCy5jtu8QrPg==,iv:uQkkw/9KK4JHHcSgfnyV/VaW/dzqNhsz4eVOc4NimGU=,tag:/Xi7TzBBM7j2SEqV1Ah4yQ==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBBVFJ5K28xSjd0VGJvWldV\nRGxqUVQ5RDI5bi9kbXYyZXo0WG42N3BmdlY0CnU1TkpZT1V1Z1VMMUp4cFZnZGJh\nTTYvd3ZMVjd1Z3YrbDBQdnpQcGdtKzAKLS0tIHEzN2laV0JzTHhlbjNKb1ZuL1Bl\nOFpuQkQxaHhLdTlhSSs1aTZhTGlZVFEKAHrcAKrtdCfQKfhaDpQKQVPpMqZCEunT\nvuvRdxMvA5T4mX3vHEJ5WKXQ8C49krfMYT9QoSy5v2OeazTOPW/XMA==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiSTRTZGhNamxTSlg2VERI\ndnBtWFlZV2hzOTgyZmFldG9xU2dJalhTaERZCms0NVlJWWVqdjdNMkNFQXRRa0o3\nZHRvTE9BK3lWbHZTWkJ1SXE0NmlJR0kKLS0tIC9BclpLNlFrb0kxRlRibWVOSWdL\nRFUvd0N6ZFpnd25yME1WSXo3Mit6Yk0Kqpyx2tzblU0hLMaLQpuGaMDzC7ZlgIFU\n3szVfhlLHuLfqDzpxTshPrPOQs+q7RvUJ+I1NeztlpGw239/+c4U5Q==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2026-05-15T11:50:25Z",
15 "mac": "ENC[AES256_GCM,data:fPcwi5HyrkY6HM90O3c9curi87X8MIWtNKF9lxQvFxPCMRVewDPteBvMgjCKDSM7RTGJVdDWnCyKTH6PXJwxGCuvOSv1CqSKreVPnNvilRETE+NLjwyQsAkgdZFDMofOvk1FhjI8gZ2aJg0yDwBvZMtaF694DhR4QVldXY2q8DA=,iv:Rs6KxSJwOSFrr7CEIs/9DED34tXbrCWfSn69KjiKSAE=,tag:tmKwx1y6LftEQBZWSs8qFQ==,type:str]",
16 "version": "3.12.2"
17 }
18}
diff --git a/hosts/surtr/dns/keys/pw.bouncy.email_acme b/hosts/surtr/dns/keys/pw.bouncy.email_acme
new file mode 100644
index 00000000..68e01632
--- /dev/null
+++ b/hosts/surtr/dns/keys/pw.bouncy.email_acme
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:+IqTJv5QETPts+voCd9wD0FjPWRL9llgavSm813UBmdThx8xIvqYx782vjsAR7tkbhUJAvD2XD5YEO32oJ7z0+qrotOHUE0uB0CmY6TTU1UmURwssQmbDCDrHyVhTHd341cLjCXJ7baWOwd3MSw39yGjOeKCo/JdqXW+2JQWiSJjinZo6TgXG7eY2LQdqSVdQVizPBtYWyxpREps9RH7/yJn1FGp7XrG1YvzYCUyWgAKGj9r8v7c9GLcvBP2hx4zYoD3fAP0SKWYKf2t9/M=,iv:CfPWjBxTTiYLneHvNfjadZyip0NHZObnbKqSLTeWuLo=,tag:ZqSjmTdI2hJ+zdgbTkgYAw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvUi9EZzlnTU1WemR0dVpp\nTlpxZi9FcTB1UWd0UG41UExvWnZRZWhiaFZVCjc3M2VnUmJrY1l0aXhGUGZtZ1ho\nMVB6bUNCRTVjMjE0UlFIM1NlRy9VTzgKLS0tIGdzT1czUnNnMjlwQXBXZHU4Mzhy\nSHJGVy9mZVZHZWFtYk4zZ2pHWEdLYmsKuNt8C+avCnHtxpVTbzaZS2rbnE+rvqec\n1wY88lWTU+nf3Ek97JOuQeeKhtERiQ6LsrMx2gzCGIbRUdI/cIGLCg==\n-----END AGE ENCRYPTED FILE-----\n",
7 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866"
8 },
9 {
10 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyU3I1cXBkQUl3dW0zbENF\nK2lLcUhLVFk2ZWZ3VWl5SDVCeFJBNGNZNVJ3CkpLKzFPWFB3Y0hpN1VybkJyL29I\nem03bjF3Qk1xdTl3YnJZQUV1c2xjSXcKLS0tIDlYT0FMNXo2Qmd2Q2xkM0Zwd3Jj\naTNDcnNXODJ2bHJpOWxqWm1qWGJ2aFEKjIAE/9N3+6AAnX4E3qOWBG+gwCF1Wn+F\ncyoBEkYIwXyMmbNgoWA2Z/nZkiqm4ANUwnGioGabiMkL/hBqGTajtQ==\n-----END AGE ENCRYPTED FILE-----\n",
11 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq"
12 }
13 ],
14 "lastmodified": "2026-06-06T13:54:42Z",
15 "mac": "ENC[AES256_GCM,data:rLOlH1N92+yzfIRSPg7GSo8SxYtjGcDbGc80nh3N6xZr7G3Fk3/EUVNfMk6YHjf8aFnCf2Odaht3hWQ2dsb5RrwAIQEyCD5/S/xdlEnJFfh6zGEFg79utL+HUB/hmYynnmuXPkDLCeRqKgbUQ2flHYZUqHkScyzqQkyFnKfzTas=,iv:bw1AoNNxlYN4Xk2bWHRB9e9gv1CiKdvFOTYX588DNP0=,tag:kfNW3HUBJSDh5vBZ6iGicQ==,type:str]",
16 "version": "3.13.1"
17 }
18}
diff --git a/hosts/surtr/dns/keys/vikunja.yggdrasil.li_acme b/hosts/surtr/dns/keys/vikunja.yggdrasil.li_acme
new file mode 100644
index 00000000..69956047
--- /dev/null
+++ b/hosts/surtr/dns/keys/vikunja.yggdrasil.li_acme
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:dchG/V5uJVuj1P35Zj9YHwAEqwHeiqGS9zPt+M9V9E3uelugJN2rTe/pnnUzw6piIsdfwmJA4qaNddGbw6Tx9bQbuJxm9wbAm+M+lYokQ1fonQIlm+tZ1dEeBQO00lXidIxbdMcAaKDyGvceexoYOMwg1UYCBl+osu5bCFjxrekYARITec2mPAU2wjjkJ91rsFvp5dvngGfwSu1Yks3ZhdbIUeJx85FjRsi0Wn5m91R8MYE8GFf5OdAbTolsW9TpHbjpVCzuLVXVqaX3Y5PR+OkSND+gta8B,iv:QjRPkiQ1vGNumjBS9r9KIxUWzapsDG8qU7RzUnd9msI=,tag:8y26IM2TJfFl6Nh4mIqLtQ==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQbmp1MmRtemNNQmtZSGN0\nZXhURWNYbkg0QS9iL1B5cndQdGthdVJPTFhzCjZ6M2JQWWhFNFFWYkIvOUY4WmUx\nblo5QkF5SldlWGhnTnQxdzBpM1Nad1UKLS0tIEE4Q3dEV081UlA4N3RMZ0szUHcv\nUm1qN09wYndHNCtFR2NWUVZWUnNRTlUKM8wfNY8Vq6Jn3Yc9n5qpYOqrHyATF0F3\nq2ZpnuDJpyUamfCTeJX2jBmTlYAw90ykmhY1IRqyW52sEY+D376pKw==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhY3hDZEZ2OTg5SkxoeTVk\nbjU1amlTZndxeStxZnVnTWxHOXpqUUhpREE4CitZdVNYcGIyMFo2RTVSUzVobmZX\ncmJDSzRSTWxaME1Qb05WZWM4eHV4Z0kKLS0tIGtMM3FhdkxqTzdFYUVkNHZ3cERt\nV21pcndweHZ5cnNkako5NkJiQ3dFZWMKY3LgLjExrq+nrirJ9PRjkPVJ1YyUjB4y\nfsz/1TPciefIWoqeCsRp+iWWwyb88Dpuv4qTqgrzncHOpw2gEmNurw==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2026-01-01T14:25:23Z",
15 "mac": "ENC[AES256_GCM,data:4ohI9Bb16Si871wcBYfINkKDhpQs6AOE8KWlmCoKsGK1pawyKNdeXTfeB/171eT/6XRklLY3e9lTobuh2sYvVDhSzrzqlDfo4Rhq/M3/fsgvyRJItKeTrfnD4LxSX4D+H7N5t9gP1Tjt1cdEDL6GgzbIksMnpdL9K1f6vgludcI=,iv:a3v5bWHhXD5XV2Hwe3AJB5YG/8mznn/ukZOHbJO5AW8=,tag:7vYNT9omLnnyxwVUYVBfSA==,type:str]",
16 "version": "3.11.0"
17 }
18}
diff --git a/hosts/surtr/dns/zones/consulting.kleen.soa b/hosts/surtr/dns/zones/consulting.kleen.soa
index 9aecf908..99a88c7b 100644
--- a/hosts/surtr/dns/zones/consulting.kleen.soa
+++ b/hosts/surtr/dns/zones/consulting.kleen.soa
@@ -1,7 +1,7 @@
1$ORIGIN kleen.consulting. 1$ORIGIN kleen.consulting.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2023020100 ; serial 4 2026032100 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -21,7 +21,7 @@ $TTL 3600
21@ IN MX 0 mailin.kleen.consulting. 21@ IN MX 0 mailin.kleen.consulting.
22@ IN TXT "v=spf1 a:mailout.kleen.consulting -all" 22@ IN TXT "v=spf1 a:mailout.kleen.consulting -all"
23 23
24surtr._domainkey IN CNAME surtr._domainkey.yggdrasil.li. 24$INCLUDE /var/lib/dkim/kleen.consulting.txt
25_dmarc IN TXT "v=DMARC1;p=reject;sp=reject;pct=100;adkim=s;aspf=s;rua=mailto:postmaster@kleen.consulting;ruf=mailto:postmaster@kleen.consulting" 25_dmarc IN TXT "v=DMARC1;p=reject;sp=reject;pct=100;adkim=s;aspf=s;rua=mailto:postmaster@kleen.consulting;ruf=mailto:postmaster@kleen.consulting"
26 26
27_acme-challenge IN NS ns.yggdrasil.li. 27_acme-challenge IN NS ns.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/email.bouncy.soa b/hosts/surtr/dns/zones/email.bouncy.soa
index 2b319a93..074f3ba2 100644
--- a/hosts/surtr/dns/zones/email.bouncy.soa
+++ b/hosts/surtr/dns/zones/email.bouncy.soa
@@ -1,7 +1,7 @@
1$ORIGIN bouncy.email. 1$ORIGIN bouncy.email.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2024070901 ; serial 4 2026060600 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -20,7 +20,7 @@ $TTL 3600
20@ IN MX 0 mailin.bouncy.email. 20@ IN MX 0 mailin.bouncy.email.
21@ IN TXT "v=spf1 a:mailout.bouncy.email -all" 21@ IN TXT "v=spf1 a:mailout.bouncy.email -all"
22 22
23surtr._domainkey IN CNAME surtr._domainkey.yggdrasil.li. 23$INCLUDE /var/lib/dkim/bouncy.email.txt
24_dmarc IN TXT "v=DMARC1;p=reject;sp=reject;pct=100;adkim=s;aspf=s;rua=mailto:postmaster@bouncy.email;ruf=mailto:postmaster@bouncy.email" 24_dmarc IN TXT "v=DMARC1;p=reject;sp=reject;pct=100;adkim=s;aspf=s;rua=mailto:postmaster@bouncy.email;ruf=mailto:postmaster@bouncy.email"
25 25
26_acme-challenge IN NS ns.yggdrasil.li. 26_acme-challenge IN NS ns.yggdrasil.li.
@@ -71,6 +71,14 @@ _acme-challenge.spm IN NS ns.yggdrasil.li.
71 71
72spm IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::" 72spm IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
73 73
74pw IN A 202.61.241.61
75pw IN AAAA 2a03:4000:52:ada::
76pw IN MX 0 mailin.bouncy.email.
77pw IN TXT "v=spf1 redirect=bouncy.email"
78_acme-challenge.pw IN NS ns.yggdrasil.li.
79
80pw IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
81
74_mta-sts IN TXT "v=STSv1; id=2022100600" 82_mta-sts IN TXT "v=STSv1; id=2022100600"
75_smtp._tls IN TXT "v=TLSRPTv1; rua=mailto:postmaster@bouncy.email" 83_smtp._tls IN TXT "v=TLSRPTv1; rua=mailto:postmaster@bouncy.email"
76mta-sts IN A 202.61.241.61 84mta-sts IN A 202.61.241.61
diff --git a/hosts/surtr/dns/zones/email.nights.soa b/hosts/surtr/dns/zones/email.nights.soa
index 913a88d4..34209a99 100644
--- a/hosts/surtr/dns/zones/email.nights.soa
+++ b/hosts/surtr/dns/zones/email.nights.soa
@@ -1,7 +1,7 @@
1$ORIGIN nights.email. 1$ORIGIN nights.email.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2023013000 ; serial 4 2025060700 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -27,11 +27,7 @@ $TTL 3600
27 27
28_acme-challenge IN NS ns.yggdrasil.li. 28_acme-challenge IN NS ns.yggdrasil.li.
29 29
30ymir._domainkey IN TXT ( 30ymir._domainkey IN CNAME ymir._domainkey.yggdrasil.li.
31 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2"
32 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
33 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
34)
35 31
36_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 32_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
37_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 33_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.141.soa b/hosts/surtr/dns/zones/li.141.soa
index ab117f09..bf650a27 100644
--- a/hosts/surtr/dns/zones/li.141.soa
+++ b/hosts/surtr/dns/zones/li.141.soa
@@ -1,7 +1,7 @@
1$ORIGIN 141.li. 1$ORIGIN 141.li.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2025020900 ; serial 4 2026032101 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -45,11 +45,8 @@ ymir IN AAAA 2a03:4000:6:d004::
45ymir IN MX 0 ymir.yggdrasil.li 45ymir IN MX 0 ymir.yggdrasil.li
46ymir IN TXT "v=spf1 redirect=ymir.yggdrasil.li" 46ymir IN TXT "v=spf1 redirect=ymir.yggdrasil.li"
47 47
48ymir._domainkey IN TXT ( 48ymir._domainkey IN CNAME ymir._domainkey.yggdrasil.li.
49 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2" 49$INCLUDE /var/lib/dkim/141.li.txt
50 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
51 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
52)
53 50
54_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 51_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
55_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 52_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.kleen.soa b/hosts/surtr/dns/zones/li.kleen.soa
index a1c7d35a..cfaaa1f1 100644
--- a/hosts/surtr/dns/zones/li.kleen.soa
+++ b/hosts/surtr/dns/zones/li.kleen.soa
@@ -1,7 +1,7 @@
1$ORIGIN kleen.li. 1$ORIGIN kleen.li.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2023013000 ; serial 4 2026032101 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -27,11 +27,8 @@ $TTL 3600
27 27
28_acme-challenge IN NS ns.yggdrasil.li. 28_acme-challenge IN NS ns.yggdrasil.li.
29 29
30ymir._domainkey IN TXT ( 30ymir._domainkey IN CNAME ymir._domainkey.yggdrasil.li.
31 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2" 31$INCLUDE /var/lib/dkim/kleen.li.txt
32 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
33 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
34)
35 32
36_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 33_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
37_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 34_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.synapse.soa b/hosts/surtr/dns/zones/li.synapse.soa
index 086d4a85..247cf025 100644
--- a/hosts/surtr/dns/zones/li.synapse.soa
+++ b/hosts/surtr/dns/zones/li.synapse.soa
@@ -1,7 +1,7 @@
1$ORIGIN synapse.li. 1$ORIGIN synapse.li.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2023092100 ; serial 4 2025060701 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
diff --git a/hosts/surtr/dns/zones/li.yggdrasil.soa b/hosts/surtr/dns/zones/li.yggdrasil.soa
index 7273827b..4eb30d74 100644
--- a/hosts/surtr/dns/zones/li.yggdrasil.soa
+++ b/hosts/surtr/dns/zones/li.yggdrasil.soa
@@ -1,7 +1,7 @@
1$ORIGIN yggdrasil.li. 1$ORIGIN yggdrasil.li.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2025050900 ; serial 4 2026051501 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -46,7 +46,7 @@ surtr IN TXT "v=spf1 a:surtr.yggdrasil.li -all"
46vpn IN A 185.243.10.86 46vpn IN A 185.243.10.86
47vpn IN AAAA 2a03:4000:20:259:: 47vpn IN AAAA 2a03:4000:20:259::
48 48
49surtr._domainkey.surtr IN CNAME surtr._domainkey.yggdrasil.li. 49$INCLUDE /var/lib/dkim/surtr.yggdrasil.li.txt surtr.yggdrasil.li.
50_dmarc.surtr IN TXT "v=DMARC1;p=reject;sp=reject;pct=100;adkim=s;aspf=s;rua=mailto:postmaster@yggdrasil.li;ruf=mailto:postmaster@yggdrasil.li" 50_dmarc.surtr IN TXT "v=DMARC1;p=reject;sp=reject;pct=100;adkim=s;aspf=s;rua=mailto:postmaster@yggdrasil.li;ruf=mailto:postmaster@yggdrasil.li"
51 51
52_acme-challenge.surtr IN NS ns.yggdrasil.li. 52_acme-challenge.surtr IN NS ns.yggdrasil.li.
@@ -101,12 +101,44 @@ _acme-challenge.audiobookshelf IN NS ns.yggdrasil.li.
101 101
102audiobookshelf IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::" 102audiobookshelf IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
103 103
104kimai IN A 202.61.241.61
105kimai IN AAAA 2a03:4000:52:ada::
106kimai IN MX 0 surtr.yggdrasil.li
107kimai IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
108_acme-challenge.kimai IN NS ns.yggdrasil.li.
109
110kimai IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
111
112changedetection IN A 202.61.241.61
113changedetection IN AAAA 2a03:4000:52:ada::
114changedetection IN MX 0 surtr.yggdrasil.li
115changedetection IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
116_acme-challenge.changedetection IN NS ns.yggdrasil.li.
117
118changedetection IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
119
120vikunja IN A 202.61.241.61
121vikunja IN AAAA 2a03:4000:52:ada::
122vikunja IN MX 0 surtr.yggdrasil.li
123vikunja IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
124_acme-challenge.vikunja IN NS ns.yggdrasil.li.
125
126vikunja IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
127
128online IN A 202.61.241.61
129online IN AAAA 2a03:4000:52:ada::
130online IN MX 0 surtr.yggdrasil.li
131online IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
132_acme-challenge.online IN NS ns.yggdrasil.li.
133
104vidhar IN AAAA 2a03:4000:52:ada:4:1:: 134vidhar IN AAAA 2a03:4000:52:ada:4:1::
105vidhar IN MX 0 ymir.yggdrasil.li 135vidhar IN MX 0 ymir.yggdrasil.li
106vidhar IN TXT "v=spf1 redirect=yggdrasil.li" 136vidhar IN TXT "v=spf1 redirect=yggdrasil.li"
107 137
108mailout IN A 188.68.51.254 138mailout IN A 188.68.51.254
109mailout IN AAAA 2a03:4000:6:d004:: 139mailout IN AAAA 2a03:4000:6:d004::
140mailout IN A 202.61.241.61
141mailout IN AAAA 2a03:4000:52:ada::
110mailout IN MX 0 ymir.yggdrasil.li 142mailout IN MX 0 ymir.yggdrasil.li
111mailout IN TXT "v=spf1 redirect=yggdrasil.li" 143mailout IN TXT "v=spf1 redirect=yggdrasil.li"
112 144
@@ -115,10 +147,7 @@ ymir._domainkey IN TXT (
115 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24" 147 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
116 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ==" 148 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
117) 149)
118 150$INCLUDE /var/lib/dkim/yggdrasil.li.txt
119surtr._domainkey IN TXT ( "v=DKIM1;k=rsa;"
120 "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwEspNBXjTjPpCqSMkcBUJnSThqMcHwvDP3mOnv8wpDrGTN+1eJ1fzso5GHooGNt0kWHOpcoVwsMDIk81SR3zzNKYWqM40KvQ2ElNJqS5VDIfnxppiG9H5Nu3M7In5jv7OTSKsEi5eDzWqqvaHn6YjNQuKHQsJsAB1zUKoR1gqpvwJlV3tnhfQEl1O3qt0tG1c6JvgZ8R8szrk9"
121 "uNZzu90PDQY9UH4K1nu+INwlMgz9hzgJHIoNJOdB+1gmvnsI4MgmT/otxwKia/UoddN3Gcu7DO1gjFi5cwOA+zOgMnzzWUbys0Q3loCKp9EYgWUJQ9CCh5U4x4/GpV2VeEJ/0GYQIDAQAB" )
122 151
123_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 152_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
124_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 153_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/org.praseodym.soa b/hosts/surtr/dns/zones/org.praseodym.soa
index df505b4c..5bd627a4 100644
--- a/hosts/surtr/dns/zones/org.praseodym.soa
+++ b/hosts/surtr/dns/zones/org.praseodym.soa
@@ -1,7 +1,7 @@
1$ORIGIN praseodym.org. 1$ORIGIN praseodym.org.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2023013000 ; serial 4 2026032103 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -32,11 +32,8 @@ surtr IN AAAA 2a03:4000:52:ada::
32surtr IN MX 0 ymir.yggdrasil.li 32surtr IN MX 0 ymir.yggdrasil.li
33surtr IN TXT "v=spf1 redirect=yggdrasil.li" 33surtr IN TXT "v=spf1 redirect=yggdrasil.li"
34 34
35ymir._domainkey IN TXT ( 35ymir._domainkey IN CNAME ymir._domainkey.yggdrasil.li.
36 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2" 36$INCLUDE /var/lib/dkim/praseodym.org.txt
37 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
38 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
39)
40 37
41_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 38_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
42_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 39_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/email/ccert-policy-server/.envrc b/hosts/surtr/email/ccert-policy-server/.envrc
new file mode 100644
index 00000000..2c909235
--- /dev/null
+++ b/hosts/surtr/email/ccert-policy-server/.envrc
@@ -0,0 +1,4 @@
1use flake
2
3[[ -d ".venv" ]] || ( uv venv && uv sync )
4. .venv/bin/activate
diff --git a/hosts/surtr/email/ccert-policy-server/.gitignore b/hosts/surtr/email/ccert-policy-server/.gitignore
new file mode 100644
index 00000000..4ccfae70
--- /dev/null
+++ b/hosts/surtr/email/ccert-policy-server/.gitignore
@@ -0,0 +1,2 @@
1.venv
2**/__pycache__
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/ccert-policy-server/poetry.lock b/hosts/surtr/email/ccert-policy-server/poetry.lock
deleted file mode 100644
index acd354e8..00000000
--- a/hosts/surtr/email/ccert-policy-server/poetry.lock
+++ /dev/null
@@ -1,169 +0,0 @@
1# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
2
3[[package]]
4name = "psycopg"
5version = "3.1.8"
6description = "PostgreSQL database adapter for Python"
7category = "main"
8optional = false
9python-versions = ">=3.7"
10files = [
11 {file = "psycopg-3.1.8-py3-none-any.whl", hash = "sha256:b1500c42063abaa01d30b056f0b300826b8dd8d586900586029a294ce74af327"},
12 {file = "psycopg-3.1.8.tar.gz", hash = "sha256:59b4a71536b146925513c0234dfd1dc42b81e65d56ce5335dff4813434dbc113"},
13]
14
15[package.dependencies]
16typing-extensions = ">=4.1"
17tzdata = {version = "*", markers = "sys_platform == \"win32\""}
18
19[package.extras]
20binary = ["psycopg-binary (>=3.1.6,<=3.1.8)"]
21c = ["psycopg-c (>=3.1.6,<=3.1.8)"]
22dev = ["black (>=22.3.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=0.990)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
23docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
24pool = ["psycopg-pool"]
25test = ["mypy (>=0.990)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-asyncio (>=0.17)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
26
27[[package]]
28name = "psycopg-binary"
29version = "3.1.8"
30description = "PostgreSQL database adapter for Python -- C optimisation distribution"
31category = "main"
32optional = false
33python-versions = ">=3.7"
34files = [
35 {file = "psycopg_binary-3.1.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f32684b4fc3863190c4b9c141342b2cbdb81632731b9c68e6946d772ba0560f2"},
36 {file = "psycopg_binary-3.1.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37212244817b3cc7193ee4b5d60765c020ead5e53589c935d249bfb96452878b"},
37 {file = "psycopg_binary-3.1.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f2563db6e44372f593a76c94452ce476306e0fb508e092f3fab4d9091a9974"},
38 {file = "psycopg_binary-3.1.8-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b36fcc67d8b23935ee871a6331c9631ecfdb11452a64f34b8ecb9642de43aec8"},
39 {file = "psycopg_binary-3.1.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8bb9f577a09e799322008e574a1671c5b2645e990f954be2b7dae669e3779750"},
40 {file = "psycopg_binary-3.1.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ac81e68262b03163ca977f34448b4cadbc49db929146406b4706fe2141d76d1"},
41 {file = "psycopg_binary-3.1.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fbfc9ae4edfb76c14d09bd70d6f399eb935008bbb3bc4cd6a4ab76645ba3443e"},
42 {file = "psycopg_binary-3.1.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8602836138bc209aa5f9821c8e8439466f151c3ec4fcdbc740697e49cff1b920"},
43 {file = "psycopg_binary-3.1.8-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:9cf94411f5a9064cf4ab1066976a7bce44f970f9603a01585c1040465eb312f9"},
44 {file = "psycopg_binary-3.1.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a8fee8d846f9614331bd764850b4c1363730d36e88e14aa28ec4639318fd2093"},
45 {file = "psycopg_binary-3.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:2d5ae85c6037e45862e304d39ec24a24ddebc7d2b5b3601155dddc07c19c0cdc"},
46 {file = "psycopg_binary-3.1.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17d187743d8ca63d24fa724bfee76e50b6473f1fef998cebcd35348b0d5936de"},
47 {file = "psycopg_binary-3.1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3762e73b6743139c5258d8b3a294edb309c691ba4f172c9f272315501390e7c2"},
48 {file = "psycopg_binary-3.1.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87973d064a72bc2716309381b713f49f57c48100fb1f046943b780a04bc011f6"},
49 {file = "psycopg_binary-3.1.8-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f8400d400f64f659a897d1ef67212012524cc44882bd24387515df9bb723364"},
50 {file = "psycopg_binary-3.1.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f45766ce8e74eb456d8672116e936391e67290c50fd0cc1b41876b61261869b6"},
51 {file = "psycopg_binary-3.1.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ecf37c6348232073ea62b0630655479021f855635f72b4170693032993cdaf"},
52 {file = "psycopg_binary-3.1.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:10b8f1f96f5e8f02a60ba76dab315d3e71cb76c18ff49aa18bbf48a8089c3202"},
53 {file = "psycopg_binary-3.1.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:58cb0d007768dbccb67783baacf1c4016c7be8a494339a514321edee3d3b787a"},
54 {file = "psycopg_binary-3.1.8-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:59d8dbea1bc3dbbc819c0320cb2b641dc362389b096098c62172f49605f58284"},
55 {file = "psycopg_binary-3.1.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4325cee1641c25719bcf063f7683e909cb8cc9932ace3f8bf20ce112e47ce743"},
56 {file = "psycopg_binary-3.1.8-cp311-cp311-win_amd64.whl", hash = "sha256:064502d191d7bc32a48670cc605ce49abcdb5e01e2697ee3fe546cff330fb8ae"},
57 {file = "psycopg_binary-3.1.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5fd8492931865cc7181169b2dbf472377a5b5808f001e73f5c25b05bb61e9622"},
58 {file = "psycopg_binary-3.1.8-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4d1a4ea2ca20f0bc944bc28e4addb80e6a22ac60a85fc7035e57c88e96f3a18"},
59 {file = "psycopg_binary-3.1.8-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c27be5ddf4a05146ae7fb8429e9367dad0dc278a7d0e2f5094dd533195c4f8a1"},
60 {file = "psycopg_binary-3.1.8-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa8ca48a35be0f9880ed2093c213f07d318fa9389a2b9194196c239e41a77841"},
61 {file = "psycopg_binary-3.1.8-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf59e1d06f420930fc4c16a42ed6476c60c83976c82e53012dbca45f009d5978"},
62 {file = "psycopg_binary-3.1.8-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cb3013b76cbab4a903f3b9c87f4518335627cb05fd89f9e04520c1743c2b919b"},
63 {file = "psycopg_binary-3.1.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:db84eaa9e2d13e37a97dcd39d2fe78e0a3052c9aa67b5f0b4f3d346a155f4d21"},
64 {file = "psycopg_binary-3.1.8-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:2c3d268cf2dbb79e52a555c2e7b26c6df2d014f3fb918d512ffc25ecc9c54582"},
65 {file = "psycopg_binary-3.1.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0fe6205af5f63ee6e4816b267bf06add5934a259cddcf7dfdfc8ed738f5127b2"},
66 {file = "psycopg_binary-3.1.8-cp37-cp37m-win_amd64.whl", hash = "sha256:f99806a5b9a5ba5cb5f46a0fa0440cd721556e0af09a7cadcc39e27ae9b1807e"},
67 {file = "psycopg_binary-3.1.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cc5d5a9b0acbf38e0b4de1c701d235f0cb750ef3de528dedfdbab1a367f2396"},
68 {file = "psycopg_binary-3.1.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:478ecbb774398e5df6ee365a4d0a77f382a65f140e76720909804255c7801d4a"},
69 {file = "psycopg_binary-3.1.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b40b56c5b3ffa8481f7bebb08473602ddb8e2e86ba25bf9261ba428eb7887175"},
70 {file = "psycopg_binary-3.1.8-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:37df8714837d2c701ba4c54462a189b95d1a4439d4d147fb71018560e9a60547"},
71 {file = "psycopg_binary-3.1.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29a38b48cbec8484d83efea4d1d0707e49a3c51a2273cfbaa3d9ba280d3df7d9"},
72 {file = "psycopg_binary-3.1.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1a2209ef4df25f4ed8d91924bd4d9c7028d254e61216366c4b894c8a6ea4f88"},
73 {file = "psycopg_binary-3.1.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:858a794c2d5e984627503581f03cc68cef97ee080993b7b6a0b7b30cb4fac107"},
74 {file = "psycopg_binary-3.1.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:574c8b7b51e8d5c06f27125fc218d1328c018c0c1ad8f1202033aa6897b8ee99"},
75 {file = "psycopg_binary-3.1.8-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e3dc783eedde10f966039ecc5f96f7df25c288ea4f6795d28b990f312c33ff09"},
76 {file = "psycopg_binary-3.1.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:94f9e7ccbfdba1c4f5de80b615187eb47a351ab64a9123d87aea4bf347c1e1d8"},
77 {file = "psycopg_binary-3.1.8-cp38-cp38-win_amd64.whl", hash = "sha256:1425c2cc4cfd4778d9dee578541f11546a93fc2f5c558a0411c94026a1cf94c7"},
78 {file = "psycopg_binary-3.1.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e68e8b8077cd45dd2683fcd9a384e7672b400e26c0c7d04dac0cf0763c12be78"},
79 {file = "psycopg_binary-3.1.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:60b22dd46e4e4f678379cf3388468171c2ecea74e90b1332d173ffa8cd83315f"},
80 {file = "psycopg_binary-3.1.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61a1ccef7e0bf6128a7818c9d22cc850cf7649cee9541e82e4a8c080a734024d"},
81 {file = "psycopg_binary-3.1.8-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e7a7b41eba96c7b9648efee57298f1aa0d96e081dea76489f52113536981712"},
82 {file = "psycopg_binary-3.1.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a161785b1c8e26cd8e8d5436fa39ba2a8af590c17f1741aae11f8076a08485e6"},
83 {file = "psycopg_binary-3.1.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a978d2bea09265eb6ebcd1b8a3aa05ea4118aa4013cb9669e12a8656975385cd"},
84 {file = "psycopg_binary-3.1.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:251d2e6dca112dd359c029f422a025d75e78f2f2af4a2aceff506fdc5120f5f9"},
85 {file = "psycopg_binary-3.1.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a1f052642a54eda53786fa8b72fca2e48ceaf0fc2f3e8709c87694fd7c45ac50"},
86 {file = "psycopg_binary-3.1.8-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:73747e6a5dfb05500ff3857f9b9ee50e4f4f663250454d773b98d818545f10fa"},
87 {file = "psycopg_binary-3.1.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:811d870ca9e97875db92f9b346492c4fa7a9edd74dce3604015dd13389fef46a"},
88 {file = "psycopg_binary-3.1.8-cp39-cp39-win_amd64.whl", hash = "sha256:8a0f425171e95379f1fe93b41d67c6dfe85b6b635944facf07ca26ff7fa8ab1d"},
89]
90
91[[package]]
92name = "psycopg-pool"
93version = "3.1.7"
94description = "Connection Pool for Psycopg"
95category = "main"
96optional = false
97python-versions = ">=3.7"
98files = [
99 {file = "psycopg-pool-3.1.7.tar.gz", hash = "sha256:d02741dc48303495f4021900630442af87d6b1c3bfd1a3ece54cc11aa43d7dde"},
100 {file = "psycopg_pool-3.1.7-py3-none-any.whl", hash = "sha256:ca1f2c366b5910acd400e16e812912827c57836af638c1717ba495111d22073b"},
101]
102
103[package.dependencies]
104typing-extensions = ">=3.10"
105
106[[package]]
107name = "sdnotify"
108version = "0.3.2"
109description = "A pure Python implementation of systemd's service notification protocol (sd_notify)"
110category = "main"
111optional = false
112python-versions = "*"
113files = [
114 {file = "sdnotify-0.3.2.tar.gz", hash = "sha256:73977fc746b36cc41184dd43c3fe81323e7b8b06c2bb0826c4f59a20c56bb9f1"},
115]
116
117[[package]]
118name = "systemd-python"
119version = "235"
120description = "Python interface for libsystemd"
121category = "main"
122optional = false
123python-versions = "*"
124files = [
125 {file = "systemd-python-235.tar.gz", hash = "sha256:4e57f39797fd5d9e2d22b8806a252d7c0106c936039d1e71c8c6b8008e695c0a"},
126]
127
128[[package]]
129name = "systemd-socketserver"
130version = "1.0"
131description = "Socket server implementation that works with systemd socket activation"
132category = "main"
133optional = false
134python-versions = ">=3"
135files = [
136 {file = "systemd_socketserver-1.0-py3-none-any.whl", hash = "sha256:987a8bfbf28d959e7c2966c742ad7bad482f05e121077defcf95bb38267db9a8"},
137]
138
139[package.dependencies]
140systemd-python = "*"
141
142[[package]]
143name = "typing-extensions"
144version = "4.5.0"
145description = "Backported and Experimental Type Hints for Python 3.7+"
146category = "main"
147optional = false
148python-versions = ">=3.7"
149files = [
150 {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"},
151 {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"},
152]
153
154[[package]]
155name = "tzdata"
156version = "2023.3"
157description = "Provider of IANA time zone data"
158category = "main"
159optional = false
160python-versions = ">=2"
161files = [
162 {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"},
163 {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"},
164]
165
166[metadata]
167lock-version = "2.0"
168python-versions = "^3.9"
169content-hash = "caba2a43081cb7820a3d1243e0c4aae70e0604405fbe1601cea99bd93a2f1429"
diff --git a/hosts/surtr/email/ccert-policy-server/pyproject.toml b/hosts/surtr/email/ccert-policy-server/pyproject.toml
index 97a18c65..518bd4f9 100644
--- a/hosts/surtr/email/ccert-policy-server/pyproject.toml
+++ b/hosts/surtr/email/ccert-policy-server/pyproject.toml
@@ -1,20 +1,30 @@
1[tool.poetry] 1[project]
2name = "ccert_policy_server" 2name = "ccert_policy_server"
3version = "0.0.0" 3version = "0.0.0"
4authors = ["Gregor Kleen <gkleen@yggdrasil.li>"]
5description = "" 4description = ""
5authors = [{ name = "Gregor Kleen", email = "gkleen@yggdrasil.li" }]
6requires-python = ">=3.12,<4"
7classifiers = [
8 "Programming Language :: Python :: 3",
9 "Programming Language :: Python :: 3.12",
10 "Programming Language :: Python :: 3.13",
11 "Programming Language :: Python :: 3.14",
12]
13dependencies = [
14 "sdnotify>=0.3.2,<0.4",
15 "systemd-socketserver>=1.0,<2",
16 "psycopg>=3.3,<4",
17 "psycopg-pool>=3.3,<4",
18 "psycopg-binary>=3.3.3,<4",
19]
6 20
7[tool.poetry.scripts] 21[project.scripts]
8ccert-policy-server = "ccert_policy_server.__main__:main" 22ccert-policy-server = "ccert_policy_server.__main__:main"
9 23
10[tool.poetry.dependencies]
11python = "^3.9"
12sdnotify = "^0.3.2"
13systemd-socketserver = "^1.0"
14psycopg = "^3.1.8"
15psycopg-pool = "^3.1.7"
16psycopg-binary = "^3.1.8"
17
18[build-system] 24[build-system]
19requires = ["poetry-core>=1.0.0"] 25requires = ["uv_build>=0.10.9,<0.11.0"]
20build-backend = "poetry.core.masonry.api" \ No newline at end of file 26build-backend = "uv_build"
27
28[tool.uv.build-backend]
29module-root = "."
30module-name = ["ccert_policy_server"]
diff --git a/hosts/surtr/email/ccert-policy-server/uv.lock b/hosts/surtr/email/ccert-policy-server/uv.lock
new file mode 100644
index 00000000..0024400b
--- /dev/null
+++ b/hosts/surtr/email/ccert-policy-server/uv.lock
@@ -0,0 +1,130 @@
1version = 1
2revision = 3
3requires-python = ">=3.12, <4"
4
5[[package]]
6name = "ccert-policy-server"
7version = "0.0.0"
8source = { editable = "." }
9dependencies = [
10 { name = "psycopg" },
11 { name = "psycopg-binary" },
12 { name = "psycopg-pool" },
13 { name = "sdnotify" },
14 { name = "systemd-socketserver" },
15]
16
17[package.metadata]
18requires-dist = [
19 { name = "psycopg", specifier = ">=3.3,<4" },
20 { name = "psycopg-binary", specifier = ">=3.3.3,<4" },
21 { name = "psycopg-pool", specifier = ">=3.3,<4" },
22 { name = "sdnotify", specifier = ">=0.3.2,<0.4" },
23 { name = "systemd-socketserver", specifier = ">=1.0,<2" },
24]
25
26[[package]]
27name = "psycopg"
28version = "3.3.3"
29source = { registry = "https://pypi.org/simple" }
30dependencies = [
31 { name = "typing-extensions", marker = "python_full_version < '3.13'" },
32 { name = "tzdata", marker = "sys_platform == 'win32'" },
33]
34sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" }
35wheels = [
36 { url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
37]
38
39[[package]]
40name = "psycopg-binary"
41version = "3.3.3"
42source = { registry = "https://pypi.org/simple" }
43wheels = [
44 { url = "https://files.pythonhosted.org/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" },
45 { url = "https://files.pythonhosted.org/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" },
46 { url = "https://files.pythonhosted.org/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" },
47 { url = "https://files.pythonhosted.org/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" },
48 { url = "https://files.pythonhosted.org/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" },
49 { url = "https://files.pythonhosted.org/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" },
50 { url = "https://files.pythonhosted.org/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" },
51 { url = "https://files.pythonhosted.org/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" },
52 { url = "https://files.pythonhosted.org/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" },
53 { url = "https://files.pythonhosted.org/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" },
54 { url = "https://files.pythonhosted.org/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" },
55 { url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" },
56 { url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" },
57 { url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" },
58 { url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" },
59 { url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" },
60 { url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" },
61 { url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" },
62 { url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" },
63 { url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" },
64 { url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" },
65 { url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" },
66 { url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" },
67 { url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" },
68 { url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" },
69 { url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" },
70 { url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" },
71 { url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" },
72 { url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" },
73 { url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" },
74 { url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" },
75 { url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" },
76 { url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" },
77]
78
79[[package]]
80name = "psycopg-pool"
81version = "3.3.0"
82source = { registry = "https://pypi.org/simple" }
83dependencies = [
84 { name = "typing-extensions" },
85]
86sdist = { url = "https://files.pythonhosted.org/packages/56/9a/9470d013d0d50af0da9c4251614aeb3c1823635cab3edc211e3839db0bcf/psycopg_pool-3.3.0.tar.gz", hash = "sha256:fa115eb2860bd88fce1717d75611f41490dec6135efb619611142b24da3f6db5", size = 31606, upload-time = "2025-12-01T11:34:33.11Z" }
87wheels = [
88 { url = "https://files.pythonhosted.org/packages/e7/c3/26b8a0908a9db249de3b4169692e1c7c19048a9bc41a4d3209cee7dbb758/psycopg_pool-3.3.0-py3-none-any.whl", hash = "sha256:2e44329155c410b5e8666372db44276a8b1ebd8c90f1c3026ebba40d4bc81063", size = 39995, upload-time = "2025-12-01T11:34:29.761Z" },
89]
90
91[[package]]
92name = "sdnotify"
93version = "0.3.2"
94source = { registry = "https://pypi.org/simple" }
95sdist = { 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" }
96
97[[package]]
98name = "systemd-python"
99version = "235"
100source = { registry = "https://pypi.org/simple" }
101sdist = { 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" }
102
103[[package]]
104name = "systemd-socketserver"
105version = "1.0"
106source = { registry = "https://pypi.org/simple" }
107dependencies = [
108 { name = "systemd-python" },
109]
110wheels = [
111 { 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" },
112]
113
114[[package]]
115name = "typing-extensions"
116version = "4.15.0"
117source = { registry = "https://pypi.org/simple" }
118sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
119wheels = [
120 { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
121]
122
123[[package]]
124name = "tzdata"
125version = "2025.3"
126source = { registry = "https://pypi.org/simple" }
127sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
128wheels = [
129 { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
130]
diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix
index 4666d1d6..8eb19b6b 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
3with lib; 3with lib;
4 4
@@ -15,30 +15,65 @@ 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 };
22 22
23 ccert-policy-server = 23 ccert-policy-server =
24 with pkgs.poetry2nix; 24 let
25 mkPoetryApplication { 25 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./ccert-policy-server; };
26 python = pkgs.python311; 26 pythonSet = flake.lib.pythonSet {
27 27 inherit pkgs;
28 projectDir = cleanPythonSources { src = ./ccert-policy-server; }; 28 python = pkgs.python312;
29 29 overlay = workspace.mkPyprojectOverlay {
30 overrides = overrides.withDefaults (self: super: { 30 sourcePreference = "wheel";
31 systemd-python = super.systemd-python.overridePythonAttrs (oldAttrs: { 31 };
32 buildInputs = (oldAttrs.buildInputs or []) ++ [ super.setuptools ]; 32 };
33 }); 33 virtualEnv = pythonSet.mkVirtualEnv "ccert-policy-server-env" workspace.deps.default;
34 }); 34 in virtualEnv.overrideAttrs (oldAttrs: {
35 }; 35 meta = (oldAttrs.meta or {}) // {
36 36 mainProgram = "ccert-policy-server";
37 nftables-nologin-script = pkgs.writeScript "nftables-mail-nologin" '' 37 };
38 #!${pkgs.zsh}/bin/zsh 38 });
39 internal-policy-server =
40 let
41 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./internal-policy-server; };
42 pythonSet = flake.lib.pythonSet {
43 inherit pkgs;
44 python = pkgs.python312;
45 overlay = workspace.mkPyprojectOverlay {
46 sourcePreference = "wheel";
47 };
48 };
49 virtualEnv = pythonSet.mkVirtualEnv "internal-policy-server-env" workspace.deps.default;
50 in virtualEnv.overrideAttrs (oldAttrs: {
51 meta = (oldAttrs.meta or {}) // {
52 mainProgram = "internal-policy-server";
53 };
54 });
55 password-server =
56 let
57 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./password-server; };
58 pythonSet = flake.lib.pythonSet {
59 inherit pkgs;
60 python = pkgs.python3;
61 overlay = workspace.mkPyprojectOverlay {
62 sourcePreference = "wheel";
63 };
64 };
65 virtualEnv = pythonSet.mkVirtualEnv "password-server-env" workspace.deps.default;
66 in virtualEnv.overrideAttrs (oldAttrs: {
67 meta = (oldAttrs.meta or {}) // {
68 mainProgram = "password-server";
69 };
70 });
39 71
72 nftables-nologin-script = pkgs.resholve.writeScript "nftables-mail-nologin" {
73 inputs = with pkgs; [inetutils nftables gnugrep findutils];
74 interpreter = lib.getExe pkgs.zsh;
75 } ''
40 set -e 76 set -e
41 export PATH="${lib.makeBinPath (with pkgs; [inetutils nftables])}:$PATH"
42 77
43 typeset -a as_sets mnt_bys route route6 78 typeset -a as_sets mnt_bys route route6
44 as_sets=(${lib.escapeShellArgs config.services.email.nologin.ASSets}) 79 as_sets=(${lib.escapeShellArgs config.services.email.nologin.ASSets})
@@ -51,7 +86,7 @@ let
51 elif [[ "''${line}" =~ "^route6:\s+(.+)$" ]]; then 86 elif [[ "''${line}" =~ "^route6:\s+(.+)$" ]]; then
52 route6+=($match[1]) 87 route6+=($match[1])
53 fi 88 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) 89 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 90 done
56 for mnt_by in $mnt_bys; do 91 for mnt_by in $mnt_bys; do
57 while IFS=$'\n' read line; do 92 while IFS=$'\n' read line; do
@@ -108,24 +143,20 @@ in {
108 services.postfix = { 143 services.postfix = {
109 enable = true; 144 enable = true;
110 enableSmtp = false; 145 enableSmtp = false;
111 hostname = "surtr.yggdrasil.li";
112 recipientDelimiter = "";
113 setSendmail = true; 146 setSendmail = true;
114 postmasterAlias = ""; rootAlias = ""; extraAliases = ""; 147 postmasterAlias = ""; rootAlias = ""; extraAliases = "";
115 destination = []; 148 settings.main = {
116 sslCert = "/run/credentials/postfix.service/surtr.yggdrasil.li.pem"; 149 recipient_delimiter = "";
117 sslKey = "/run/credentials/postfix.service/surtr.yggdrasil.li.key.pem"; 150 mydestination = [];
118 networks = []; 151 mynetworks = [];
119 config = let 152 myhostname = "surtr.yggdrasil.li";
120 relay_ccert = "texthash:${pkgs.writeText "relay_ccert" ""}"; 153
121 in {
122 smtpd_tls_security_level = "may"; 154 smtpd_tls_security_level = "may";
123 155
124 #the dh params 156 smtpd_tls_chain_files = [
125 smtpd_tls_dh1024_param_file = toString config.security.dhparams.params."postfix-1024".path; 157 "/run/credentials/postfix.service/surtr.yggdrasil.li.full.pem"
126 smtpd_tls_dh512_param_file = toString config.security.dhparams.params."postfix-512".path; 158 ];
127 #enable ECDH 159
128 smtpd_tls_eecdh_grade = "strong";
129 #enabled SSL protocols, don't allow SSLv2 and SSLv3 160 #enabled SSL protocols, don't allow SSLv2 and SSLv3
130 smtpd_tls_protocols = ["!SSLv2" "!SSLv3" "!TLSv1" "!TLSv1.1"]; 161 smtpd_tls_protocols = ["!SSLv2" "!SSLv3" "!TLSv1" "!TLSv1.1"];
131 smtpd_tls_mandatory_protocols = ["!SSLv2" "!SSLv3" "!TLSv1" "!TLSv1.1"]; 162 smtpd_tls_mandatory_protocols = ["!SSLv2" "!SSLv3" "!TLSv1" "!TLSv1.1"];
@@ -155,26 +186,23 @@ in {
155 186
156 smtp_tls_connection_reuse = true; 187 smtp_tls_connection_reuse = true;
157 188
158 tls_server_sni_maps = ''texthash:${pkgs.writeText "sni" ( 189 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 190
165 smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix"; 191 smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix";
166 192
167 local_recipient_maps = ""; 193 local_recipient_maps = "";
168 194
169 # 10 GiB 195 message_size_limit = 10 * 1024 * 1024 * 1024;
170 message_size_limit = "10737418240"; 196 mailbox_size_limit = 10 * 1024 * 1024 * 1024;
171 # 10 GiB
172 mailbox_size_limit = "10737418240";
173 197
174 smtpd_delay_reject = true; 198 smtpd_delay_reject = true;
175 smtpd_helo_required = true; 199 smtpd_helo_required = true;
176 smtpd_helo_restrictions = "permit"; 200 smtpd_helo_restrictions = "permit";
177 201
202 smtpd_sender_restrictions = [
203 "reject_plaintext_session"
204 ];
205
178 smtpd_recipient_restrictions = [ 206 smtpd_recipient_restrictions = [
179 "reject_unauth_pipelining" 207 "reject_unauth_pipelining"
180 "reject_non_fqdn_recipient" 208 "reject_non_fqdn_recipient"
@@ -184,12 +212,12 @@ in {
184 dbname = email 212 dbname = email
185 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' 213 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s'
186 ''}" 214 ''}"
187 "check_ccert_access ${relay_ccert}"
188 "reject_non_fqdn_helo_hostname" 215 "reject_non_fqdn_helo_hostname"
189 "reject_invalid_helo_hostname" 216 "reject_invalid_helo_hostname"
190 "reject_unauth_destination" 217 "reject_unauth_destination"
191 "reject_unknown_recipient_domain" 218 "reject_unknown_recipient_domain"
192 "reject_unverified_recipient" 219 "reject_unverified_recipient"
220 "check_policy_service unix:/run/postfix-internal-policy.sock"
193 ]; 221 ];
194 unverified_recipient_reject_code = "550"; 222 unverified_recipient_reject_code = "550";
195 unverified_recipient_reject_reason = "Recipient address lookup failed"; 223 unverified_recipient_reject_reason = "Recipient address lookup failed";
@@ -204,19 +232,17 @@ in {
204 address_verify_sender_ttl = "30045s"; 232 address_verify_sender_ttl = "30045s";
205 233
206 smtpd_relay_restrictions = [ 234 smtpd_relay_restrictions = [
207 "check_ccert_access ${relay_ccert}"
208 "reject_unauth_destination" 235 "reject_unauth_destination"
209 ]; 236 ];
210 237
211 propagate_unmatched_extensions = ["canonical" "virtual" "alias"]; 238 propagate_unmatched_extensions = ["canonical" "virtual" "alias"];
212 smtpd_authorized_verp_clients = ""; 239 smtpd_authorized_verp_clients = "";
213 authorized_verp_clients = "";
214 240
215 smtpd_client_event_limit_exceptions = ""; 241 smtpd_client_event_limit_exceptions = "";
216 242
217 milter_default_action = "accept"; 243 milter_default_action = "accept";
218 smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock" "local:/run/postsrsd/postsrsd-milter.sock"]; 244 smtpd_milters = ["local:/run/rspamd/rspamd-milter.sock" "local:/run/postsrsd/postsrsd-milter.sock"];
219 non_smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; 245 non_smtpd_milters = ["local:/run/rspamd/rspamd-milter.sock"];
220 246
221 alias_maps = ""; 247 alias_maps = "";
222 248
@@ -227,6 +253,40 @@ in {
227 bounce_queue_lifetime = "20m"; 253 bounce_queue_lifetime = "20m";
228 delay_warning_time = "10m"; 254 delay_warning_time = "10m";
229 255
256 bounce_template_file = pkgs.writeText "bounce.cf" ''
257 failure_template = <<EOF
258 Charset: us-ascii
259 From: Mail Delivery System <MAILER-DAEMON>
260 Subject: Undelivered Mail Returned to Sender
261 Postmaster-Subject: Postmaster Copy: Undelivered Mail
262
263 This is the mail system at host $myhostname.
264
265 I'm sorry to have to inform you that your message could not
266 be delivered to one or more recipients. It's attached below.
267
268 The mail system
269 EOF
270
271 delay_template = <<EOF
272 Charset: us-ascii
273 From: Mail Delivery System <MAILER-DAEMON>
274 Subject: Delayed Mail (still being retried)
275 Postmaster-Subject: Postmaster Warning: Delayed Mail
276
277 This is the mail system at host $myhostname.
278
279 ####################################################################
280 # THIS IS A WARNING ONLY. YOU DO NOT NEED TO RESEND YOUR MESSAGE. #
281 ####################################################################
282
283 Your message could not be delivered for more than $delay_warning_time_minutes minute(s).
284 It will be retried until it is $maximal_queue_lifetime_minutes minute(s) old.
285
286 The mail system
287 EOF
288 '';
289
230 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" '' 290 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" ''
231 # Allow DSN requests from local subnet only 291 # Allow DSN requests from local subnet only
232 192.168.0.0/16 silent-discard 292 192.168.0.0/16 silent-discard
@@ -251,13 +311,26 @@ in {
251 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp"; 311 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp";
252 smtputf8_enable = false; 312 smtputf8_enable = false;
253 313
254 authorized_submit_users = "inline:{ root= postfwd= }"; 314 authorized_submit_users = "inline:{ root= postfwd= ${config.services.dovecot2.settings.mail_uid}= }";
315 authorized_flush_users = "inline:{ root= }";
316 authorized_mailq_users = "inline:{ root= }";
255 317
256 postscreen_access_list = ""; 318 postscreen_access_list = "";
257 postscreen_denylist_action = "drop"; 319 postscreen_denylist_action = "drop";
258 postscreen_greet_action = "enforce"; 320 postscreen_greet_action = "enforce";
321
322 sender_bcc_maps = ''pgsql:${pkgs.writeText "sender_bcc_maps.cf" ''
323 hosts = postgresql:///email
324 dbname = email
325 query = SELECT value FROM sender_bcc_maps WHERE key = '%s'
326 ''}'';
327 recipient_bcc_maps = ''pgsql:${pkgs.writeText "recipient_bcc_maps.cf" ''
328 hosts = postgresql:///email
329 dbname = email
330 query = SELECT value FROM recipient_bcc_maps WHERE key = '%s'
331 ''}'';
259 }; 332 };
260 masterConfig = { 333 settings.master = {
261 "465" = { 334 "465" = {
262 type = "inet"; 335 type = "inet";
263 private = false; 336 private = false;
@@ -280,13 +353,12 @@ in {
280 hosts = postgresql:///email 353 hosts = postgresql:///email
281 dbname = email 354 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')) 355 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}'' 356 ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_tls_all_clientcerts,reject}''
284 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" 357 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject"
285 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" 358 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
286 "-o" "unverified_sender_reject_code=550" 359 "-o" "unverified_sender_reject_code=550"
287 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}" 360 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}"
288 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li" 361 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li"
289 "-o" ''smtpd_milters=${config.services.opendkim.socket}''
290 ]; 362 ];
291 }; 363 };
292 "466" = { 364 "466" = {
@@ -310,13 +382,12 @@ in {
310 hosts = postgresql:///email 382 hosts = postgresql:///email
311 dbname = email 383 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')) 384 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}'' 385 ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_sasl_authenticated,reject}''
314 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject" 386 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject"
315 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" 387 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
316 "-o" "unverified_sender_reject_code=550" 388 "-o" "unverified_sender_reject_code=550"
317 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}" 389 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}"
318 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li" 390 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li"
319 "-o" ''smtpd_milters=${config.services.opendkim.socket}''
320 ]; 391 ];
321 }; 392 };
322 subcleanup = { 393 subcleanup = {
@@ -325,7 +396,10 @@ in {
325 maxproc = 0; 396 maxproc = 0;
326 args = [ 397 args = [
327 "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" '' 398 "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" ''
399 if /^Received: /
400 !/by surtr\.yggdrasil\.li/ STRIP
328 /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1 401 /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1
402 endif
329 ''}" 403 ''}"
330 ]; 404 ];
331 }; 405 };
@@ -364,24 +438,11 @@ in {
364 domains = [ "surtr.yggdrasil.li" ] ++ concatMap (domain: [".${domain}" domain]) emailDomains; 438 domains = [ "surtr.yggdrasil.li" ] ++ concatMap (domain: [".${domain}" domain]) emailDomains;
365 separator = "+"; 439 separator = "+";
366 extraConfig = '' 440 extraConfig = ''
441 socketmap = unix:/run/postsrsd/postsrsd-socketmap.sock
367 milter = unix:/run/postsrsd/postsrsd-milter.sock 442 milter = unix:/run/postsrsd/postsrsd-milter.sock
368 ''; 443 '';
369 }; 444 };
370 445
371 services.opendkim = {
372 enable = true;
373 user = "postfix"; group = "postfix";
374 socket = "local:/run/opendkim/opendkim.sock";
375 domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li"] ++ emailDomains)}'';
376 selector = "surtr";
377 configFile = builtins.toFile "opendkim.conf" ''
378 Syslog true
379 MTA surtr.yggdrasil.li
380 MTACommand ${config.security.wrapperDir}/sendmail
381 LogResults true
382 '';
383 };
384
385 services.rspamd = { 446 services.rspamd = {
386 enable = true; 447 enable = true;
387 workers = { 448 workers = {
@@ -413,6 +474,8 @@ in {
413 milter = yes; 474 milter = yes;
414 timeout = 120s; 475 timeout = 120s;
415 476
477 client_ca_name = "yggdrasil.li";
478
416 upstream "local" { 479 upstream "local" {
417 default = yes; 480 default = yes;
418 self_scan = yes; 481 self_scan = yes;
@@ -449,7 +512,13 @@ in {
449 "redis.conf".text = '' 512 "redis.conf".text = ''
450 servers = "${config.services.redis.servers.rspamd.unixSocket}"; 513 servers = "${config.services.redis.servers.rspamd.unixSocket}";
451 ''; 514 '';
452 "dkim_signing.conf".text = "enabled = false;"; 515 "dkim_signing.conf".text = ''
516 enabled = true;
517 allow_username_mismatch = true;
518
519 path = "/var/lib/rspamd/dkim/$domain.key";
520 selector = "mail";
521 '';
453 "neural.conf".text = "enabled = false;"; 522 "neural.conf".text = "enabled = false;";
454 "classifier-bayes.conf".text = '' 523 "classifier-bayes.conf".text = ''
455 enable = true; 524 enable = true;
@@ -470,242 +539,225 @@ in {
470 spam = true; 539 spam = true;
471 } 540 }
472 ''; 541 '';
542 "logging.inc".text = ''
543 debug_modules = ["milter", "dkim_signing"];
544 '';
473 # "redirectors.inc".text = '' 545 # "redirectors.inc".text = ''
474 # visit.creeper.host 546 # visit.creeper.host
475 # ''; 547 # '';
476 }; 548 };
477 }; 549 };
478 550
479 users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user "dovecot2" ]; 551 users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user config.services.dovecot2.settings.mail_uid ];
480 552
481 services.redis.servers.rspamd.enable = true; 553 services.redis.servers.rspamd.enable = true;
482 554
483 users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ]; 555 users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ];
484 556
485 environment.systemPackages = with pkgs; [ dovecot_pigeonhole dovecot_fts_xapian ]; 557 environment.systemPackages = with pkgs; [ dovecot_pigeonhole ];
486 services.dovecot2 = { 558 services.dovecot2 = {
559 package = pkgs.dovecot;
487 enable = true; 560 enable = true;
488 enablePAM = false; 561 enablePAM = false;
489 sslServerCert = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.pem"; 562 settings = {
490 sslServerKey = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.key.pem"; 563 dovecot_config_version = "2.4.2";
491 sslCACert = toString ./ca/ca.crt; 564 dovecot_storage_version = "2.4.0";
492 mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u";
493 mailPlugins.globally.enable = [ "fts" "fts_xapian" ];
494 protocols = [ "lmtp" "sieve" ];
495 sieve = {
496 extensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"];
497 globalExtensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"];
498 };
499 extraConfig = let
500 dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" ''
501 driver = pgsql
502 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'
504 user_query = SELECT "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n'
505 iterate_query = SELECT "user" FROM imap_user
506 '';
507 in ''
508 mail_home = /var/lib/mail/%u
509
510 mail_plugins = $mail_plugins quota
511
512 first_valid_uid = ${toString config.users.users.dovecot2.uid}
513 last_valid_uid = ${toString config.users.users.dovecot2.uid}
514 first_valid_gid = ${toString config.users.groups.dovecot2.gid}
515 last_valid_gid = ${toString config.users.groups.dovecot2.gid}
516
517 ${concatMapStringsSep "\n\n" (domain:
518 concatMapStringsSep "\n" (subdomain: ''
519 local_name ${subdomain} {
520 ssl_cert = </run/credentials/dovecot2.service/${subdomain}.pem
521 ssl_key = </run/credentials/dovecot2.service/${subdomain}.key.pem
522 }
523 '') ["imap.${domain}" domain]
524 ) emailDomains}
525
526 ssl_require_crl = no
527 ssl_verify_client_cert = yes
528
529 ssl_min_protocol = TLSv1.2
530 ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
531 ssl_prefer_server_ciphers = no
532
533 auth_ssl_username_from_cert = yes
534 ssl_cert_username_field = commonName
535 auth_mechanisms = plain login external
536
537 auth_verbose = yes
538 verbose_ssl = yes
539 auth_debug = yes
540
541 service auth {
542 user = dovecot2
543 }
544 service auth-worker {
545 user = dovecot2
546 }
547 565
548 userdb { 566 sql_driver = "pgsql";
549 driver = prefetch 567 "pgsql /run/postgresql".parameters = {
550 } 568 dbname = "email";
551 userdb { 569 };
552 driver = sql
553 args = ${dovecotSqlConf}
554 }
555 passdb {
556 driver = sql
557 args = ${dovecotSqlConf}
558 }
559
560 protocol lmtp {
561 userdb {
562 driver = sql
563 args = ${pkgs.writeText "dovecot-sql.conf" ''
564 driver = pgsql
565 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
567 ''}
568
569 skip = never
570 result_failure = return-fail
571 result_internalfail = return-fail
572 }
573
574 mail_plugins = $mail_plugins sieve
575 }
576 570
577 mailbox_list_index = yes 571 protocols = {
578 postmaster_address = postmaster@yggdrasil.li 572 imap = true;
579 recipient_delimiter = 573 lmtp = true;
580 auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-+_@ 574 sieve = true;
575 };
581 576
582 service lmtp { 577 mail_plugins = {
583 vsz_limit = 1G 578 quota = true;
579 fts = true;
580 fts_flatcurve = true;
581 };
584 582
585 unix_listener /run/dovecot-lmtp { 583 mail_uid = "dovecot2";
586 mode = 0600 584 mail_gid = "dovecot2";
587 user = postfix 585
588 group = postfix 586 first_valid_uid = config.ids.uids.dovecot2;
589 } 587 last_valid_uid = config.ids.uids.dovecot2;
590 } 588 first_valid_gid = config.ids.gids.dovecot2;
591 service auth { 589 last_valid_gid = config.ids.gids.dovecot2;
592 vsz_limit = 2G 590
591 mail_driver = "maildir";
592 mail_path = "/var/lib/mail/%{user}/maildir";
593 mail_index_path = "/var/lib/dovecot/indices/%{user}";
594 ssl_server_ca_file = ./ca/ca.crt;
595 ssl_server_key_file = "/run/credentials/dovecot.service/surtr.yggdrasil.li.key.pem";
596 ssl_server_cert_file = "/run/credentials/dovecot.service/surtr.yggdrasil.li.pem";
597
598 mail_home = "/var/lib/mail/%{user}";
599
600 ssl_server_require_crl = false;
601 ssl_server_request_client_cert = true;
602
603 ssl_min_protocol = "TLSv1.2";
604 ssl_curve_list = "X25519MLKEM768:X25519";
605 ssl_cipher_list = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305";
606
607 auth_ssl_username_from_cert = "yes";
608 ssl_server_cert_username_field = "commonName";
609 auth_mechanisms = ["plain" "login" "external"];
610
611 log_debug = "category=ssl OR category=auth";
612 auth_verbose = true;
613
614 "service auth-worker".user = "$SET:default_internal_user";
615 "userdb prefetch" = {};
616 "userdb sql" = {
617 sql_query = "SELECT \"user\", quota_rule, '${config.services.dovecot2.settings.mail_uid}' as uid, 'dovecot2' as gid FROM imap_user WHERE \"user\" = '%{user | username}'";
618 sql_iterate_query = "SELECT \"user\" FROM imap_user";
619 fields = {
620 uid = "$SET:default_internal_user";
621 gid = "$SET:default_internal_user";
622 };
623 };
624 "passdb sql" = {
625 sql_query = ''
626 SELECT (CASE WHEN '%{cert}' = 'valid' AND '%{mechanism}' = 'EXTERNAL' THEN NULL ELSE "password" END) as password, (CASE WHEN '%{cert}' = 'valid' AND '%{mechanism}' = 'EXTERNAL' THEN true WHEN password IS NULL THEN true ELSE NULL END) as nopassword, "user", quota_rule, '${config.services.dovecot2.settings.mail_uid}' as uid, '${config.services.dovecot2.settings.mail_gid}' as gid FROM imap_user WHERE "user" = '%{user | username}'
627 '';
628 };
593 629
594 unix_listener /run/dovecot-sasl { 630 "protocol lmtp" = {
595 mode = 0600 631 mail_plugins.sieve = true;
596 user = postfix 632 "userdb sql-lmtp" = {
597 group = postfix 633 driver = "sql";
598 } 634 sql_query = ''
599 } 635 SELECT DISTINCT ON (extension IS NULL, local IS NULL) "user", quota_rule, '${config.services.dovecot2.settings.mail_uid}' as uid, '${config.services.dovecot2.settings.mail_gid}' as gid FROM lmtp_mapping WHERE CASE WHEN extension IS NOT NULL AND local IS NOT NULL THEN ('%{user | username}' :: citext) = local || '+' || extension AND domain = ('%{user | domain}' :: citext) WHEN local IS NOT NULL THEN (local = ('%{user | username}' :: citext) OR ('%{user | username}' :: citext) ILIKE local || '+%%') AND domain = ('%{user | domain}' :: citext) WHEN extension IS NOT NULL THEN ('%{user | username}' :: citext) ILIKE '%%+' || extension AND domain = ('%{user | domain}' :: citext) ELSE domain = ('%{user | domain}' :: citext) END ORDER BY (extension IS NULL) ASC, (local IS NULL) ASC
636 '';
600 637
601 namespace inbox { 638 skip = "never";
602 separator = / 639 result_failure = "return-fail";
603 inbox = yes 640 result_internalfail = "return-fail";
604 prefix = 641 };
642 };
605 643
606 mailbox Trash { 644 mailbox_list_index = true;
607 auto = no 645 mailbox_list_utf8 = true;
608 special_use = \Trash 646 postmaster_address = "postmaster@yggdrasil.li";
609 } 647 recipient_delimiter = null;
610 mailbox Junk { 648 auth_username_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-+_@";
611 auto = no
612 special_use = \Junk
613 }
614 mailbox Drafts {
615 auto = no
616 special_use = \Drafts
617 }
618 mailbox Sent {
619 auto = subscribe
620 special_use = \Sent
621 }
622 mailbox "Sent Messages" {
623 auto = no
624 special_use = \Sent
625 }
626 }
627 649
628 plugin { 650 "service lmtp" = {
629 quota = count 651 vsz_limit = "1G";
630 quota_rule = *:storage=1GB
631 quota_rule2 = Trash:storage=+10%%
632 quota_status_overquota = "552 5.2.2 Mailbox is full"
633 quota_status_success = DUNNO
634 quota_status_nouser = DUNNO
635 quota_grace = 10%%
636 quota_max_mail_size = ${config.services.postfix.config.message_size_limit}
637 quota_vsizes = yes
638 }
639 652
640 protocol imap { 653 "unix_listener /run/dovecot-lmtp" = {
641 mail_max_userip_connections = 50 654 mode = "0600";
642 mail_plugins = $mail_plugins imap_quota imap_sieve 655 user = "postfix";
643 } 656 group = "postfix";
657 };
658 };
659 "service auth" = {
660 vsz_limit = "2G";
644 661
645 service imap-login { 662 "unix_listener /run/dovecot-sasl" = {
646 inet_listener imap { 663 mode = "0600";
647 port = 0 664 user = "postfix";
648 } 665 group = "postfix";
649 } 666 };
667 };
650 668
651 service managesieve-login { 669 quota_storage_size = "1G";
652 inet_listener sieve { 670 "namespace inbox" = {
653 port = 4190 671 separator = "/";
654 } 672 inbox = true;
655 }
656 673
657 plugin { 674 "mailbox Trash" = {
658 sieve_plugins = sieve_imapsieve sieve_extprograms 675 auto = false;
659 sieve = file:~/sieve;active=~/dovecot.sieve 676 special_use = "\\Trash";
660 sieve_redirect_envelope_from = orig_recipient 677 quota_storage_percentage = "110";
661 sieve_before = /etc/dovecot/sieve_before.d 678 };
679 "mailbox Junk" = {
680 auto = false;
681 special_use = "\\Junk";
682 };
683 "mailbox Drafts" = {
684 auto = false;
685 special_use = "\\Drafts";
686 };
687 "mailbox Sent" = {
688 auto = "subscribe";
689 special_use = "\\Sent";
690 };
691 "mailbox \"Sent Messages\"" = {
692 auto = false;
693 special_use = "\\Sent";
694 };
695 };
662 696
663 sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment 697 quota_status_overquota = "552 5.2.2 Mailbox is full";
664 sieve_pipe_bin_dir = ${dovecotSievePipeBin}/pipe/bin 698 quota_status_success = "DUNNO";
699 quota_status_nouser = "DUNNO";
700 quota_storage_grace = "100M";
701 quota_mail_size = 10 * 1024 * 1024 * 1024;
665 702
666 imapsieve_mailbox1_name = * 703 sieve_plugins = {
667 imapsieve_mailbox1_causes = FLAG 704 "sieve_imapsieve" = true;
668 imapsieve_mailbox1_before = /etc/dovecot/sieve_flag.d/learn-junk.sieve 705 "sieve_extprograms" = true;
669 } 706 };
707 sieve_redirect_envelope_from = "orig_recipient";
708 sieve_extensions = {
709 imapsieve = true;
710 vacation-seconds = true;
711 "vnd.dovecot.debug" = true;
712 };
713 sieve_global_extensions = {
714 "vnd.dovecot.pipe" = true;
715 "vnd.dovecot.environment" = true;
716 };
717 sieve_pipe_bin_dir = "${dovecotSievePipeBin}/pipe/bin";
670 718
671 plugin { 719 "sieve_script before" = {
672 plugin = fts fts_xapian 720 type = "before";
673 fts = xapian 721 path = "/etc/dovecot/sieve_before.d";
674 fts_xapian = partial=2 full=20 attachments=1 verbose=1 722 };
723 "sieve_script flag" = {
724 type = "before";
725 cause.flag = true;
726 path = "/etc/dovecot/sieve_flag.d";
727 };
728 "sieve_script personal" = {
729 driver = "file";
730 path = "~/sieve";
731 active_path = "~/dovecot.sieve";
732 };
675 733
676 fts_autoindex = yes 734 "fts flatcurve" = {
735 autoindex = true;
736 };
737 language_tokenizers = ["generic" "email-address"];
738 language_filters = ["normalizer-icu" "snowball" "stopwords"];
739 "language en" = {
740 default = true;
741 filters = ["lowercase" "snowball" "stopwords"];
742 };
743 "language de" = {};
677 744
678 fts_enforced = no 745 "protocol imap" = {
679 } 746 mail_max_userip_connections = 50;
747 mail_plugins = {
748 imap_quota = true;
749 imap_sieve = true;
750 };
751 };
680 752
681 service indexer-worker { 753 "service imap-login"."inet_listener imap".port = 0;
682 vsz_limit = ${toString (1024 * 1024 * 1024)} 754 "service managesieve-login"."inet_listener sieve".port = 4190;
683 }
684 '';
685 };
686 755
687 systemd.services.dovecot-fts-xapian-optimize = { 756 "service indexer-worker".vsz_limit = 1024 * 1024 * 1024;
688 description = "Optimize dovecot indices for fts_xapian"; 757 } // (genAttrs' (concatMap (domain: ["imap.${domain}" domain]) emailDomains) (subdomain: nameValuePair "local_name ${subdomain}" {
689 requisite = [ "dovecot2.service" ]; 758 ssl_server_key_file = "/run/credentials/dovecot.service/${subdomain}.key.pem";
690 after = [ "dovecot2.service" ]; 759 ssl_server_cert_file = "/run/credentials/dovecot.service/${subdomain}.pem";
691 startAt = "*-*-* 22:00:00 Europe/Berlin"; 760 }));
692 serviceConfig = {
693 Type = "oneshot";
694 ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
695 PrivateDevices = true;
696 PrivateNetwork = true;
697 ProtectKernelTunables = true;
698 ProtectKernelModules = true;
699 ProtectControlGroups = true;
700 ProtectHome = true;
701 ProtectSystem = true;
702 PrivateTmp = true;
703 };
704 };
705 systemd.timers.dovecot-fts-xapian-optimize = {
706 timerConfig = {
707 RandomizedDelaySec = 4 * 3600;
708 };
709 }; 761 };
710 762
711 environment.etc = { 763 environment.etc = {
@@ -740,46 +792,31 @@ in {
740 ''; 792 '';
741 }; 793 };
742 794
743 security.dhparams = {
744 params = {
745 "postfix-512".bits = 512;
746 "postfix-1024".bits = 2048;
747
748 "postfix-smtps-512".bits = 512;
749 "postfix-smtps-1024".bits = 2048;
750 };
751 };
752
753 security.acme.rfc2136Domains = { 795 security.acme.rfc2136Domains = {
754 "surtr.yggdrasil.li" = { 796 "surtr.yggdrasil.li" = {
755 restartUnits = [ "postfix.service" "dovecot2.service" ]; 797 restartUnits = [ "postfix.service" "dovecot.service" ];
798 };
799 "pw.bouncy.email" = {
800 restartUnits = [ "nginx.service" ];
756 }; 801 };
757 } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains) 802 } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains)
758 // listToAttrs (concatMap (domain: [ 803 // listToAttrs (concatMap (domain: [
759 (nameValuePair domain { restartUnits = ["postfix.service" "dovecot2.service"]; }) 804 (nameValuePair domain { restartUnits = ["postfix.service" "dovecot.service"]; })
760 (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; }) 805 (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; })
761 (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; }) 806 (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; })
762 (nameValuePair "imap.${domain}" { restartUnits = ["dovecot2.service"]; }) 807 (nameValuePair "imap.${domain}" { restartUnits = ["dovecot.service"]; })
763 (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; }) 808 (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; })
764 ]) emailDomains); 809 ]) emailDomains);
765 810
766 systemd.services.postfix = { 811 systemd.services.postfix = {
767 serviceConfig.LoadCredential = [ 812 serviceConfig.LoadCredential = let
768 "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" 813 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" 814 in [
770 ] ++ concatMap (domain: 815 (tlsCredential "surtr.yggdrasil.li")
771 map (subdomain: "${subdomain}.full.pem:${config.security.acme.certs.${subdomain}.directory}/full.pem") 816 ] ++ concatMap (domain: map tlsCredential [domain "mailin.${domain}" "mailsub.${domain}"]) emailDomains;
772 [domain "mailin.${domain}" "mailsub.${domain}"]
773 ) emailDomains;
774 }; 817 };
775 818
776 systemd.services.dovecot2 = { 819 systemd.services.dovecot = {
777 preStart = ''
778 for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do
779 ${pkgs.dovecot_pigeonhole}/bin/sievec $f
780 done
781 '';
782
783 serviceConfig = { 820 serviceConfig = {
784 LoadCredential = [ 821 LoadCredential = [
785 "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" 822 "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem"
@@ -800,6 +837,11 @@ in {
800 "unix:/run/spm/server.sock" = {}; 837 "unix:/run/spm/server.sock" = {};
801 }; 838 };
802 }; 839 };
840 upstreams.password-server = {
841 servers = {
842 "unix:/run/email-password-server.sock" = {};
843 };
844 };
803 845
804 virtualHosts = listToAttrs (map (domain: nameValuePair "spm.${domain}" { 846 virtualHosts = listToAttrs (map (domain: nameValuePair "spm.${domain}" {
805 forceSSL = true; 847 forceSSL = true;
@@ -843,17 +885,53 @@ in {
843 charset utf-8; 885 charset utf-8;
844 source_charset utf-8; 886 source_charset utf-8;
845 ''; 887 '';
846 root = pkgs.runCommand "mta-sts.${domain}" {} '' 888 root = pkgs.writeTextFile {
847 mkdir -p $out/.well-known 889 name = "mta-sts.${domain}";
848 cp ${pkgs.writeText "mta-sts.${domain}.txt" '' 890 destination = "/.well-known/mta-sts.txt";
891 text = ''
849 version: STSv1 892 version: STSv1
850 mode: enforce 893 mode: enforce
851 max_age: 2419200 894 max_age: 2419200
852 mx: mailin.${domain} 895 mx: mailin.${domain}
853 ''} $out/.well-known/mta-sts.txt 896 '';
854 ''; 897 };
898 };
899 }) emailDomains) // {
900 "pw.bouncy.email" = {
901 forceSSL = true;
902 kTLS = true;
903 http3 = false;
904 sslCertificate = "/run/credentials/nginx.service/pw.bouncy.email.pem";
905 sslCertificateKey = "/run/credentials/nginx.service/pw.bouncy.email.key.pem";
906 extraConfig = ''
907 ssl_stapling off;
908 ssl_verify_client optional;
909 ssl_client_certificate ${toString ./ca/ca.crt};
910 '';
911 locations = {
912 "@backend" = {
913 proxyPass = "http://password-server";
914
915 extraConfig = ''
916 proxy_set_header SSL-CLIENT-VERIFY $ssl_client_verify;
917 proxy_set_header SSL-CLIENT-S-DN $ssl_client_s_dn;
918 '';
919 };
920 "/" = {
921 root = pkgs.symlinkJoin {
922 name = "root";
923 paths = [
924 (pkgs.writeTextDir "robots.txt" ''
925 User-agent: *
926 Disallow: /
927 '')
928 ];
929 };
930 tryFiles = "$uri @backend";
931 };
855 }; 932 };
856 }) emailDomains); 933 };
934 };
857 }; 935 };
858 936
859 systemd.services.nginx.serviceConfig.LoadCredential = concatMap (domain: [ 937 systemd.services.nginx.serviceConfig.LoadCredential = concatMap (domain: [
@@ -863,12 +941,15 @@ in {
863 "mta-sts.${domain}.key.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/key.pem" 941 "mta-sts.${domain}.key.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/key.pem"
864 "mta-sts.${domain}.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/fullchain.pem" 942 "mta-sts.${domain}.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/fullchain.pem"
865 "mta-sts.${domain}.chain.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/chain.pem" 943 "mta-sts.${domain}.chain.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/chain.pem"
866 ]) emailDomains; 944 ]) emailDomains ++ [
945 "pw.bouncy.email.key.pem:${config.security.acme.certs."pw.bouncy.email".directory}/key.pem"
946 "pw.bouncy.email.pem:${config.security.acme.certs."pw.bouncy.email".directory}/fullchain.pem"
947 ];
867 948
868 systemd.services.spm = { 949 systemd.services.spm = {
869 serviceConfig = { 950 serviceConfig = {
870 Type = "notify"; 951 Type = "notify";
871 ExecStart = "${pkgs.spm}/bin/spm-server"; 952 ExecStart = getExe' pkgs.spm "spm-server";
872 User = "spm"; 953 User = "spm";
873 Group = "spm"; 954 Group = "spm";
874 955
@@ -926,7 +1007,7 @@ in {
926 serviceConfig = { 1007 serviceConfig = {
927 Type = "notify"; 1008 Type = "notify";
928 1009
929 ExecStart = "${ccert-policy-server}/bin/ccert-policy-server"; 1010 ExecStart = getExe' ccert-policy-server "ccert-policy-server";
930 1011
931 Environment = [ 1012 Environment = [
932 "PGDATABASE=email" 1013 "PGDATABASE=email"
@@ -959,6 +1040,109 @@ in {
959 }; 1040 };
960 users.groups."postfix-ccert-sender-policy" = {}; 1041 users.groups."postfix-ccert-sender-policy" = {};
961 1042
1043 systemd.sockets."postfix-internal-policy" = {
1044 requiredBy = ["postfix.service"];
1045 wants = ["postfix-internal-policy.service"];
1046 socketConfig = {
1047 ListenStream = "/run/postfix-internal-policy.sock";
1048 };
1049 };
1050 systemd.services."postfix-internal-policy" = {
1051 after = [ "postgresql.service" ];
1052 bindsTo = [ "postgresql.service" ];
1053
1054 serviceConfig = {
1055 Type = "notify";
1056
1057 ExecStart = lib.getExe internal-policy-server;
1058
1059 Environment = [
1060 "PGDATABASE=email"
1061 ];
1062
1063 DynamicUser = false;
1064 User = "postfix-internal-policy";
1065 Group = "postfix-internal-policy";
1066 ProtectSystem = "strict";
1067 SystemCallFilter = "@system-service";
1068 NoNewPrivileges = true;
1069 ProtectKernelTunables = true;
1070 ProtectKernelModules = true;
1071 ProtectKernelLogs = true;
1072 ProtectControlGroups = true;
1073 MemoryDenyWriteExecute = true;
1074 RestrictSUIDSGID = true;
1075 KeyringMode = "private";
1076 ProtectClock = true;
1077 RestrictRealtime = true;
1078 PrivateDevices = true;
1079 PrivateTmp = true;
1080 ProtectHostname = true;
1081 ReadWritePaths = ["/run/postgresql"];
1082 };
1083 };
1084 users.users."postfix-internal-policy" = {
1085 isSystemUser = true;
1086 group = "postfix-internal-policy";
1087 };
1088 users.groups."postfix-internal-policy" = {};
1089
1090 systemd.sockets."email-password-server" = {
1091 requiredBy = ["postfix.service"];
1092 wants = ["email-password-server.service"];
1093 socketConfig = {
1094 ListenStream = "/run/email-password-server.sock";
1095 };
1096 };
1097 systemd.services."email-password-server" = {
1098 after = [ "postgresql.service" ];
1099 bindsTo = [ "postgresql.service" ];
1100
1101 serviceConfig = {
1102 Type = "notify";
1103
1104 ExecStart = getExe' password-server "password-server";
1105
1106 Environment = [
1107 "PGDATABASE=email"
1108 ];
1109
1110 LoadCredential = [
1111 "secret:${config.sops.secrets."email-password-server-secret".path}"
1112 ];
1113
1114 DynamicUser = false;
1115 User = "email-password-server";
1116 Group = "email-password-server";
1117 ProtectSystem = "strict";
1118 SystemCallFilter = "@system-service";
1119 NoNewPrivileges = true;
1120 ProtectKernelTunables = true;
1121 ProtectKernelModules = true;
1122 ProtectKernelLogs = true;
1123 ProtectControlGroups = true;
1124 MemoryDenyWriteExecute = true;
1125 RestrictSUIDSGID = true;
1126 KeyringMode = "private";
1127 ProtectClock = true;
1128 RestrictRealtime = true;
1129 PrivateDevices = true;
1130 PrivateTmp = true;
1131 ProtectHostname = true;
1132 ReadWritePaths = ["/run/postgresql"];
1133 };
1134 };
1135 users.users."email-password-server" = {
1136 isSystemUser = true;
1137 group = "email-password-server";
1138 };
1139 users.groups."email-password-server" = {};
1140 sops.secrets."email-password-server-secret" = {
1141 format = "binary";
1142 sopsFile = ./email-password-server-secret;
1143 };
1144
1145
962 services.postfwd = { 1146 services.postfwd = {
963 enable = true; 1147 enable = true;
964 cache = false; 1148 cache = false;
@@ -992,7 +1176,7 @@ in {
992 wantedBy = [ "nftables.service" ]; 1176 wantedBy = [ "nftables.service" ];
993 1177
994 timerConfig = { 1178 timerConfig = {
995 OnActiveSec = "20h"; 1179 OnCalendar = "4/8:00";
996 RandomizedDelaySec = "8h"; 1180 RandomizedDelaySec = "8h";
997 }; 1181 };
998 }; 1182 };
diff --git a/hosts/surtr/email/email-password-server-secret b/hosts/surtr/email/email-password-server-secret
new file mode 100644
index 00000000..10c376ac
--- /dev/null
+++ b/hosts/surtr/email/email-password-server-secret
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:eQQWZ7AKptyarPz99drDjNC84/7hozMTW/ywlC7D4mDpbzEra7PeSH0PiOBF7uGi9xUf0EVLotJWCAmScFjzsTw=,iv:DKuItQbYFxYQRLRu8AKXFUu5wP3BXc/34UgawfQOuVk=,tag:60PK57Emw0P+RWVZ1UdVPA==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0YUVERm1xSlJtTVFOVG44\nNVJTNE02R3RNUndGZmI2QkJJenFGZVNWU0NZCll3djF3L2pwM2RhdTZtUnBrZTZ6\nSVBwV1JRNzRlcUZFNzEvYjVXeXlDczgKLS0tIERWNEUrRFlNc1MzMVBFWW5SVVZS\nejR5RFBhZEtUVTBHdEJ3M1ZwTnM2cjgKRdmRIsIIIWuLmjQ5NGbdxEkZ4xiJbSvO\nIWUQKzVxcaJQ+6r6IU+Y6+RH8ZoOTJR8jnuEQG0wSDdJflEzz08jsA==\n-----END AGE ENCRYPTED FILE-----\n",
7 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866"
8 },
9 {
10 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHQlhDMmprcW1Mb1RadmZZ\nd0FjTU5KUTZaTW1aTzQ5T0JnZUNna09xYzFjCi85QkQwaWgvSjZ2MWdWSWJTQVhy\nNjAvQXBWc2FKTlhVczVrQUFDYW4rbFUKLS0tIE9FVldDSkwxVDIvSnpoOWFzakdD\nZ0NHZkpPVUNLdmMrUVFFTXREd0xaaFUKZ6vEWHM/oMG1hy0ilaersujGRkfzHobo\nBkmO7bCmdIHe0KEy9g5IebMzyImIa32V83733o2TMMKUqDVmIcFXvw==\n-----END AGE ENCRYPTED FILE-----\n",
11 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq"
12 }
13 ],
14 "lastmodified": "2026-06-06T13:50:08Z",
15 "mac": "ENC[AES256_GCM,data:d0zMcf4lUvjkB0JBzEMCcOZu+3Ln9ll3AbCD9LUrDzWmxuX+j8htj4A6Ss3Ga34AF+KgMU8Cc8LPYwaVNM2StzkQL9sdOKfNFSk1OjLPrCeNDkf/Fl8EeJPqT6lW1bNsaIKX7NDLzYAWgCkwVFzDb9ijYP49GjUNIF+STIYIJfU=,iv:zf4dAqPhWXMaMpkuJzkeo6U0E+tOPt4zOgZ6ixEWfaI=,tag:Py8TvYToKYaKoVGA9uBQPQ==,type:str]",
16 "version": "3.13.1"
17 }
18}
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 @@
1use 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 @@
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 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
66class 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
73def 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
105if __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]
2name = "internal-policy-server"
3version = "0.1.0"
4requires-python = ">=3.12"
5dependencies = [
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]
14internal-policy-server = "internal_policy_server.__main__:main"
15
16[build-system]
17requires = ["hatchling"]
18build-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 @@
1version = 1
2revision = 2
3requires-python = ">=3.12"
4
5[[package]]
6name = "internal-policy-server"
7version = "0.1.0"
8source = { editable = "." }
9dependencies = [
10 { name = "psycopg" },
11 { name = "psycopg-binary" },
12 { name = "psycopg-pool" },
13 { name = "sdnotify" },
14 { name = "systemd-socketserver" },
15]
16
17[package.metadata]
18requires-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]]
27name = "psycopg"
28version = "3.2.9"
29source = { registry = "https://pypi.org/simple" }
30dependencies = [
31 { name = "typing-extensions", marker = "python_full_version < '3.13'" },
32 { name = "tzdata", marker = "sys_platform == 'win32'" },
33]
34sdist = { 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" }
35wheels = [
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]]
40name = "psycopg-binary"
41version = "3.2.9"
42source = { registry = "https://pypi.org/simple" }
43wheels = [
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]]
69name = "psycopg-pool"
70version = "3.2.6"
71source = { registry = "https://pypi.org/simple" }
72dependencies = [
73 { name = "typing-extensions" },
74]
75sdist = { 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" }
76wheels = [
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]]
81name = "sdnotify"
82version = "0.3.2"
83source = { registry = "https://pypi.org/simple" }
84sdist = { 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]]
87name = "systemd-python"
88version = "235"
89source = { registry = "https://pypi.org/simple" }
90sdist = { 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]]
93name = "systemd-socketserver"
94version = "1.0"
95source = { registry = "https://pypi.org/simple" }
96dependencies = [
97 { name = "systemd-python" },
98]
99wheels = [
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]]
104name = "typing-extensions"
105version = "4.13.2"
106source = { registry = "https://pypi.org/simple" }
107sdist = { 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" }
108wheels = [
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]]
113name = "tzdata"
114version = "2025.2"
115source = { registry = "https://pypi.org/simple" }
116sdist = { 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" }
117wheels = [
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]
diff --git a/hosts/surtr/email/password-server/.envrc b/hosts/surtr/email/password-server/.envrc
new file mode 100644
index 00000000..2c909235
--- /dev/null
+++ b/hosts/surtr/email/password-server/.envrc
@@ -0,0 +1,4 @@
1use flake
2
3[[ -d ".venv" ]] || ( uv venv && uv sync )
4. .venv/bin/activate
diff --git a/hosts/surtr/email/password-server/.gitignore b/hosts/surtr/email/password-server/.gitignore
new file mode 100644
index 00000000..4ccfae70
--- /dev/null
+++ b/hosts/surtr/email/password-server/.gitignore
@@ -0,0 +1,2 @@
1.venv
2**/__pycache__
diff --git a/hosts/surtr/email/password-server/password_server/__init__.py b/hosts/surtr/email/password-server/password_server/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/hosts/surtr/email/password-server/password_server/__init__.py
diff --git a/hosts/surtr/email/password-server/password_server/__main__.py b/hosts/surtr/email/password-server/password_server/__main__.py
new file mode 100644
index 00000000..f2321080
--- /dev/null
+++ b/hosts/surtr/email/password-server/password_server/__main__.py
@@ -0,0 +1,268 @@
1from systemd.daemon import listen_fds
2from sdnotify import SystemdNotifier
3import sys
4import logging
5from threading import Thread
6import uvicorn
7import argparse
8from functools import partial
9from inspect import signature
10from starlette.applications import Starlette
11from starlette.routing import Route, Mount
12from starlette.responses import PlainTextResponse, HTMLResponse
13from starlette.datastructures import State, Secret
14from starlette.middleware import Middleware
15from starlette.middleware.authentication import AuthenticationMiddleware
16from contextlib import asynccontextmanager
17from pathlib import Path
18import os
19from psycopg_pool import ConnectionPool
20from psycopg.rows import namedtuple_row
21from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError, SimpleUser, requires
22from starlette.requests import HTTPConnection
23from cryptography.hazmat.primitives import hashes
24from cryptography.hazmat.primitives.hmac import HMAC
25from cryptography.hazmat.primitives.kdf.hkdf import HKDF
26from cryptography.hazmat.primitives.asymmetric import ed25519
27from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
28import jwt
29from datetime import datetime, timezone, timedelta
30from base64 import b64encode
31from xkcdpass import xkcd_password as xp
32from cryptography.hazmat.primitives import constant_time
33
34class SSLProxyAuthBackend(AuthenticationBackend):
35 def __init__(self, state):
36 self.state = state
37
38 async def authenticate(self, conn):
39 if 'SSL-CLIENT-VERIFY' not in conn.headers or 'SSL-CLIENT-S-DN' not in conn.headers:
40 return
41
42 if conn.headers['SSL-CLIENT-VERIFY'].lower() == 'none':
43 return
44
45 if conn.headers['SSL-CLIENT-VERIFY'].lower() != 'success':
46 raise AuthenticationError('Requires SSL-CLIENT-VERIFY: success')
47
48 username = conn.headers['SSL-CLIENT-S-DN'].removeprefix('CN=')
49 if not username:
50 raise AuthenticationError('Requires SSL-CLIENT-S-DN: CN=(.+)')
51
52 creds = ["authenticated"]
53
54 with self.state.db_pool.connection() as conn:
55 with conn.cursor() as cur:
56 cur.row_factory = namedtuple_row
57
58 cur.execute('SELECT EXISTS(SELECT true from "mailbox" INNER JOIN "password_admin" ON "mailbox".id = "password_admin"."mailbox" where "mailbox"."mailbox" = %(user)s) as "exists"', params = {'user': username})
59 if (row := cur.fetchone()) is not None:
60 if row.exists:
61 creds.append("admin")
62
63 return AuthCredentials(creds), SimpleUser(username)
64
65@requires("admin")
66def gen_jwt(request):
67 if 'mailbox' not in request.query_params or not request.query_params['mailbox']:
68 return PlainTextResponse('Require GET parameter mailbox', status_code=400)
69 username = request.query_params['mailbox']
70
71 private_key = derive_jwk(request.app.state.secret)
72 return PlainTextResponse(jwt.encode(
73 {
74 "exp": datetime.now(tz=timezone.utc) + timedelta(days=2),
75 "sub": username,
76 "pw": current_mailbox_password(db_pool=request.app.state.db_pool, username=username, secret=request.app.state.secret),
77 },
78 private_key,
79 algorithm='EdDSA',
80 ))
81
82async def handle_reset(request):
83 if request.method == "GET":
84 if 'jwt' not in request.query_params:
85 return PlainTextResponse('Require GET parameter jwt', status_code=400)
86 provided_jwt = request.query_params['jwt']
87 else:
88 form = await request.form()
89 if 'jwt' not in form:
90 return PlainTextResponse('Requires POST multipart form parameter jwt', status_code=400)
91 provided_jwt = form['jwt']
92
93 private_key = derive_jwk(request.app.state.secret)
94 claims = jwt.decode(
95 provided_jwt,
96 private_key.public_key(),
97 algorithms=['EdDSA'],
98 )
99 if 'sub' not in claims:
100 return PlainTextResponse('Requires sub claim within provided jwt', status_code=400)
101 username = claims['sub']
102 if 'pw' in claims:
103 password_hash = current_mailbox_password(db_pool=request.app.state.db_pool, username=username, secret=request.app.state.secret)
104 if not constant_time.bytes_eq(password_hash.encode('utf-8'), claims['pw'].encode('utf-8')):
105 return PlainTextResponse('Password was changed since jwt was issued', status_code=403)
106
107 if request.method == "GET":
108 wordfile = xp.locate_wordfile()
109 wordlist = xp.generate_wordlist(wordfile=wordfile)
110 new_password = xp.generate_xkcdpassword(wordlist)
111 new_jwt = jwt.encode(
112 {
113 "exp": datetime.now(tz=timezone.utc) + timedelta(hours=1),
114 "sub": username,
115 "pw": current_mailbox_password(db_pool=request.app.state.db_pool, username=username, secret=request.app.state.secret),
116 "new_pw": new_password,
117 },
118 private_key,
119 algorithm='EdDSA',
120 )
121 return HTMLResponse("<form method=\"POST\"><dl><dt>New password</dt><dd style=\"font-family: monospace; white-space: pre-wrap\">{new_password}</dd></dl><input type=\"hidden\" name=\"jwt\" value=\"{jwt}\"><button>Accept new password</button></form>".format(new_password=new_password, jwt=new_jwt))
122 else:
123 if 'new_pw' not in claims:
124 return PlainTextResponse('Requires new_pw claim within provided jwt', status_code=400)
125 new_password = claims['new_pw']
126 salt = os.urandom(16)
127 kdf = Argon2id(
128 salt=salt,
129 length=32,
130 iterations=3,
131 lanes=1,
132 memory_cost=64 * 1024,
133 )
134 pw_hash = kdf.derive_phc_encoded(new_password.encode('utf-8'))
135 with request.app.state.db_pool.connection() as conn:
136 with conn.cursor() as cur:
137 cur.execute('UPDATE mailbox SET "password" = %(pw_hash)s WHERE "mailbox" = %(user)s', params = {'user': username, 'pw_hash': "{{ARGON2ID.B64}}{}".format(b64encode(pw_hash.encode('utf-8')).decode('utf-8'))})
138 return HTMLResponse("<dl><dt>New password</dt><dd style=\"font-family: monospace; white-space: pre-wrap\">{new_password}</dd></dl>".format(new_password=new_password))
139
140
141def current_mailbox_password(*, db_pool, username, secret):
142 with db_pool.connection() as conn:
143 with conn.cursor() as cur:
144 cur.row_factory = namedtuple_row
145
146 cur.execute('SELECT password FROM "mailbox" WHERE "mailbox" = %(user)s', params = {'user': username})
147 row = cur.fetchone()
148 hmac = HMAC(derive_key(secret, b'hmac'), hashes.BLAKE2b(64))
149 hmac.update((row.password if row.password else '').encode('utf-8') if row else b'')
150 return b64encode(hmac.finalize()).decode('utf-8')
151
152def derive_key(secret, info, length=32):
153 hkdf = HKDF(
154 algorithm=hashes.BLAKE2b(64),
155 length=length,
156 salt=None,
157 info=info,
158 )
159 return hkdf.derive(str(secret).encode('utf-8'))
160
161def derive_jwk(secret):
162 return ed25519.Ed25519PrivateKey.from_private_bytes(derive_key(secret, b'jwk', length=32))
163
164def make_app():
165 state = State()
166 secret_path = Path(os.environ["CREDENTIALS_DIRECTORY"]) / 'secret'
167 with secret_path.open('r') as fh:
168 state.secret = Secret(fh.read().strip())
169 state.db_pool = ConnectionPool(min_size=1)
170 state.db_pool.wait()
171
172 @asynccontextmanager
173 async def lifespan(app):
174 app.state = state
175 yield
176
177 return Starlette(
178 routes=[
179 Route('/', handle_reset, methods=["GET", "POST"]),
180 Mount('/jwt',
181 routes=[
182 Route('/', gen_jwt),
183 ],
184 middleware=[Middleware(AuthenticationMiddleware, backend=SSLProxyAuthBackend(state=state))],
185 ),
186 ],
187 lifespan=lifespan,
188 )
189
190def serve():
191 fds = listen_fds()
192 if fds:
193 app = make_app()
194 for fd in fds:
195 Thread(name=f'Server for fd {fd}', target=partial(uvicorn.run, app=app, fd=fd)).start()
196 else:
197 return 2
198
199 SystemdNotifier().notify('READY=1')
200
201def main():
202 global logger
203 logger = logging.getLogger('uvicorn')
204 console_handler = logging.StreamHandler()
205 console_handler.setFormatter( logging.Formatter('[%(levelname)s](%(name)s): %(message)s') )
206 if sys.stderr.isatty():
207 console_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s](%(name)s): %(message)s') )
208 logger.addHandler(console_handler)
209 logger.setLevel(logging.DEBUG)
210
211 # log uncaught exceptions
212 def log_exceptions(type, value, tb):
213 global logger
214
215 logger.error(value)
216 sys.__excepthook__(type, value, tb) # calls default excepthook
217
218 sys.excepthook = log_exceptions
219
220 def set_default_subparser(self, name, args=None, positional_args=0):
221 """default subparser selection. Call after setup, just before parse_args()
222 name: is the name of the subparser to call by default
223 args: if set is the argument list handed to parse_args()
224
225 , tested with 2.7, 3.2, 3.3, 3.4
226 it works with 2.6 assuming argparse is installed
227 """
228 subparser_found = False
229 for arg in sys.argv[1:]:
230 if arg in ['-h', '--help']: # global help if no subparser
231 break
232 else:
233 for x in self._subparsers._actions:
234 if not isinstance(x, argparse._SubParsersAction):
235 continue
236 for sp_name in x._name_parser_map.keys():
237 if sp_name in sys.argv[1:]:
238 subparser_found = True
239 if not subparser_found:
240 # insert default in last position before global positional
241 # arguments, this implies no global options are specified after
242 # first positional argument
243 if args is None:
244 sys.argv.insert(len(sys.argv) - positional_args, name)
245 else:
246 args.insert(len(args) - positional_args, name)
247
248 argparse.ArgumentParser.set_default_subparser = set_default_subparser
249
250 parser = argparse.ArgumentParser(
251 prog=__name__,
252 )
253 subparsers = parser.add_subparsers()
254 serve_parser = subparsers.add_parser('serve')
255 serve_parser.set_defaults(cmd = serve)
256 parser.set_default_subparser('serve')
257 args = parser.parse_args()
258
259 return args.cmd(
260 **{
261 k: v
262 for k, v in vars(args).items()
263 if k in signature(args.cmd).parameters.keys()
264 }
265 )
266
267if __name__ == '__main__':
268 sys.exit(main())
diff --git a/hosts/surtr/email/password-server/pyproject.toml b/hosts/surtr/email/password-server/pyproject.toml
new file mode 100644
index 00000000..196d6b3c
--- /dev/null
+++ b/hosts/surtr/email/password-server/pyproject.toml
@@ -0,0 +1,28 @@
1[project]
2name = "password-server"
3version = "0.1.0"
4requires-python = ">=3.13"
5dependencies = [
6 "cryptography>=48.0.0",
7 "psycopg>=3.3.4",
8 "psycopg-binary>=3.3.4",
9 "psycopg-pool>=3.3.1",
10 "pyjwt[crypto]>=2.13.0",
11 "python-multipart>=0.0.32",
12 "sdnotify>=0.3.2",
13 "starlette>=1.2.1",
14 "systemd-python>=235",
15 "uvicorn>=0.49.0",
16 "xkcdpass>=1.30.0",
17]
18
19[project.scripts]
20password-server = "password_server.__main__:main"
21
22[build-system]
23requires = ["uv_build>=0.10.9,<0.11.0"]
24build-backend = "uv_build"
25
26[tool.uv.build-backend]
27module-root = "."
28module-name = ["password_server"]
diff --git a/hosts/surtr/email/password-server/uv.lock b/hosts/surtr/email/password-server/uv.lock
new file mode 100644
index 00000000..b69f382e
--- /dev/null
+++ b/hosts/surtr/email/password-server/uv.lock
@@ -0,0 +1,334 @@
1version = 1
2revision = 3
3requires-python = ">=3.13"
4
5[[package]]
6name = "anyio"
7version = "4.13.0"
8source = { registry = "https://pypi.org/simple" }
9dependencies = [
10 { name = "idna" },
11]
12sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
13wheels = [
14 { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
15]
16
17[[package]]
18name = "cffi"
19version = "2.0.0"
20source = { registry = "https://pypi.org/simple" }
21dependencies = [
22 { name = "pycparser", marker = "implementation_name != 'PyPy'" },
23]
24sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
25wheels = [
26 { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
27 { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
28 { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
29 { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
30 { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
31 { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
32 { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
33 { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
34 { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
35 { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
36 { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
37 { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
38 { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
39 { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
40 { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
41 { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
42 { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
43 { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
44 { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
45 { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
46 { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
47 { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
48 { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
49 { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
50 { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
51 { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
52 { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
53 { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
54 { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
55 { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
56 { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
57 { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
58 { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
59 { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
60]
61
62[[package]]
63name = "click"
64version = "8.4.1"
65source = { registry = "https://pypi.org/simple" }
66dependencies = [
67 { name = "colorama", marker = "sys_platform == 'win32'" },
68]
69sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
70wheels = [
71 { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
72]
73
74[[package]]
75name = "colorama"
76version = "0.4.6"
77source = { registry = "https://pypi.org/simple" }
78sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
79wheels = [
80 { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
81]
82
83[[package]]
84name = "cryptography"
85version = "48.0.0"
86source = { registry = "https://pypi.org/simple" }
87dependencies = [
88 { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
89]
90sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
91wheels = [
92 { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
93 { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
94 { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
95 { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
96 { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
97 { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
98 { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
99 { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
100 { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
101 { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
102 { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
103 { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
104 { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
105 { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
106 { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
107 { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
108 { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
109 { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
110 { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
111 { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
112 { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
113 { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
114 { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
115 { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
116 { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
117 { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
118 { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
119 { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
120 { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
121 { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
122 { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
123 { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
124 { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
125 { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
126 { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
127 { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
128 { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
129 { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
130 { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
131 { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
132 { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
133 { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
134]
135
136[[package]]
137name = "h11"
138version = "0.16.0"
139source = { registry = "https://pypi.org/simple" }
140sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
141wheels = [
142 { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
143]
144
145[[package]]
146name = "idna"
147version = "3.18"
148source = { registry = "https://pypi.org/simple" }
149sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
150wheels = [
151 { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
152]
153
154[[package]]
155name = "password-server"
156version = "0.1.0"
157source = { editable = "." }
158dependencies = [
159 { name = "cryptography" },
160 { name = "psycopg" },
161 { name = "psycopg-binary" },
162 { name = "psycopg-pool" },
163 { name = "pyjwt", extra = ["crypto"] },
164 { name = "python-multipart" },
165 { name = "sdnotify" },
166 { name = "starlette" },
167 { name = "systemd-python" },
168 { name = "uvicorn" },
169 { name = "xkcdpass" },
170]
171
172[package.metadata]
173requires-dist = [
174 { name = "cryptography", specifier = ">=48.0.0" },
175 { name = "psycopg", specifier = ">=3.3.4" },
176 { name = "psycopg-binary", specifier = ">=3.3.4" },
177 { name = "psycopg-pool", specifier = ">=3.3.1" },
178 { name = "pyjwt", extras = ["crypto"], specifier = ">=2.13.0" },
179 { name = "python-multipart", specifier = ">=0.0.32" },
180 { name = "sdnotify", specifier = ">=0.3.2" },
181 { name = "starlette", specifier = ">=1.2.1" },
182 { name = "systemd-python", specifier = ">=235" },
183 { name = "uvicorn", specifier = ">=0.49.0" },
184 { name = "xkcdpass", specifier = ">=1.30.0" },
185]
186
187[[package]]
188name = "psycopg"
189version = "3.3.4"
190source = { registry = "https://pypi.org/simple" }
191dependencies = [
192 { name = "tzdata", marker = "sys_platform == 'win32'" },
193]
194sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" }
195wheels = [
196 { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" },
197]
198
199[[package]]
200name = "psycopg-binary"
201version = "3.3.4"
202source = { registry = "https://pypi.org/simple" }
203wheels = [
204 { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" },
205 { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" },
206 { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" },
207 { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" },
208 { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" },
209 { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" },
210 { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" },
211 { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" },
212 { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" },
213 { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" },
214 { url = "https://files.pythonhosted.org/packages/bd/3b/218efbc9e645becd80cdf651acda05f85cfe546b7a9c0458c7cbc8fe1f74/psycopg_binary-3.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:dbfdb9b6cc79f31104a7b162a2b921b765fcc62af6c00540a167a8de47e4ed38", size = 3564592, upload-time = "2026-05-01T23:30:31.764Z" },
215 { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" },
216 { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" },
217 { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" },
218 { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" },
219 { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" },
220 { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" },
221 { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" },
222 { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" },
223 { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" },
224 { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" },
225 { url = "https://files.pythonhosted.org/packages/eb/e6/5fff07a70d1f945ed90ae131c3bd76cab32beff7c58c6db15ad5820b6d1f/psycopg_binary-3.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:c37e024c07308cd06cf3ec51bfd0e7f6157585a4d84d1bce4a7f5f7913719bf8", size = 3666849, upload-time = "2026-05-01T23:31:51.165Z" },
226]
227
228[[package]]
229name = "psycopg-pool"
230version = "3.3.1"
231source = { registry = "https://pypi.org/simple" }
232dependencies = [
233 { name = "typing-extensions" },
234]
235sdist = { url = "https://files.pythonhosted.org/packages/90/82/7a23d26039827ecd4ebe93905651029ddd307c5182ad59296dfb6f67b528/psycopg_pool-3.3.1.tar.gz", hash = "sha256:b10b10b7a175d5cc1592147dc5b7eec8a9e0834eb3ed2c4a92c858e2f51eb63c", size = 31661, upload-time = "2026-05-01T23:31:59.809Z" }
236wheels = [
237 { url = "https://files.pythonhosted.org/packages/37/ed/89c2c620af0e1660354cd8aabf9f5b21f911597ce22acb37c805d6c86bc8/psycopg_pool-3.3.1-py3-none-any.whl", hash = "sha256:2af5b432941c4c9ad5c87b3fa410aec910ec8f7c122855897983a06c45f2e4b5", size = 40023, upload-time = "2026-05-01T23:31:53.136Z" },
238]
239
240[[package]]
241name = "pycparser"
242version = "3.0"
243source = { registry = "https://pypi.org/simple" }
244sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
245wheels = [
246 { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
247]
248
249[[package]]
250name = "pyjwt"
251version = "2.13.0"
252source = { registry = "https://pypi.org/simple" }
253sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
254wheels = [
255 { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
256]
257
258[package.optional-dependencies]
259crypto = [
260 { name = "cryptography" },
261]
262
263[[package]]
264name = "python-multipart"
265version = "0.0.32"
266source = { registry = "https://pypi.org/simple" }
267sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
268wheels = [
269 { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
270]
271
272[[package]]
273name = "sdnotify"
274version = "0.3.2"
275source = { registry = "https://pypi.org/simple" }
276sdist = { 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" }
277
278[[package]]
279name = "starlette"
280version = "1.2.1"
281source = { registry = "https://pypi.org/simple" }
282dependencies = [
283 { name = "anyio" },
284]
285sdist = { url = "https://files.pythonhosted.org/packages/25/44/ec35f1b6e83094b997da438a02c8c9b0ade2b1e84cfc48bd4656780760a6/starlette-1.2.1.tar.gz", hash = "sha256:9b9b5ebb992e67d6093741e63c2f59e4f6fff986f81163c087867bd7b924b3f6", size = 2701854, upload-time = "2026-05-31T01:07:51.847Z" }
286wheels = [
287 { url = "https://files.pythonhosted.org/packages/1c/54/196d0c1db10af76baa4f64894448505d60d3cdf70ef92cbb35f46a4e4c71/starlette-1.2.1-py3-none-any.whl", hash = "sha256:4de0082d08c8f6764a85a54cf1120d6939507a19905c7768acad2a9f875d2b89", size = 73350, upload-time = "2026-05-31T01:07:50.09Z" },
288]
289
290[[package]]
291name = "systemd-python"
292version = "235"
293source = { registry = "https://pypi.org/simple" }
294sdist = { 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" }
295
296[[package]]
297name = "typing-extensions"
298version = "4.15.0"
299source = { registry = "https://pypi.org/simple" }
300sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
301wheels = [
302 { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
303]
304
305[[package]]
306name = "tzdata"
307version = "2026.2"
308source = { registry = "https://pypi.org/simple" }
309sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
310wheels = [
311 { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
312]
313
314[[package]]
315name = "uvicorn"
316version = "0.49.0"
317source = { registry = "https://pypi.org/simple" }
318dependencies = [
319 { name = "click" },
320 { name = "h11" },
321]
322sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
323wheels = [
324 { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
325]
326
327[[package]]
328name = "xkcdpass"
329version = "1.30.0"
330source = { registry = "https://pypi.org/simple" }
331sdist = { url = "https://files.pythonhosted.org/packages/18/98/bdd7df66d995eab38887a8eb0afb023750b0c590eb7d8545a7b722f683ef/xkcdpass-1.30.0.tar.gz", hash = "sha256:8a3a6b60255da40d0e5c812458280278c82d2c1cb90e48afbd6777dbbf8795c3", size = 2763380, upload-time = "2026-01-11T16:09:15.567Z" }
332wheels = [
333 { url = "https://files.pythonhosted.org/packages/6b/be/ea93adc1b4597b62c236d61dc6cf0e26ca8a729cb5afae4dc5acc5b33fa8/xkcdpass-1.30.0-py3-none-any.whl", hash = "sha256:3653a4a1e13de230808bcaf11f8c04207a5d3df8e2f7e1de698e11c262b5b797", size = 2746372, upload-time = "2026-01-12T14:48:30.627Z" },
334]
diff --git a/hosts/surtr/http/default.nix b/hosts/surtr/http/default.nix
index f3a7154e..0e13acf7 100644
--- a/hosts/surtr/http/default.nix
+++ b/hosts/surtr/http/default.nix
@@ -2,19 +2,16 @@
2{ 2{
3 imports = [ 3 imports = [
4 ./webdav 4 ./webdav
5 ./online.nix
5 ]; 6 ];
6 7
7 config = { 8 config = {
8 services.nginx = { 9 services.nginx = {
9 enable = true; 10 enable = true;
10 package = pkgs.nginxQuic;
11 recommendedGzipSettings = false; 11 recommendedGzipSettings = false;
12 recommendedProxySettings = true; 12 recommendedProxySettings = true;
13 recommendedTlsSettings = true; 13 recommendedTlsSettings = true;
14 sslDhparam = config.security.dhparams.params.nginx.path;
15 commonHttpConfig = '' 14 commonHttpConfig = ''
16 ssl_ecdh_curve X448:X25519:prime256v1:secp521r1:secp384r1;
17
18 log_format main 15 log_format main
19 '$remote_addr "$remote_user" ' 16 '$remote_addr "$remote_user" '
20 '"$host" "$request" $status $bytes_sent ' 17 '"$host" "$request" $status $bytes_sent '
diff --git a/hosts/surtr/http/online.nix b/hosts/surtr/http/online.nix
new file mode 100644
index 00000000..daad65d9
--- /dev/null
+++ b/hosts/surtr/http/online.nix
@@ -0,0 +1,29 @@
1{ config, ... }:
2{
3 config = {
4 services.nginx.virtualHosts."online.yggdrasil.li" = {
5 forceSSL = true;
6 kTLS = true;
7 http3 = true;
8 sslCertificate = "/run/credentials/nginx.service/online.yggdrasil.li.pem";
9 sslCertificateKey = "/run/credentials/nginx.service/online.yggdrasil.li.key.pem";
10 sslTrustedCertificate = "/run/credentials/nginx.service/online.yggdrasil.li.chain.pem";
11
12 locations."/".extraConfig = ''
13 add_header X-NetworkManager-Status online;
14 add_header Cache-Control "max-age=0, must-revalidate";
15 return 204;
16 '';
17 };
18 security.acme.rfc2136Domains."online.yggdrasil.li" = {
19 restartUnits = ["nginx.service"];
20 };
21 systemd.services.nginx.serviceConfig = {
22 LoadCredential = [
23 "online.yggdrasil.li.key.pem:${config.security.acme.certs."online.yggdrasil.li".directory}/key.pem"
24 "online.yggdrasil.li.pem:${config.security.acme.certs."online.yggdrasil.li".directory}/fullchain.pem"
25 "online.yggdrasil.li.chain.pem:${config.security.acme.certs."online.yggdrasil.li".directory}/chain.pem"
26 ];
27 };
28 };
29}
diff --git a/hosts/surtr/kimai.nix b/hosts/surtr/kimai.nix
new file mode 100644
index 00000000..454b3d80
--- /dev/null
+++ b/hosts/surtr/kimai.nix
@@ -0,0 +1,68 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "kimai.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."kimai" = {
13 servers = {
14 "[2a03:4000:52:ada:6::2]:80" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "kimai.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/kimai.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/kimai.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/kimai.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://kimai;
35
36 proxy_http_version 1.1;
37 proxy_set_header Upgrade $http_upgrade;
38 proxy_set_header Connection "upgrade";
39
40 proxy_redirect off;
41 proxy_set_header Host $host;
42 proxy_set_header X-Real-IP $remote_addr;
43 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44 proxy_set_header X-Forwarded-Host $server_name;
45 proxy_set_header X-Forwarded-Proto $scheme;
46
47 client_max_body_size 0;
48 proxy_request_buffering off;
49 proxy_buffering off;
50
51 proxy_read_timeout 300;
52 '';
53 };
54 };
55 };
56 };
57
58 systemd.services.nginx = {
59 serviceConfig = {
60 LoadCredential = [
61 "kimai.yggdrasil.li.key.pem:${config.security.acme.certs."kimai.yggdrasil.li".directory}/key.pem"
62 "kimai.yggdrasil.li.pem:${config.security.acme.certs."kimai.yggdrasil.li".directory}/fullchain.pem"
63 "kimai.yggdrasil.li.chain.pem:${config.security.acme.certs."kimai.yggdrasil.li".directory}/chain.pem"
64 ];
65 };
66 };
67 };
68}
diff --git a/hosts/surtr/matrix/default.nix b/hosts/surtr/matrix/default.nix
index 7a1b968e..26517cd7 100644
--- a/hosts/surtr/matrix/default.nix
+++ b/hosts/surtr/matrix/default.nix
@@ -291,7 +291,6 @@ with lib;
291 realm = "turn.synapse.li"; 291 realm = "turn.synapse.li";
292 cert = "/run/credentials/coturn.service/turn.synapse.li.pem"; 292 cert = "/run/credentials/coturn.service/turn.synapse.li.pem";
293 pkey = "/run/credentials/coturn.service/turn.synapse.li.key.pem"; 293 pkey = "/run/credentials/coturn.service/turn.synapse.li.key.pem";
294 dh-file = config.security.dhparams.params.coturn.path;
295 relay-ips = ["202.61.241.61" "2a03:4000:52:ada::"]; 294 relay-ips = ["202.61.241.61" "2a03:4000:52:ada::"];
296 extraConfig = '' 295 extraConfig = ''
297 # for debugging 296 # for debugging
diff --git a/hosts/surtr/postgresql/default.nix b/hosts/surtr/postgresql/default.nix
index 059f4088..c43d5983 100644
--- a/hosts/surtr/postgresql/default.nix
+++ b/hosts/surtr/postgresql/default.nix
@@ -280,6 +280,85 @@ in {
280 CREATE VIEW imap_user ("user", "password", quota_rule) AS SELECT mailbox.mailbox AS "user", "password", quota_rule FROM mailbox_quota_rule INNER JOIN mailbox ON mailbox_quota_rule.mailbox = mailbox.mailbox; 280 CREATE VIEW imap_user ("user", "password", quota_rule) AS SELECT mailbox.mailbox AS "user", "password", quota_rule FROM mailbox_quota_rule INNER JOIN mailbox ON mailbox_quota_rule.mailbox = mailbox.mailbox;
281 281
282 COMMIT; 282 COMMIT;
283
284 BEGIN;
285 SELECT _v.register_patch('013-internal', ARRAY['000-base'], null);
286
287 ALTER TABLE mailbox_mapping ADD COLUMN internal bool NOT NULL DEFAULT false;
288 CREATE TABLE mailbox_mapping_access (
289 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
290 mailbox_mapping uuid REFERENCES mailbox_mapping(id),
291 mailbox uuid REFERENCES mailbox(id)
292 );
293 CREATE USER "postfix-internal-policy";
294 GRANT CONNECT ON DATABASE "email" TO "postfix-internal-policy";
295 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "postfix-internal-policy";
296 GRANT SELECT ON ALL TABLES IN SCHEMA public TO "postfix-internal-policy";
297
298 COMMIT;
299
300 BEGIN;
301 SELECT _v.register_patch('014-relay', ARRAY['000-base'], null);
302
303 CREATE TABLE relay_access (
304 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
305 mailbox uuid REFERENCES mailbox(id),
306 domain citext NOT NULL CONSTRAINT domain_non_empty CHECK (domain <> ''')
307 );
308
309 COMMIT;
310
311 BEGIN;
312 SELECT _v.register_patch('015-relay-unique', ARRAY['000-base', '014-relay'], null);
313
314 CREATE UNIQUE INDEX relay_unique ON relay_access (mailbox, domain);
315
316 COMMIT;
317
318 BEGIN;
319 SELECT _v.register_patch('015-sender_bcc', null, null);
320
321 CREATE TABLE sender_bcc_maps (
322 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
323 key text NOT NULL CONSTRAINT key_not_empty CHECK (key <> '''),
324 value text NOT NULL CONSTRAINT value_not_empty CHECK (value <> '''),
325 CONSTRAINT key_unique UNIQUE (key)
326 );
327
328 COMMIT;
329
330 BEGIN;
331 SELECT _v.register_patch('016-recipient_bcc', null, null);
332
333 CREATE TABLE recipient_bcc_maps (
334 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
335 key text NOT NULL CONSTRAINT key_not_empty CHECK (key <> '''),
336 value text NOT NULL CONSTRAINT value_not_empty CHECK (value <> '''),
337 CONSTRAINT recipient_bcc_maps_key_unique UNIQUE (key)
338 );
339
340 COMMIT;
341
342 BEGIN;
343 SELECT _v.register_patch('017-password_admin', ARRAY['000-base'], null);
344
345 CREATE TABLE password_admin (
346 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
347 mailbox uuid REFERENCES mailbox(id)
348 );
349 CREATE USER "email-password-server";
350 GRANT CONNECT ON DATABASE "email" TO "email-password-server";
351 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "email-password-server";
352 GRANT SELECT ON ALL TABLES IN SCHEMA public TO "email-password-server";
353
354 COMMIT;
355
356 BEGIN;
357 SELECT _v.register_patch('018-password_admin', ARRAY['000-base', '017-password_admin'], null);
358
359 GRANT UPDATE ON mailbox TO "email-password-server";
360
361 COMMIT;
283 ''} 362 ''}
284 363
285 psql etebase postgres -eXf ${pkgs.writeText "etebase.sql" '' 364 psql etebase postgres -eXf ${pkgs.writeText "etebase.sql" ''
diff --git a/hosts/surtr/tls/default.nix b/hosts/surtr/tls/default.nix
index b1c05888..6621b06d 100644
--- a/hosts/surtr/tls/default.nix
+++ b/hosts/surtr/tls/default.nix
@@ -41,7 +41,7 @@ in {
41 41
42 acceptTerms = true; 42 acceptTerms = true;
43 # DNS challenge is slow 43 # DNS challenge is slow
44 preliminarySelfsigned = true; 44 # preliminarySelfsigned = true;
45 defaults = { 45 defaults = {
46 email = "phikeebaogobaegh@141.li"; 46 email = "phikeebaogobaegh@141.li";
47 # We don't like NIST curves and Let's Encrypt doesn't support 47 # We don't like NIST curves and Let's Encrypt doesn't support
@@ -58,16 +58,18 @@ in {
58 extraDomainNames = optional domainCfg.wildcard "*.${domain}"; 58 extraDomainNames = optional domainCfg.wildcard "*.${domain}";
59 dnsResolver = "127.0.0.1:53"; 59 dnsResolver = "127.0.0.1:53";
60 dnsProvider = "rfc2136"; 60 dnsProvider = "rfc2136";
61 credentialsFile = pkgs.writeText "${domain}_credentials.env" '' 61 environmentFile = pkgs.writeText "${domain}_credentials.env" ''
62 RFC2136_NAMESERVER=127.0.0.1:53 62 RFC2136_NAMESERVER=127.0.0.1:53
63 RFC2136_TSIG_ALGORITHM=hmac-sha256. 63 RFC2136_TSIG_ALGORITHM=hmac-sha256.
64 RFC2136_TSIG_KEY=${domain}_acme_key 64 RFC2136_TSIG_KEY=${domain}_acme_key
65 RFC2136_TSIG_SECRET_FILE=/run/credentials/acme-${domain}.service/${tsigSecretName domain}
66 RFC2136_TTL=0 65 RFC2136_TTL=0
67 RFC2136_PROPAGATION_TIMEOUT=60 66 RFC2136_PROPAGATION_TIMEOUT=60
68 RFC2136_POLLING_INTERVAL=2 67 RFC2136_POLLING_INTERVAL=2
69 RFC2136_SEQUENCE_INTERVAL=1 68 RFC2136_SEQUENCE_INTERVAL=1
70 ''; 69 '';
70 credentialFiles = {
71 RFC2136_TSIG_SECRET_FILE = config.sops.secrets.${tsigSecretName domain}.path;
72 };
71 dnsPropagationCheck = false; 73 dnsPropagationCheck = false;
72 postRun = mkIf (domainCfg.restartUnits != []) '' 74 postRun = mkIf (domainCfg.restartUnits != []) ''
73 systemctl --no-block try-restart ${escapeShellArgs domainCfg.restartUnits} 75 systemctl --no-block try-restart ${escapeShellArgs domainCfg.restartUnits}
@@ -79,16 +81,15 @@ in {
79 sops.secrets = mapAttrs' (domain: domainCfg: nameValuePair (tsigSecretName domain) { 81 sops.secrets = mapAttrs' (domain: domainCfg: nameValuePair (tsigSecretName domain) {
80 format = "binary"; 82 format = "binary";
81 sopsFile = tsigKey domain; 83 sopsFile = tsigKey domain;
82 restartUnits = [ "acme-${domain}.service" ]; 84 restartUnits = [ "acme-order-renew${domain}.service" ];
83 }) cfg.rfc2136Domains; 85 }) cfg.rfc2136Domains;
84 86
85 # Provide appropriate `tsig_key/*` to systemd service performing 87 # Provide appropriate `tsig_key/*` to systemd service performing
86 # certificate provisioning 88 # certificate provisioning
87 systemd.services = mapAttrs' (domain: domainCfg: nameValuePair "acme-${domain}" { 89 systemd.services = mapAttrs' (domain: domainCfg: nameValuePair "acme-order-renew-${domain}" {
88 after = [ "knot.service" ]; 90 after = [ "knot.service" ];
89 bindsTo = [ "knot.service" ]; 91 bindsTo = [ "knot.service" ];
90 serviceConfig = { 92 serviceConfig = {
91 LoadCredential = [ "${tsigSecretName domain}:${config.sops.secrets.${tsigSecretName domain}.path}" ];
92 SystemCallFilter = mkForce [ "@system-service" "~@privileged" "@chown" ]; 93 SystemCallFilter = mkForce [ "@system-service" "~@privileged" "@chown" ];
93 }; 94 };
94 }) cfg.rfc2136Domains; 95 }) cfg.rfc2136Domains;
diff --git a/hosts/surtr/tls/tsig_keys/changedetection.yggdrasil.li b/hosts/surtr/tls/tsig_keys/changedetection.yggdrasil.li
new file mode 100644
index 00000000..ac332fe5
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/changedetection.yggdrasil.li
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:OD12OI11EpjWIGtCGzSIeFXIht1tM7YrEbo3XqcxD0XFaZ3CrELJgru9gtN/,iv:SXPNed6CUWCUDomJbx1kOjvxTBoHrgb6tKw9Jb/Qa0M=,tag:RiueVMBSdAF96d6190bwfg==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwR3FGdks2TUtkSUJLcG1v\nbXZmWFZNVmc3a0x5d2tPNmpJVDJPQmIyNkFNClAwNzI0aHE5SFdaQ0RoZnE0ZEx5\nL3IzQTRpd0g4cHJSUHlrbGtRK283SEkKLS0tIDdLVzE0SFgvS0l1eDd5SHVQQ1By\nNmZ1M0cxSnNxTE5OdHBLZ2FFRWhXdUkKTykJ2kRJPrcPwuw3ufNaCJ6pOuvtDUcl\noHizOV+Yco7nhKtINE93mD4xIiER0i5h7lpKOTUGgjzhjJP2DR7ifw==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxNVRmMVIvVVZ4SkVMUC9o\nOVMxekNiZTRJWTExMHU5Q3lOaVFkbnhGT3lrCmM3ZjNSV056WjlTeEZzdHpKL1Fl\nbFF4R1phSitzWlY2dFJuRDFvK09LWm8KLS0tIEZINU5KallPdnZsZEh6WUxJYW1K\ncEV0ZkJVK2JiZ1ZtSTZRQUtiaGFBTEkKC6DQLWqY4WrRCSRrWAqlvjw6lp0Y+XGo\nrwxWMwyEocizMR6i//a5P8RBPnvzAEHbXMobI4mSDyfIdezWUX/QNA==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-12-08T12:46:13Z",
15 "mac": "ENC[AES256_GCM,data:FLdO6Bz74+aTd9ns8ysbcrNdwogJvnm/sRRTLntf5zAH16MyI+QbsBo2LORWr5O3t24+EfmZBhMsfj/AXvqkcMFjPwIhALQpPjjT2JfAsLFtSUqZRjBNKYkfoLlTUKb083RgDjEUIVGgsZzJLCyFtfZP0NXicTUsUz9mRZCYwYU=,iv:sSPuuoE3qgt+Qhh76rZtSCBnHYLK3AN7IljUDkr14AE=,tag:56rYSDonQwfKjNR5fBgQiA==,type:str]",
16 "version": "3.11.0"
17 }
18}
diff --git a/hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li b/hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li
new file mode 100644
index 00000000..b9199975
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:ATcU3Ix7o5d/49rD5H8je1ozTjoghrloMh5DIZ5WE3oYauUAknpGfr9xq92V,iv:vy9YK5Ot7CCjMtgAGVeAUQuaSw4F5kmmZ0GJYV9kCdQ=,tag:F/MXTUM2AI1fGXa9Ewn8yQ==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDMEF0cUdydERYVzJCa3pW\nTlo0NUFON0d5RGJFVnVTNVg3cjNEUERQMEdFClEvQW5odlNEd2F1VTFmMWQrL2RB\ncllFZVpIVVJrNTJsSGF4UEdZMnVmQzAKLS0tIFUrQkkzRVZiOFNiTnFCT1pEYVRM\nQm8wV1JkQ3RrR1dkL0FsNkhsY2kxa1kKGnAo/6oibgXexUU31THdLu6X+pRtrkjD\nZnXGPZ2xaESDVUVEYQPVpNrjt9brZGJBI1BasrkEwHAXMbJC236yYQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3MGs1Z2ZqK2pqWHdVYTJH\naTlncHdPa3Zld0JhQW5Ccmc1SStWSnlDR0JrCmpML2d4TGdldUdoZCtaWVpPZVl0\nVm4waWVBS1orRS90ZS96N0Y2M29LY0UKLS0tIEI1Z2VVbVVxRUpOZEN4NnBRRklC\nQXloelZCb04xbmduTlVuL005TlRGMHMKfLB6zA3sj3HgDBC7VGfGVB6I1zJpt0PV\nkCV2yADgvAA2pT9HPg9IWAEpTPysOBiuE2jPNtFvylZYwTDHoumFnQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-24T09:42:23Z",
15 "mac": "ENC[AES256_GCM,data:0pk1LpWPmX9td/TwJFxwWp5pTDyW78UtHXMDah+V9Tmgi8hH7ONdysgjwpDwS/c4zGnMA3qtobEL286U3//CTXt2qVsiUGLsnngzs2E6yBg8oGMYlGrch4M355Fl5ZxYsc8QLA6qWcuZ4H3QW8PnoqdJixcHoYLoxG01dzh4Bc0=,iv:zchk4enI1D80BkJLji5RLm7OTk3GeF8nYHuwqBxCXIM=,tag:bgkknPMqkSidi6bDFfv6UQ==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
diff --git a/hosts/surtr/tls/tsig_keys/online.yggdrasil.li b/hosts/surtr/tls/tsig_keys/online.yggdrasil.li
new file mode 100644
index 00000000..37cb5995
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/online.yggdrasil.li
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:pZn6ayJ1pCjLLodFAnpw3mgkcBYVdW7t9RYEysbdxfXUaf2jyIWX8h7dgpKO,iv:IWoU9AgUSRRPgD1R/2khVtLudo1GhPZV6C6q6wNZYDA=,tag:lJYqX6YV8z+uC1URFrw3eg==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6WjgyYmUybVJwWW01dnY3\nYjk1dTI0c3pYU2dlSThLc1ZsT0JDeFFzWEIwCncrd3VTN0x3Wi94emdCNlpVWFBW\nSkVYM3pGRE44RHNmOW1HVkRtc2RVN1UKLS0tIE5NWG1TV0pqcm9NekxyRW9LVGhW\nYXY3ZlYwRytoQkwwVENSaFBNejRYelEKpEbY1WDa0iUugYj1PZFt4HidietHmcI/\ncbl8l+h/hbJxuC2UtaMVSlfe3IzJmnwnqpuYAiEm3WaF1LDiPzMWvA==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGNm5KQVhzeGQzNVVnaWFi\nRnBXMEl3UENDTExKeUZqdjk0RUl6V2hZYlZVClk5V3hnbkFycDRqOTlZWjZnRnpy\nSmVGT1BhbjQ1T21hZ3RqZFl5RU9jNVkKLS0tIDhHN1lRZkNjcjdnRHJrNjRlczFk\nQ0o4RUlQSkp1UXRsZmdHQXUxZ1Rwb1UK88VMOotD0qScxCM10cgWHpz577eih5TV\n5SchQznh3icCidYWnAG4i4vvKFDwQBiAE69Yx/3rTYlv/fJYMxpX6Q==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2026-05-15T11:50:25Z",
15 "mac": "ENC[AES256_GCM,data:5f+Ge8WZoXp2QvPNi6ghrePo8KDaB93vIbqfllhwQI4kardiWSuaRANygAJC/d/zzok8EiOIiJ4qp8pcJJpW23K9qTT+6ZGJohdywDzk9IoHZ/FsJSHEiS3bxwnPE11n1Ia5xT14RFWPxswJy/YW8Go9sKSmiXuFXEcud4aZ8LU=,iv:2opq4uH+VGnlAcI4ffKrosKe+pVNDmTG+gC3ph6UFEM=,tag:NcYdxYhXCjnkUS5BvfpPXA==,type:str]",
16 "version": "3.12.2"
17 }
18}
diff --git a/hosts/surtr/tls/tsig_keys/pw.bouncy.email b/hosts/surtr/tls/tsig_keys/pw.bouncy.email
new file mode 100644
index 00000000..a2afac85
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/pw.bouncy.email
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:BPS7FIHRYPT2rRPa/XzpvPTMPqnncW5AMYCxeuAvGuuKi/dhASPboppvY/gS,iv:5g/WoOS3eOx/WK9d/qkiINfBsTduDPAUAJkToocCuHc=,tag:u0hSkteSRnjKEeXCOyIxzA==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxUWV2dVpMRExZRGJsM2ZP\nM1dqUnplai9CRmlMb1dBeXdwMTdNOWVQOEJRCnVYYS9SOU8vWUxSV0xTRmJDaEF4\nUGNzYVgzakhhSXMxMHgxQUxwMjU3bzgKLS0tIHVSVGdyRndseFRGblU5Z1J1L3ZK\nMlBnTVRVdHdheThWU2Urc0pZaE5WR3MKBUVK79os1sn2lVUVnj7iQgP9qZ44OXOP\n3s8PfxzaWyaARAK9yQX05a4pCMeiHdcrvaByPQxdzndVxopHNQqCBw==\n-----END AGE ENCRYPTED FILE-----\n",
7 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866"
8 },
9 {
10 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrYUYrcWRUQlVua283MTUx\nK3Y4K0VPd3p6Nzh6YXYzN21WTTV2bktzRFRFCjNjQmR1MDZIQ05vNTlkRkN5dDBX\nelM3bGtmWUNSVnVQb3Z6aWZyTjgrRDgKLS0tIENJcU5NZkIwZnFZck0vVmZ4bmp5\nTkpYRURrVGI5NCtJVWFFWjlXOStZR3cKJcqRnti3SnYLyC2/TKwkfGux8B7G1uta\nVmAMG8hf2tSJOaxM22/K/LGOXY8rinRYTCzCqvYX37HrBTRuGlcMOg==\n-----END AGE ENCRYPTED FILE-----\n",
11 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq"
12 }
13 ],
14 "lastmodified": "2026-06-06T13:54:42Z",
15 "mac": "ENC[AES256_GCM,data:yVLQhcoaKrchMQq4A35GithgHCdl7v0hxQluVNWRPWNdHDNtFJB+qH1Otjcew+i+gqVJGZ5F6YBapU93tFQGSeUqEjnhelPwTglAld28rQvBqGm1uBK1FrltY3wMxhLkiVKuLAVNkbyi9hOHfNyuhca5bhA1pQaMN0OoiSykgLg=,iv:sx0G7th9PA874xfOLZMKAI5TGwkWZ83PCcxS/On0omw=,tag:WhmHJp31WVao59+vGuA7PA==,type:str]",
16 "version": "3.13.1"
17 }
18}
diff --git a/hosts/surtr/tls/tsig_keys/vikunja.yggdrasil.li b/hosts/surtr/tls/tsig_keys/vikunja.yggdrasil.li
new file mode 100644
index 00000000..d5c19c2d
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/vikunja.yggdrasil.li
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:XfGp8x7wPhGf/Imo3m7/38mo5GD8zUODXEt1YZCVwWYDb6usiPMpGlNDTLzH,iv:mD0j48MKuU30h7llO575vKROOrojJzoA4Md9I6MR4Gg=,tag:h9EAJfViKUg3YQvU1I/ICQ==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZbHU3bDlwSS9VcnJNTzd6\ndFZyaC9CS2hQeWVSYVlDTzJLSlEvcEdJQlRFCmNoRzJpcUdycmx4T3dpREoxZTRL\neisxSGhlOXpUd0V2ZWgxdHc4empoRHcKLS0tIHhPNFFBUjNiaUMxWUx4NXNWeVNl\neHVib1Qwb2wyTWk3ZEtJQlNweHVhOUkK2kT2nmv9vwWGsMjW1RfLywGF/0yjLaNt\nb5M/v9jzYtV6S0PB1jegc4QxkGEKH4AxGsc/mqt6cXTG0em6hIIVDA==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSejM4cDZHTEtMdW5keUtk\naVV6Um4yOGd2anp0djduZ1pPMHg4bmNWTTBRCjZ2VE52Ym1YcDVNSEdPRUw5TFA3\nQnIwVjNwMzdUQWtjVUxndWozV2lnWUkKLS0tIGJSTTlEeE1sUysxUmVHVnNSaWRT\nRjJpOXFoUDgvMGxpaGNlV1ZVV0QvK3cKGDVfAudB5v201FOWFaTfCdT+io85Q14P\natzYgCAHbrpkicYL54gAdsgTPKfbnRU62DQlz7b2y69fU+kam4W0cw==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2026-01-01T14:25:24Z",
15 "mac": "ENC[AES256_GCM,data:A/1ZIUhKxSZBiivvbuDykcZ5DaUzq7FBD74mkP0ZECev0oBrziYtTgqA4MBgukV/hSQohDl8rzxReiQ7vxUotr6UocnF4x43guhABBayWh9Sp9n5T4yMIYYAemEQ67jYbTdRbgm4xQ+jSofSDRNFILBpdxDbCITRWBbIXh92iIo=,iv:Y4wZAd0+rc3c4eTnNz1QMJGQN1c0FQEx4/CB3t5sEeY=,tag:ha91ZLhWBDDHSubXXDos3A==,type:str]",
16 "version": "3.11.0"
17 }
18}
diff --git a/hosts/surtr/vikunja.nix b/hosts/surtr/vikunja.nix
new file mode 100644
index 00000000..50d8c00e
--- /dev/null
+++ b/hosts/surtr/vikunja.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "vikunja.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."vikunja" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:3456" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "vikunja.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/vikunja.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/vikunja.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/vikunja.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://vikunja;
35
36 proxy_http_version 1.1;
37 proxy_set_header Upgrade $http_upgrade;
38 proxy_set_header Connection "upgrade";
39
40 proxy_redirect off;
41 proxy_set_header Host $host;
42 proxy_set_header X-Real-IP $remote_addr;
43 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44 proxy_set_header X-Forwarded-Host $server_name;
45 proxy_set_header X-Forwarded-Proto $scheme;
46
47 client_max_body_size 0;
48 proxy_request_buffering off;
49 proxy_buffering off;
50 '';
51 };
52 };
53 };
54 };
55
56 systemd.services.nginx = {
57 serviceConfig = {
58 LoadCredential = [
59 "vikunja.yggdrasil.li.key.pem:${config.security.acme.certs."vikunja.yggdrasil.li".directory}/key.pem"
60 "vikunja.yggdrasil.li.pem:${config.security.acme.certs."vikunja.yggdrasil.li".directory}/fullchain.pem"
61 "vikunja.yggdrasil.li.chain.pem:${config.security.acme.certs."vikunja.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}