summaryrefslogtreecommitdiff
path: root/hosts/surtr
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/surtr')
-rw-r--r--hosts/surtr/audiobookshelf.nix66
-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/audiobookshelf.yggdrasil.li_acme19
-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.soa49
-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.nix788
-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/audiobookshelf.yggdrasil.li19
-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
-rw-r--r--hosts/surtr/vpn/default.nix8
-rw-r--r--hosts/surtr/vpn/geri.pub2
54 files changed, 2240 insertions, 573 deletions
diff --git a/hosts/surtr/audiobookshelf.nix b/hosts/surtr/audiobookshelf.nix
new file mode 100644
index 00000000..728851a4
--- /dev/null
+++ b/hosts/surtr/audiobookshelf.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "audiobookshelf.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."audiobookshelf" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:28982" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "audiobookshelf.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/audiobookshelf.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/audiobookshelf.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/audiobookshelf.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://audiobookshelf;
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 "audiobookshelf.yggdrasil.li.key.pem:${config.security.acme.certs."audiobookshelf.yggdrasil.li".directory}/key.pem"
60 "audiobookshelf.yggdrasil.li.pem:${config.security.acme.certs."audiobookshelf.yggdrasil.li".directory}/fullchain.pem"
61 "audiobookshelf.yggdrasil.li.chain.pem:${config.security.acme.certs."audiobookshelf.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}
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 0a79d047..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 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 e878d625..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"]; 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/audiobookshelf.yggdrasil.li_acme b/hosts/surtr/dns/keys/audiobookshelf.yggdrasil.li_acme
new file mode 100644
index 00000000..e3af9966
--- /dev/null
+++ b/hosts/surtr/dns/keys/audiobookshelf.yggdrasil.li_acme
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:5BLk/MZtQsLjpdKGiZjtOH1soIXB05MkEoPWyr5m27ho7H5udDjRZE0/OjvSlMzQNjFNJc+OwbeHwaJ8sPLCnXqoFHJXikAmi0gpdFC0uN0JGYCBT1EaK7j6yVHyiqFXo6xwVSCO/zP/fbjItiuqU+B4llGx5N2I4HQqRLFoW/35PZazkR15xI1zo8LeC8T+jm16apFQw2Ih/RqsJK7XlHqnXq9SzeA2qhgkCbJf6aJ6zDS7eQVFt7qHh2mVtV7Al++bZiIkJiNJ5SJO1ck18w2t9HwXBhfMFKXvfFW0I6kKur1qSJk=,iv:rxm5PwOzXDaK+nj2k3bUqlYbIFFA49ispyfamtQqU/A=,tag:7dOua39f+0JsLldLRLr1NQ==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwV1hIcWNNRVJXOG1xYlFK\nb1VxWG03dGVmS3dmQTltUTR0VmEzYjRFanlBCnE1M1BTZjNCQnpsNkU5ZlR5T3BR\nOHliWUV2bXMrK0w1K3JXbUE0dEhKdzgKLS0tIDdkUEZ4QUE0NUN2TXFYcklybjBS\nRVpxV2J6U1BnUng1dytWTGRkd0FrRGcKVaMCzcrX+BQqbmh95JEk3lRdfnv9uO8n\nwotKxp/+xX7GEF42BOzJ/mWcgyVjABlnWeVyaRxfpbCUrmNuFYO6XA==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkZmRFR0gwc1pjVVorK0t6\nVHU4U0xob09Dd2J5QUJXbndZdWp3cytpYWhRCnFnMlZ0cjNGdXN1RWEvQlVHSE1M\nMEdzSjlEb2NkaE1zMEJJOTlCMC9FVFEKLS0tIGJiTEJSRVZZbVUzZVFGeXQwYm1w\ndWVYQVdoVlJnRENTYzhnQm1BcWVRdW8K0ECfLVQBQuZWFFFjdHLcJBz+CDzsOOwh\nOA38qxHD4EWfXR1c3G8RDbow7nB2MGc1Zohc7qhtuTj2wL0qhVKWqA==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-09T17:07:16Z",
15 "mac": "ENC[AES256_GCM,data:K5I6YXQXCUPHFBNVlXIdLLKqiNPVZh95KoHni2m16SdAvTyBab79SZ5xNvotrtKXp0iISCogEgdMm+OWbxYywEiZ+sUsxgx6RE5nAXruZOiAwzuyUr88qgCHTBZnKzgaDZlbYOgWB+LCzr8s5JbcTD4G6/RaXYyqmx7igygubHA=,iv:Zbij4eoDqoP5XYhAsDGBGqlcP5ACQAY/QngTmrJYRzs=,tag:WYxhUEZwmXkQmnpyOuP2Bg==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
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 2ef120e6..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 2025021901 ; 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.
@@ -93,12 +93,52 @@ _acme-challenge.hledger IN NS ns.yggdrasil.li.
93 93
94hledger IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::" 94hledger IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
95 95
96audiobookshelf IN A 202.61.241.61
97audiobookshelf IN AAAA 2a03:4000:52:ada::
98audiobookshelf IN MX 0 surtr.yggdrasil.li
99audiobookshelf IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
100_acme-challenge.audiobookshelf IN NS ns.yggdrasil.li.
101
102audiobookshelf IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
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
96vidhar IN AAAA 2a03:4000:52:ada:4:1:: 134vidhar IN AAAA 2a03:4000:52:ada:4:1::
97vidhar IN MX 0 ymir.yggdrasil.li 135vidhar IN MX 0 ymir.yggdrasil.li
98vidhar IN TXT "v=spf1 redirect=yggdrasil.li" 136vidhar IN TXT "v=spf1 redirect=yggdrasil.li"
99 137
100mailout IN A 188.68.51.254 138mailout IN A 188.68.51.254
101mailout 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::
102mailout IN MX 0 ymir.yggdrasil.li 142mailout IN MX 0 ymir.yggdrasil.li
103mailout IN TXT "v=spf1 redirect=yggdrasil.li" 143mailout IN TXT "v=spf1 redirect=yggdrasil.li"
104 144
@@ -107,10 +147,7 @@ ymir._domainkey IN TXT (
107 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24" 147 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
108 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ==" 148 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
109) 149)
110 150$INCLUDE /var/lib/dkim/yggdrasil.li.txt
111surtr._domainkey IN TXT ( "v=DKIM1;k=rsa;"
112 "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwEspNBXjTjPpCqSMkcBUJnSThqMcHwvDP3mOnv8wpDrGTN+1eJ1fzso5GHooGNt0kWHOpcoVwsMDIk81SR3zzNKYWqM40KvQ2ElNJqS5VDIfnxppiG9H5Nu3M7In5jv7OTSKsEi5eDzWqqvaHn6YjNQuKHQsJsAB1zUKoR1gqpvwJlV3tnhfQEl1O3qt0tG1c6JvgZ8R8szrk9"
113 "uNZzu90PDQY9UH4K1nu+INwlMgz9hzgJHIoNJOdB+1gmvnsI4MgmT/otxwKia/UoddN3Gcu7DO1gjFi5cwOA+zOgMnzzWUbys0Q3loCKp9EYgWUJQ9CCh5U4x4/GpV2VeEJ/0GYQIDAQAB" )
114 151
115_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 152_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
116_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 13b33c7f..d9e6fff9 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,22 +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 recpipient_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 ];
159
127 #enable ECDH 160 #enable ECDH
128 smtpd_tls_eecdh_grade = "strong"; 161 smtpd_tls_eecdh_grade = "strong";
129 #enabled SSL protocols, don't allow SSLv2 and SSLv3 162 #enabled SSL protocols, don't allow SSLv2 and SSLv3
@@ -155,21 +188,14 @@ in {
155 188
156 smtp_tls_connection_reuse = true; 189 smtp_tls_connection_reuse = true;
157 190
158 tls_server_sni_maps = ''texthash:${pkgs.writeText "sni" ( 191 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 192
165 smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix"; 193 smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix";
166 194
167 local_recipient_maps = ""; 195 local_recipient_maps = "";
168 196
169 # 10 GiB 197 message_size_limit = 10 * 1024 * 1024 * 1024;
170 message_size_limit = "10737418240"; 198 mailbox_size_limit = 10 * 1024 * 1024 * 1024;
171 # 10 GiB
172 mailbox_size_limit = "10737418240";
173 199
174 smtpd_delay_reject = true; 200 smtpd_delay_reject = true;
175 smtpd_helo_required = true; 201 smtpd_helo_required = true;
@@ -184,12 +210,12 @@ in {
184 dbname = email 210 dbname = email
185 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' 211 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s'
186 ''}" 212 ''}"
187 "check_ccert_access ${relay_ccert}"
188 "reject_non_fqdn_helo_hostname" 213 "reject_non_fqdn_helo_hostname"
189 "reject_invalid_helo_hostname" 214 "reject_invalid_helo_hostname"
190 "reject_unauth_destination" 215 "reject_unauth_destination"
191 "reject_unknown_recipient_domain" 216 "reject_unknown_recipient_domain"
192 "reject_unverified_recipient" 217 "reject_unverified_recipient"
218 "check_policy_service unix:/run/postfix-internal-policy.sock"
193 ]; 219 ];
194 unverified_recipient_reject_code = "550"; 220 unverified_recipient_reject_code = "550";
195 unverified_recipient_reject_reason = "Recipient address lookup failed"; 221 unverified_recipient_reject_reason = "Recipient address lookup failed";
@@ -204,7 +230,6 @@ in {
204 address_verify_sender_ttl = "30045s"; 230 address_verify_sender_ttl = "30045s";
205 231
206 smtpd_relay_restrictions = [ 232 smtpd_relay_restrictions = [
207 "check_ccert_access ${relay_ccert}"
208 "reject_unauth_destination" 233 "reject_unauth_destination"
209 ]; 234 ];
210 235
@@ -215,8 +240,8 @@ in {
215 smtpd_client_event_limit_exceptions = ""; 240 smtpd_client_event_limit_exceptions = "";
216 241
217 milter_default_action = "accept"; 242 milter_default_action = "accept";
218 smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; 243 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"]; 244 non_smtpd_milters = ["local:/run/rspamd/rspamd-milter.sock"];
220 245
221 alias_maps = ""; 246 alias_maps = "";
222 247
@@ -227,6 +252,37 @@ in {
227 bounce_queue_lifetime = "20m"; 252 bounce_queue_lifetime = "20m";
228 delay_warning_time = "10m"; 253 delay_warning_time = "10m";
229 254
255 failure_template_file = toString (pkgs.writeText "failure.cf" ''
256 Charset: us-ascii
257 From: Mail Delivery System <MAILER-DAEMON>
258 Subject: Undelivered Mail Returned to Sender
259 Postmaster-Subject: Postmaster Copy: Undelivered Mail
260
261 This is the mail system at host $myhostname.
262
263 I'm sorry to have to inform you that your message could not
264 be delivered to one or more recipients. It's attached below.
265
266 The mail system
267 '');
268 delay_template_file = toString (pkgs.writeText "delay.cf" ''
269 Charset: us-ascii
270 From: Mail Delivery System <MAILER-DAEMON>
271 Subject: Delayed Mail (still being retried)
272 Postmaster-Subject: Postmaster Warning: Delayed Mail
273
274 This is the mail system at host $myhostname.
275
276 ####################################################################
277 # THIS IS A WARNING ONLY. YOU DO NOT NEED TO RESEND YOUR MESSAGE. #
278 ####################################################################
279
280 Your message could not be delivered for more than $delay_warning_time_minutes minute(s).
281 It will be retried until it is $maximal_queue_lifetime_minutes minute(s) old.
282
283 The mail system
284 '');
285
230 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" '' 286 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" ''
231 # Allow DSN requests from local subnet only 287 # Allow DSN requests from local subnet only
232 192.168.0.0/16 silent-discard 288 192.168.0.0/16 silent-discard
@@ -237,11 +293,6 @@ in {
237 ::/0 silent-discard, dsn 293 ::/0 silent-discard, dsn
238 ''}"; 294 ''}";
239 295
240 sender_canonical_maps = "tcp:localhost:${toString config.services.postsrsd.forwardPort}";
241 sender_canonical_classes = "envelope_sender";
242 recipient_canonical_maps = "tcp:localhost:${toString config.services.postsrsd.reversePort}";
243 recipient_canonical_classes = ["envelope_recipient" "header_recipient"];
244
245 virtual_mailbox_domains = ''pgsql:${pkgs.writeText "virtual_mailbox_domains.cf" '' 296 virtual_mailbox_domains = ''pgsql:${pkgs.writeText "virtual_mailbox_domains.cf" ''
246 hosts = postgresql:///email 297 hosts = postgresql:///email
247 dbname = email 298 dbname = email
@@ -256,13 +307,26 @@ in {
256 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp"; 307 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp";
257 smtputf8_enable = false; 308 smtputf8_enable = false;
258 309
259 authorized_submit_users = "inline:{ root= postfwd= }"; 310 authorized_submit_users = "inline:{ root= postfwd= ${config.services.dovecot2.settings.mail_uid}= }";
311 authorized_flush_users = "inline:{ root= }";
312 authorized_mailq_users = "inline:{ root= }";
260 313
261 postscreen_access_list = ""; 314 postscreen_access_list = "";
262 postscreen_denylist_action = "drop"; 315 postscreen_denylist_action = "drop";
263 postscreen_greet_action = "enforce"; 316 postscreen_greet_action = "enforce";
317
318 sender_bcc_maps = ''pgsql:${pkgs.writeText "sender_bcc_maps.cf" ''
319 hosts = postgresql:///email
320 dbname = email
321 query = SELECT value FROM sender_bcc_maps WHERE key = '%s'
322 ''}'';
323 recipient_bcc_maps = ''pgsql:${pkgs.writeText "recipient_bcc_maps.cf" ''
324 hosts = postgresql:///email
325 dbname = email
326 query = SELECT value FROM recipient_bcc_maps WHERE key = '%s'
327 ''}'';
264 }; 328 };
265 masterConfig = { 329 settings.master = {
266 "465" = { 330 "465" = {
267 type = "inet"; 331 type = "inet";
268 private = false; 332 private = false;
@@ -285,13 +349,12 @@ in {
285 hosts = postgresql:///email 349 hosts = postgresql:///email
286 dbname = email 350 dbname = email
287 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')) 351 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'))
288 ''},permit_tls_all_clientcerts,reject}'' 352 ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_tls_all_clientcerts,reject}''
289 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" 353 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject"
290 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" 354 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
291 "-o" "unverified_sender_reject_code=550" 355 "-o" "unverified_sender_reject_code=550"
292 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}" 356 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}"
293 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li" 357 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li"
294 "-o" ''smtpd_milters=${config.services.opendkim.socket}''
295 ]; 358 ];
296 }; 359 };
297 "466" = { 360 "466" = {
@@ -315,13 +378,12 @@ in {
315 hosts = postgresql:///email 378 hosts = postgresql:///email
316 dbname = email 379 dbname = email
317 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')) 380 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'))
318 ''},permit_sasl_authenticated,reject}'' 381 ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_sasl_authenticated,reject}''
319 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject" 382 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject"
320 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" 383 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
321 "-o" "unverified_sender_reject_code=550" 384 "-o" "unverified_sender_reject_code=550"
322 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}" 385 "-o" "unverified_sender_reject_reason={Sender address rejected: undeliverable address}"
323 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li" 386 "-o" "milter_macro_daemon_name=surtr.yggdrasil.li"
324 "-o" ''smtpd_milters=${config.services.opendkim.socket}''
325 ]; 387 ];
326 }; 388 };
327 subcleanup = { 389 subcleanup = {
@@ -330,7 +392,10 @@ in {
330 maxproc = 0; 392 maxproc = 0;
331 args = [ 393 args = [
332 "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" '' 394 "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" ''
395 if /^Received: /
396 !/by surtr\.yggdrasil\.li/ STRIP
333 /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1 397 /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1
398 endif
334 ''}" 399 ''}"
335 ]; 400 ];
336 }; 401 };
@@ -366,23 +431,11 @@ in {
366 431
367 services.postsrsd = { 432 services.postsrsd = {
368 enable = true; 433 enable = true;
369 domain = "surtr.yggdrasil.li"; 434 domains = [ "surtr.yggdrasil.li" ] ++ concatMap (domain: [".${domain}" domain]) emailDomains;
370 separator = "+"; 435 separator = "+";
371 excludeDomains = [ "surtr.yggdrasil.li" 436 extraConfig = ''
372 ] ++ concatMap (domain: [".${domain}" domain]) emailDomains; 437 socketmap = unix:/run/postsrsd/postsrsd-socketmap.sock
373 }; 438 milter = unix:/run/postsrsd/postsrsd-milter.sock
374
375 services.opendkim = {
376 enable = true;
377 user = "postfix"; group = "postfix";
378 socket = "local:/run/opendkim/opendkim.sock";
379 domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li"] ++ emailDomains)}'';
380 selector = "surtr";
381 configFile = builtins.toFile "opendkim.conf" ''
382 Syslog true
383 MTA surtr.yggdrasil.li
384 MTACommand ${config.security.wrapperDir}/sendmail
385 LogResults true
386 ''; 439 '';
387 }; 440 };
388 441
@@ -417,6 +470,8 @@ in {
417 milter = yes; 470 milter = yes;
418 timeout = 120s; 471 timeout = 120s;
419 472
473 client_ca_name = "yggdrasil.li";
474
420 upstream "local" { 475 upstream "local" {
421 default = yes; 476 default = yes;
422 self_scan = yes; 477 self_scan = yes;
@@ -453,7 +508,13 @@ in {
453 "redis.conf".text = '' 508 "redis.conf".text = ''
454 servers = "${config.services.redis.servers.rspamd.unixSocket}"; 509 servers = "${config.services.redis.servers.rspamd.unixSocket}";
455 ''; 510 '';
456 "dkim_signing.conf".text = "enabled = false;"; 511 "dkim_signing.conf".text = ''
512 enabled = true;
513 allow_username_mismatch = true;
514
515 path = "/var/lib/rspamd/dkim/$domain.key";
516 selector = "mail";
517 '';
457 "neural.conf".text = "enabled = false;"; 518 "neural.conf".text = "enabled = false;";
458 "classifier-bayes.conf".text = '' 519 "classifier-bayes.conf".text = ''
459 enable = true; 520 enable = true;
@@ -474,242 +535,225 @@ in {
474 spam = true; 535 spam = true;
475 } 536 }
476 ''; 537 '';
538 "logging.inc".text = ''
539 debug_modules = ["milter", "dkim_signing"];
540 '';
477 # "redirectors.inc".text = '' 541 # "redirectors.inc".text = ''
478 # visit.creeper.host 542 # visit.creeper.host
479 # ''; 543 # '';
480 }; 544 };
481 }; 545 };
482 546
483 users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user "dovecot2" ]; 547 users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user config.services.dovecot2.settings.mail_uid ];
484 548
485 services.redis.servers.rspamd.enable = true; 549 services.redis.servers.rspamd.enable = true;
486 550
487 users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ]; 551 users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ];
488 552
489 environment.systemPackages = with pkgs; [ dovecot_pigeonhole dovecot_fts_xapian ]; 553 environment.systemPackages = with pkgs; [ dovecot_pigeonhole ];
490 services.dovecot2 = { 554 services.dovecot2 = {
555 package = pkgs.dovecot;
491 enable = true; 556 enable = true;
492 enablePAM = false; 557 enablePAM = false;
493 sslServerCert = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.pem"; 558 settings = {
494 sslServerKey = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.key.pem"; 559 dovecot_config_version = "2.4.2";
495 sslCACert = toString ./ca/ca.crt; 560 dovecot_storage_version = "2.4.0";
496 mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u";
497 mailPlugins.globally.enable = [ "fts" "fts_xapian" ];
498 protocols = [ "lmtp" "sieve" ];
499 sieve = {
500 extensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"];
501 globalExtensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"];
502 };
503 extraConfig = let
504 dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" ''
505 driver = pgsql
506 connect = dbname=email
507 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'
508 user_query = SELECT "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n'
509 iterate_query = SELECT "user" FROM imap_user
510 '';
511 in ''
512 mail_home = /var/lib/mail/%u
513
514 mail_plugins = $mail_plugins quota
515
516 first_valid_uid = ${toString config.users.users.dovecot2.uid}
517 last_valid_uid = ${toString config.users.users.dovecot2.uid}
518 first_valid_gid = ${toString config.users.groups.dovecot2.gid}
519 last_valid_gid = ${toString config.users.groups.dovecot2.gid}
520
521 ${concatMapStringsSep "\n\n" (domain:
522 concatMapStringsSep "\n" (subdomain: ''
523 local_name ${subdomain} {
524 ssl_cert = </run/credentials/dovecot2.service/${subdomain}.pem
525 ssl_key = </run/credentials/dovecot2.service/${subdomain}.key.pem
526 }
527 '') ["imap.${domain}" domain]
528 ) emailDomains}
529
530 ssl_require_crl = no
531 ssl_verify_client_cert = yes
532
533 ssl_min_protocol = TLSv1.2
534 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
535 ssl_prefer_server_ciphers = no
536
537 auth_ssl_username_from_cert = yes
538 ssl_cert_username_field = commonName
539 auth_mechanisms = plain login external
540
541 auth_verbose = yes
542 verbose_ssl = yes
543 auth_debug = yes
544
545 service auth {
546 user = dovecot2
547 }
548 service auth-worker {
549 user = dovecot2
550 }
551
552 userdb {
553 driver = prefetch
554 }
555 userdb {
556 driver = sql
557 args = ${dovecotSqlConf}
558 }
559 passdb {
560 driver = sql
561 args = ${dovecotSqlConf}
562 }
563 561
564 protocol lmtp { 562 sql_driver = "pgsql";
565 userdb { 563 "pgsql /run/postgresql".parameters = {
566 driver = sql 564 dbname = "email";
567 args = ${pkgs.writeText "dovecot-sql.conf" '' 565 };
568 driver = pgsql
569 connect = dbname=email
570 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
571 ''}
572
573 skip = never
574 result_failure = return-fail
575 result_internalfail = return-fail
576 }
577
578 mail_plugins = $mail_plugins sieve
579 }
580 566
581 mailbox_list_index = yes 567 protocols = {
582 postmaster_address = postmaster@yggdrasil.li 568 imap = true;
583 recipient_delimiter = 569 lmtp = true;
584 auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-+_@ 570 sieve = true;
571 };
585 572
586 service lmtp { 573 mail_plugins = {
587 vsz_limit = 1G 574 quota = true;
575 fts = true;
576 fts_flatcurve = true;
577 };
588 578
589 unix_listener /run/dovecot-lmtp { 579 mail_uid = "dovecot2";
590 mode = 0600 580 mail_gid = "dovecot2";
591 user = postfix 581
592 group = postfix 582 first_valid_uid = config.ids.uids.dovecot2;
593 } 583 last_valid_uid = config.ids.uids.dovecot2;
594 } 584 first_valid_gid = config.ids.gids.dovecot2;
595 service auth { 585 last_valid_gid = config.ids.gids.dovecot2;
596 vsz_limit = 2G 586
587 mail_driver = "maildir";
588 mail_path = "/var/lib/mail/%{user}/maildir";
589 mail_index_path = "/var/lib/dovecot/indices/%{user}";
590 ssl_server_ca_file = ./ca/ca.crt;
591 ssl_server_key_file = "/run/credentials/dovecot.service/surtr.yggdrasil.li.key.pem";
592 ssl_server_cert_file = "/run/credentials/dovecot.service/surtr.yggdrasil.li.pem";
593
594 mail_home = "/var/lib/mail/%{user}";
595
596 ssl_server_require_crl = false;
597 ssl_server_request_client_cert = true;
598
599 ssl_min_protocol = "TLSv1.2";
600 ssl_curve_list = "X25519MLKEM768:X25519";
601 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";
602
603 auth_ssl_username_from_cert = "yes";
604 ssl_server_cert_username_field = "commonName";
605 auth_mechanisms = ["plain" "login" "external"];
606
607 log_debug = "category=ssl OR category=auth";
608 auth_verbose = true;
609
610 "service auth-worker".user = "$SET:default_internal_user";
611 "userdb prefetch" = {};
612 "userdb sql" = {
613 sql_query = "SELECT \"user\", quota_rule, '${config.services.dovecot2.settings.mail_uid}' as uid, 'dovecot2' as gid FROM imap_user WHERE \"user\" = '%{user | username}'";
614 sql_iterate_query = "SELECT \"user\" FROM imap_user";
615 fields = {
616 uid = "$SET:default_internal_user";
617 gid = "$SET:default_internal_user";
618 };
619 };
620 "passdb sql" = {
621 sql_query = ''
622 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}'
623 '';
624 };
597 625
598 unix_listener /run/dovecot-sasl { 626 "protocol lmtp" = {
599 mode = 0600 627 mail_plugins.sieve = true;
600 user = postfix 628 "userdb sql-lmtp" = {
601 group = postfix 629 driver = "sql";
602 } 630 sql_query = ''
603 } 631 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
632 '';
604 633
605 namespace inbox { 634 skip = "never";
606 separator = / 635 result_failure = "return-fail";
607 inbox = yes 636 result_internalfail = "return-fail";
608 prefix = 637 };
638 };
609 639
610 mailbox Trash { 640 mailbox_list_index = true;
611 auto = no 641 mailbox_list_utf8 = true;
612 special_use = \Trash 642 postmaster_address = "postmaster@yggdrasil.li";
613 } 643 recipient_delimiter = null;
614 mailbox Junk { 644 auth_username_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-+_@";
615 auto = no
616 special_use = \Junk
617 }
618 mailbox Drafts {
619 auto = no
620 special_use = \Drafts
621 }
622 mailbox Sent {
623 auto = subscribe
624 special_use = \Sent
625 }
626 mailbox "Sent Messages" {
627 auto = no
628 special_use = \Sent
629 }
630 }
631 645
632 plugin { 646 "service lmtp" = {
633 quota = count 647 vsz_limit = "1G";
634 quota_rule = *:storage=1GB
635 quota_rule2 = Trash:storage=+10%%
636 quota_status_overquota = "552 5.2.2 Mailbox is full"
637 quota_status_success = DUNNO
638 quota_status_nouser = DUNNO
639 quota_grace = 10%%
640 quota_max_mail_size = ${config.services.postfix.config.message_size_limit}
641 quota_vsizes = yes
642 }
643 648
644 protocol imap { 649 "unix_listener /run/dovecot-lmtp" = {
645 mail_max_userip_connections = 50 650 mode = "0600";
646 mail_plugins = $mail_plugins imap_quota imap_sieve 651 user = "postfix";
647 } 652 group = "postfix";
653 };
654 };
655 "service auth" = {
656 vsz_limit = "2G";
648 657
649 service imap-login { 658 "unix_listener /run/dovecot-sasl" = {
650 inet_listener imap { 659 mode = "0600";
651 port = 0 660 user = "postfix";
652 } 661 group = "postfix";
653 } 662 };
663 };
654 664
655 service managesieve-login { 665 quota_storage_size = "1G";
656 inet_listener sieve { 666 "namespace inbox" = {
657 port = 4190 667 separator = "/";
658 } 668 inbox = true;
659 }
660 669
661 plugin { 670 "mailbox Trash" = {
662 sieve_plugins = sieve_imapsieve sieve_extprograms 671 auto = false;
663 sieve = file:~/sieve;active=~/dovecot.sieve 672 special_use = "\\Trash";
664 sieve_redirect_envelope_from = orig_recipient 673 quota_storage_percentage = "110";
665 sieve_before = /etc/dovecot/sieve_before.d 674 };
675 "mailbox Junk" = {
676 auto = false;
677 special_use = "\\Junk";
678 };
679 "mailbox Drafts" = {
680 auto = false;
681 special_use = "\\Drafts";
682 };
683 "mailbox Sent" = {
684 auto = "subscribe";
685 special_use = "\\Sent";
686 };
687 "mailbox \"Sent Messages\"" = {
688 auto = false;
689 special_use = "\\Sent";
690 };
691 };
666 692
667 sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment 693 quota_status_overquota = "552 5.2.2 Mailbox is full";
668 sieve_pipe_bin_dir = ${dovecotSievePipeBin}/pipe/bin 694 quota_status_success = "DUNNO";
695 quota_status_nouser = "DUNNO";
696 quota_storage_grace = "100M";
697 quota_mail_size = 10 * 1024 * 1024 * 1024;
669 698
670 imapsieve_mailbox1_name = * 699 sieve_plugins = {
671 imapsieve_mailbox1_causes = FLAG 700 "sieve_imapsieve" = true;
672 imapsieve_mailbox1_before = /etc/dovecot/sieve_flag.d/learn-junk.sieve 701 "sieve_extprograms" = true;
673 } 702 };
703 sieve_redirect_envelope_from = "orig_recipient";
704 sieve_extensions = {
705 imapsieve = true;
706 vacation-seconds = true;
707 "vnd.dovecot.debug" = true;
708 };
709 sieve_global_extensions = {
710 "vnd.dovecot.pipe" = true;
711 "vnd.dovecot.environment" = true;
712 };
713 sieve_pipe_bin_dir = "${dovecotSievePipeBin}/pipe/bin";
674 714
675 plugin { 715 "sieve_script before" = {
676 plugin = fts fts_xapian 716 type = "before";
677 fts = xapian 717 path = "/etc/dovecot/sieve_before.d";
678 fts_xapian = partial=2 full=20 attachments=1 verbose=1 718 };
719 "sieve_script flag" = {
720 type = "before";
721 cause.flag = true;
722 path = "/etc/dovecot/sieve_flag.d";
723 };
724 "sieve_script personal" = {
725 driver = "file";
726 path = "~/sieve";
727 active_path = "~/dovecot.sieve";
728 };
679 729
680 fts_autoindex = yes 730 "fts flatcurve" = {
731 autoindex = true;
732 };
733 language_tokenizers = ["generic" "email-address"];
734 language_filters = ["normalizer-icu" "snowball" "stopwords"];
735 "language en" = {
736 default = true;
737 filters = ["lowercase" "snowball" "stopwords"];
738 };
739 "language de" = {};
681 740
682 fts_enforced = no 741 "protocol imap" = {
683 } 742 mail_max_userip_connections = 50;
743 mail_plugins = {
744 imap_quota = true;
745 imap_sieve = true;
746 };
747 };
684 748
685 service indexer-worker { 749 "service imap-login"."inet_listener imap".port = 0;
686 vsz_limit = ${toString (1024 * 1024 * 1024)} 750 "service managesieve-login"."inet_listener sieve".port = 4190;
687 }
688 '';
689 };
690 751
691 systemd.services.dovecot-fts-xapian-optimize = { 752 "service indexer-worker".vsz_limit = 1024 * 1024 * 1024;
692 description = "Optimize dovecot indices for fts_xapian"; 753 } // (genAttrs' (concatMap (domain: ["imap.${domain}" domain]) emailDomains) (subdomain: nameValuePair "local_name ${subdomain}" {
693 requisite = [ "dovecot2.service" ]; 754 ssl_server_key_file = "/run/credentials/dovecot.service/${subdomain}.key.pem";
694 after = [ "dovecot2.service" ]; 755 ssl_server_cert_file = "/run/credentials/dovecot.service/${subdomain}.pem";
695 startAt = "*-*-* 22:00:00 Europe/Berlin"; 756 }));
696 serviceConfig = {
697 Type = "oneshot";
698 ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
699 PrivateDevices = true;
700 PrivateNetwork = true;
701 ProtectKernelTunables = true;
702 ProtectKernelModules = true;
703 ProtectControlGroups = true;
704 ProtectHome = true;
705 ProtectSystem = true;
706 PrivateTmp = true;
707 };
708 };
709 systemd.timers.dovecot-fts-xapian-optimize = {
710 timerConfig = {
711 RandomizedDelaySec = 4 * 3600;
712 };
713 }; 757 };
714 758
715 environment.etc = { 759 environment.etc = {
@@ -744,46 +788,31 @@ in {
744 ''; 788 '';
745 }; 789 };
746 790
747 security.dhparams = {
748 params = {
749 "postfix-512".bits = 512;
750 "postfix-1024".bits = 2048;
751
752 "postfix-smtps-512".bits = 512;
753 "postfix-smtps-1024".bits = 2048;
754 };
755 };
756
757 security.acme.rfc2136Domains = { 791 security.acme.rfc2136Domains = {
758 "surtr.yggdrasil.li" = { 792 "surtr.yggdrasil.li" = {
759 restartUnits = [ "postfix.service" "dovecot2.service" ]; 793 restartUnits = [ "postfix.service" "dovecot.service" ];
794 };
795 "pw.bouncy.email" = {
796 restartUnits = [ "nginx.service" ];
760 }; 797 };
761 } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains) 798 } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains)
762 // listToAttrs (concatMap (domain: [ 799 // listToAttrs (concatMap (domain: [
763 (nameValuePair domain { restartUnits = ["postfix.service" "dovecot2.service"]; }) 800 (nameValuePair domain { restartUnits = ["postfix.service" "dovecot.service"]; })
764 (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; }) 801 (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; })
765 (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; }) 802 (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; })
766 (nameValuePair "imap.${domain}" { restartUnits = ["dovecot2.service"]; }) 803 (nameValuePair "imap.${domain}" { restartUnits = ["dovecot.service"]; })
767 (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; }) 804 (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; })
768 ]) emailDomains); 805 ]) emailDomains);
769 806
770 systemd.services.postfix = { 807 systemd.services.postfix = {
771 serviceConfig.LoadCredential = [ 808 serviceConfig.LoadCredential = let
772 "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" 809 tlsCredential = domain: "${domain}.full.pem:${config.security.acme.certs.${domain}.directory}/full.pem";
773 "surtr.yggdrasil.li.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/fullchain.pem" 810 in [
774 ] ++ concatMap (domain: 811 (tlsCredential "surtr.yggdrasil.li")
775 map (subdomain: "${subdomain}.full.pem:${config.security.acme.certs.${subdomain}.directory}/full.pem") 812 ] ++ concatMap (domain: map tlsCredential [domain "mailin.${domain}" "mailsub.${domain}"]) emailDomains;
776 [domain "mailin.${domain}" "mailsub.${domain}"]
777 ) emailDomains;
778 }; 813 };
779 814
780 systemd.services.dovecot2 = { 815 systemd.services.dovecot = {
781 preStart = ''
782 for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do
783 ${pkgs.dovecot_pigeonhole}/bin/sievec $f
784 done
785 '';
786
787 serviceConfig = { 816 serviceConfig = {
788 LoadCredential = [ 817 LoadCredential = [
789 "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" 818 "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem"
@@ -804,6 +833,11 @@ in {
804 "unix:/run/spm/server.sock" = {}; 833 "unix:/run/spm/server.sock" = {};
805 }; 834 };
806 }; 835 };
836 upstreams.password-server = {
837 servers = {
838 "unix:/run/email-password-server.sock" = {};
839 };
840 };
807 841
808 virtualHosts = listToAttrs (map (domain: nameValuePair "spm.${domain}" { 842 virtualHosts = listToAttrs (map (domain: nameValuePair "spm.${domain}" {
809 forceSSL = true; 843 forceSSL = true;
@@ -847,17 +881,39 @@ in {
847 charset utf-8; 881 charset utf-8;
848 source_charset utf-8; 882 source_charset utf-8;
849 ''; 883 '';
850 root = pkgs.runCommand "mta-sts.${domain}" {} '' 884 root = pkgs.writeTextFile {
851 mkdir -p $out/.well-known 885 name = "mta-sts.${domain}";
852 cp ${pkgs.writeText "mta-sts.${domain}.txt" '' 886 destination = "/.well-known/mta-sts.txt";
887 text = ''
853 version: STSv1 888 version: STSv1
854 mode: enforce 889 mode: enforce
855 max_age: 2419200 890 max_age: 2419200
856 mx: mailin.${domain} 891 mx: mailin.${domain}
857 ''} $out/.well-known/mta-sts.txt 892 '';
858 ''; 893 };
859 }; 894 };
860 }) emailDomains); 895 }) emailDomains) // {
896 "pw.bouncy.email" = {
897 forceSSL = true;
898 kTLS = true;
899 http3 = false;
900 sslCertificate = "/run/credentials/nginx.service/pw.bouncy.email.pem";
901 sslCertificateKey = "/run/credentials/nginx.service/pw.bouncy.email.key.pem";
902 extraConfig = ''
903 ssl_stapling off;
904 ssl_verify_client optional;
905 ssl_client_certificate ${toString ./ca/ca.crt};
906 '';
907 locations."/" = {
908 proxyPass = "http://password-server";
909
910 extraConfig = ''
911 proxy_set_header SSL-CLIENT-VERIFY $ssl_client_verify;
912 proxy_set_header SSL-CLIENT-S-DN $ssl_client_s_dn;
913 '';}
914 ;
915 };
916 };
861 }; 917 };
862 918
863 systemd.services.nginx.serviceConfig.LoadCredential = concatMap (domain: [ 919 systemd.services.nginx.serviceConfig.LoadCredential = concatMap (domain: [
@@ -867,12 +923,15 @@ in {
867 "mta-sts.${domain}.key.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/key.pem" 923 "mta-sts.${domain}.key.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/key.pem"
868 "mta-sts.${domain}.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/fullchain.pem" 924 "mta-sts.${domain}.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/fullchain.pem"
869 "mta-sts.${domain}.chain.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/chain.pem" 925 "mta-sts.${domain}.chain.pem:${config.security.acme.certs."mta-sts.${domain}".directory}/chain.pem"
870 ]) emailDomains; 926 ]) emailDomains ++ [
927 "pw.bouncy.email.key.pem:${config.security.acme.certs."pw.bouncy.email".directory}/key.pem"
928 "pw.bouncy.email.pem:${config.security.acme.certs."pw.bouncy.email".directory}/fullchain.pem"
929 ];
871 930
872 systemd.services.spm = { 931 systemd.services.spm = {
873 serviceConfig = { 932 serviceConfig = {
874 Type = "notify"; 933 Type = "notify";
875 ExecStart = "${pkgs.spm}/bin/spm-server"; 934 ExecStart = getExe' pkgs.spm "spm-server";
876 User = "spm"; 935 User = "spm";
877 Group = "spm"; 936 Group = "spm";
878 937
@@ -930,7 +989,7 @@ in {
930 serviceConfig = { 989 serviceConfig = {
931 Type = "notify"; 990 Type = "notify";
932 991
933 ExecStart = "${ccert-policy-server}/bin/ccert-policy-server"; 992 ExecStart = getExe' ccert-policy-server "ccert-policy-server";
934 993
935 Environment = [ 994 Environment = [
936 "PGDATABASE=email" 995 "PGDATABASE=email"
@@ -963,6 +1022,109 @@ in {
963 }; 1022 };
964 users.groups."postfix-ccert-sender-policy" = {}; 1023 users.groups."postfix-ccert-sender-policy" = {};
965 1024
1025 systemd.sockets."postfix-internal-policy" = {
1026 requiredBy = ["postfix.service"];
1027 wants = ["postfix-internal-policy.service"];
1028 socketConfig = {
1029 ListenStream = "/run/postfix-internal-policy.sock";
1030 };
1031 };
1032 systemd.services."postfix-internal-policy" = {
1033 after = [ "postgresql.service" ];
1034 bindsTo = [ "postgresql.service" ];
1035
1036 serviceConfig = {
1037 Type = "notify";
1038
1039 ExecStart = lib.getExe internal-policy-server;
1040
1041 Environment = [
1042 "PGDATABASE=email"
1043 ];
1044
1045 DynamicUser = false;
1046 User = "postfix-internal-policy";
1047 Group = "postfix-internal-policy";
1048 ProtectSystem = "strict";
1049 SystemCallFilter = "@system-service";
1050 NoNewPrivileges = true;
1051 ProtectKernelTunables = true;
1052 ProtectKernelModules = true;
1053 ProtectKernelLogs = true;
1054 ProtectControlGroups = true;
1055 MemoryDenyWriteExecute = true;
1056 RestrictSUIDSGID = true;
1057 KeyringMode = "private";
1058 ProtectClock = true;
1059 RestrictRealtime = true;
1060 PrivateDevices = true;
1061 PrivateTmp = true;
1062 ProtectHostname = true;
1063 ReadWritePaths = ["/run/postgresql"];
1064 };
1065 };
1066 users.users."postfix-internal-policy" = {
1067 isSystemUser = true;
1068 group = "postfix-internal-policy";
1069 };
1070 users.groups."postfix-internal-policy" = {};
1071
1072 systemd.sockets."email-password-server" = {
1073 requiredBy = ["postfix.service"];
1074 wants = ["email-password-server.service"];
1075 socketConfig = {
1076 ListenStream = "/run/email-password-server.sock";
1077 };
1078 };
1079 systemd.services."email-password-server" = {
1080 after = [ "postgresql.service" ];
1081 bindsTo = [ "postgresql.service" ];
1082
1083 serviceConfig = {
1084 Type = "notify";
1085
1086 ExecStart = getExe' password-server "password-server";
1087
1088 Environment = [
1089 "PGDATABASE=email"
1090 ];
1091
1092 LoadCredential = [
1093 "secret:${config.sops.secrets."email-password-server-secret".path}"
1094 ];
1095
1096 DynamicUser = false;
1097 User = "email-password-server";
1098 Group = "email-password-server";
1099 ProtectSystem = "strict";
1100 SystemCallFilter = "@system-service";
1101 NoNewPrivileges = true;
1102 ProtectKernelTunables = true;
1103 ProtectKernelModules = true;
1104 ProtectKernelLogs = true;
1105 ProtectControlGroups = true;
1106 MemoryDenyWriteExecute = true;
1107 RestrictSUIDSGID = true;
1108 KeyringMode = "private";
1109 ProtectClock = true;
1110 RestrictRealtime = true;
1111 PrivateDevices = true;
1112 PrivateTmp = true;
1113 ProtectHostname = true;
1114 ReadWritePaths = ["/run/postgresql"];
1115 };
1116 };
1117 users.users."email-password-server" = {
1118 isSystemUser = true;
1119 group = "email-password-server";
1120 };
1121 users.groups."email-password-server" = {};
1122 sops.secrets."email-password-server-secret" = {
1123 format = "binary";
1124 sopsFile = ./email-password-server-secret;
1125 };
1126
1127
966 services.postfwd = { 1128 services.postfwd = {
967 enable = true; 1129 enable = true;
968 cache = false; 1130 cache = false;
@@ -996,7 +1158,7 @@ in {
996 wantedBy = [ "nftables.service" ]; 1158 wantedBy = [ "nftables.service" ];
997 1159
998 timerConfig = { 1160 timerConfig = {
999 OnActiveSec = "20h"; 1161 OnCalendar = "4/8:00";
1000 RandomizedDelaySec = "8h"; 1162 RandomizedDelaySec = "8h";
1001 }; 1163 };
1002 }; 1164 };
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/audiobookshelf.yggdrasil.li b/hosts/surtr/tls/tsig_keys/audiobookshelf.yggdrasil.li
new file mode 100644
index 00000000..8dd610dd
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/audiobookshelf.yggdrasil.li
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:r9jhdTlbDnCMq1QLJutn76uz1Ml8MFs7fXYRSiVYh1gafcXXsUZBq5+qqoQI,iv:un/luttuKpCiMf53fa2SRY0ffttGiYwT8DuHCKEnnEI=,tag:SkNULZSulQmP99aB/Ec+Fw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkZGJzaEsrSU4raHlTVDVB\nczRnWVlSTTRuNXU0T3F1RTkxKytXeVJRdGpFCk9WMzNBR1NaTTMzN3BGQ2JmTjVt\nRU4rSWxCYjJPYVRzLzR0OVRYQm45TUkKLS0tIDNyMnpPN2VKUFFadTkveXRYeWps\nYUNaTjRJLzdWUnREaUVIWkpFV0FTZ2MKJS0K49SdkLW4p67FlgboHy/OVvCiUA7g\nuv5b+yotkQmh5xJwr7CUvwRewqJh56mg1yhWmE8wzpgLZMIjRXcQCQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQQ1h3M1lXTXVNd0d6cmtU\nU2JtUzFFblJudmEycnJONkkwME9wWm5jWVFzCnRYVEFWaVNvSW9GZ05TRWF4L2ho\nanltVytEU3ZOdHk1VHY5aGJDUkdDdmcKLS0tIEtzOFVkbmpjbWN5d0c1VEpxc1Rr\nSzJwclYxeC9TVWNaK2gwUmJSY0x1ZVUKTNivp5iS+1tzVMjMn17/ncvHcELhjQ/B\n0OVz4VpKM2wv6CjEcIMxmchqT8p8GFYVRrKUdqO2GEKOoe8ANtidWA==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-09T17:07:16Z",
15 "mac": "ENC[AES256_GCM,data:SwS+8UQnPgHORobKLu+u2pNaMdKIvR+etUed8btbbne/IX/Wpxt0qyPYXNNGGRkN3KAxTHWjRRdrKU1bkuTU3ER1c94T935ExDESKJLVjzaEF5VSWCqLyUNCMsY2ANw84UES2swK4YI4zF1CP7rD8tKFFld78IWZoeQ7XNGDMRA=,iv:neLvamISgQ5+aqW1iRj9xJoXq1weNNyy7KCFG2+WRQE=,tag:66SDO61WnKU6DVElo9CImg==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
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}
diff --git a/hosts/surtr/vpn/default.nix b/hosts/surtr/vpn/default.nix
index 1bdcf74e..92223144 100644
--- a/hosts/surtr/vpn/default.nix
+++ b/hosts/surtr/vpn/default.nix
@@ -1,4 +1,4 @@
1{ pkgs, config, lib, ... }: 1{ flake, pkgs, config, lib, ... }:
2 2
3with lib; 3with lib;
4 4
@@ -22,7 +22,11 @@ in {
22 "--load-credential=surtr.priv:/run/credentials/container@vpn.service/surtr.priv" 22 "--load-credential=surtr.priv:/run/credentials/container@vpn.service/surtr.priv"
23 "--network-ipvlan=ens3:upstream" 23 "--network-ipvlan=ens3:upstream"
24 ]; 24 ];
25 config = { 25 config = let hostConfig = config; in { config, pkgs, ... }: {
26 system.stateVersion = lib.mkIf hostConfig.containers."vpn".ephemeral config.system.nixos.release;
27 system.configurationRevision = mkIf (flake ? rev) flake.rev;
28 nixpkgs.pkgs = hostConfig.nixpkgs.pkgs;
29
26 boot.kernel.sysctl = { 30 boot.kernel.sysctl = {
27 "net.core.rmem_max" = 4194304; 31 "net.core.rmem_max" = 4194304;
28 "net.core.wmem_max" = 4194304; 32 "net.core.wmem_max" = 4194304;
diff --git a/hosts/surtr/vpn/geri.pub b/hosts/surtr/vpn/geri.pub
index ed5de2b2..2cd9b24e 100644
--- a/hosts/surtr/vpn/geri.pub
+++ b/hosts/surtr/vpn/geri.pub
@@ -1 +1 @@
sYuQSNZHzfegv8HRz71jnZm2nFLGeRnaGwVonhKUj2k= hhER05bvstOTGfiAG3IJsFkBNWCUZHokBXwaiC5d534=