summaryrefslogtreecommitdiff
path: root/hosts/surtr
diff options
context:
space:
mode:
Diffstat (limited to 'hosts/surtr')
-rw-r--r--hosts/surtr/changedetection-io.nix66
-rw-r--r--hosts/surtr/default.nix4
-rw-r--r--hosts/surtr/dns/default.nix2
-rw-r--r--hosts/surtr/dns/keys/changedetection.yggdrasil.li_acme18
-rw-r--r--hosts/surtr/dns/zones/li.yggdrasil.soa10
-rw-r--r--hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py2
-rw-r--r--hosts/surtr/email/default.nix169
-rw-r--r--hosts/surtr/http/default.nix3
-rw-r--r--hosts/surtr/postgresql/default.nix2
-rw-r--r--hosts/surtr/tls/default.nix8
-rw-r--r--hosts/surtr/tls/tsig_keys/changedetection.yggdrasil.li18
11 files changed, 206 insertions, 96 deletions
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 9d3101c0..4a9bd6fe 100644
--- a/hosts/surtr/default.nix
+++ b/hosts/surtr/default.nix
@@ -8,6 +8,7 @@ with lib;
8 ./zfs.nix ./dns ./tls ./http ./bifrost ./matrix ./postgresql 8 ./zfs.nix ./dns ./tls ./http ./bifrost ./matrix ./postgresql
9 ./prometheus ./email ./vpn ./borg.nix ./etebase ./immich.nix 9 ./prometheus ./email ./vpn ./borg.nix ./etebase ./immich.nix
10 ./paperless.nix ./hledger.nix ./audiobookshelf.nix ./kimai.nix 10 ./paperless.nix ./hledger.nix ./audiobookshelf.nix ./kimai.nix
11 ./changedetection-io.nix
11 ]; 12 ];
12 13
13 config = { 14 config = {
@@ -22,7 +23,6 @@ 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
@@ -31,7 +31,7 @@ with lib;
31 kernelPatches = [ 31 kernelPatches = [
32 { name = "zswap-default"; 32 { name = "zswap-default";
33 patch = null; 33 patch = null;
34 extraStructuredConfig = with lib.kernel; { 34 structuredExtraConfig = with lib.kernel; {
35 ZSWAP_DEFAULT_ON = yes; 35 ZSWAP_DEFAULT_ON = yes;
36 ZSWAP_SHRINKER_DEFAULT_ON = yes; 36 ZSWAP_SHRINKER_DEFAULT_ON = yes;
37 }; 37 };
diff --git a/hosts/surtr/dns/default.nix b/hosts/surtr/dns/default.nix
index 8aca2b97..96599901 100644
--- a/hosts/surtr/dns/default.nix
+++ b/hosts/surtr/dns/default.nix
@@ -157,7 +157,7 @@ in {
157 ${concatMapStringsSep "\n" mkZone [ 157 ${concatMapStringsSep "\n" mkZone [
158 { domain = "yggdrasil.li"; 158 { domain = "yggdrasil.li";
159 addACLs = { "yggdrasil.li" = ["ymir_acme_acl"]; }; 159 addACLs = { "yggdrasil.li" = ["ymir_acme_acl"]; };
160 acmeDomains = ["surtr.yggdrasil.li" "yggdrasil.li" "etesync.yggdrasil.li" "immich.yggdrasil.li" "app.etesync.yggdrasil.li" "paperless.yggdrasil.li" "hledger.yggdrasil.li" "audiobookshelf.yggdrasil.li" "kimai.yggdrasil.li"]; 160 acmeDomains = ["surtr.yggdrasil.li" "yggdrasil.li" "etesync.yggdrasil.li" "immich.yggdrasil.li" "app.etesync.yggdrasil.li" "paperless.yggdrasil.li" "hledger.yggdrasil.li" "audiobookshelf.yggdrasil.li" "kimai.yggdrasil.li" "changedetection.yggdrasil.li"];
161 } 161 }
162 { domain = "nights.email"; 162 { domain = "nights.email";
163 addACLs = { "nights.email" = ["ymir_acme_acl"]; }; 163 addACLs = { "nights.email" = ["ymir_acme_acl"]; };
diff --git a/hosts/surtr/dns/keys/changedetection.yggdrasil.li_acme b/hosts/surtr/dns/keys/changedetection.yggdrasil.li_acme
new file mode 100644
index 00000000..dcc7f85b
--- /dev/null
+++ b/hosts/surtr/dns/keys/changedetection.yggdrasil.li_acme
@@ -0,0 +1,18 @@
1{
2 "data": "ENC[AES256_GCM,data:QWZAer8xKZvAGl3HxaIdsOT1n3os4EDyQoZOU3YzwDpPfweVRhhBfyAg7M6rMRj8K8ffkkRWatDmgyHV1R43GfNyb1sLjqdqPysYXxC8KlP22WlT+1xstQ2q1KYmeN6VEKF0q+QOMMPRvwQbSQ0eC4mXcE+WgQSTVywjab9hQuc8vin69RbFxbhepxYLXT1rzQpLlxFmUNZBcLpSqsHkSDa2B0d4j2kIvSl2BuUgb3QJwgyNS5pGbnfyVfmus7p5+/pVFCe5EwTVjwgpn/cpIB0mu1Bbt9r0EvCkYXI6wKcLDVbfdV7KsA==,iv:wjHpcClpybzCIi3JhxgXTd5nW9y223pJn2rBde/2cy8=,tag:etfnmj+HhIKeZMGjxE5jiw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlWkk2Z0FUNlNoaFVaSFEr\nSE54eUpHaTNzTTdjaytJemtQUW1mWjJNUGhzCkU1OGlNb3pidVFKalVCZUhBODNi\nMjcxR0xLUDRwYkZ5V2I1Q3Z6Y2pmRWMKLS0tIGNzaGNiTEMvdEhBMDRZY2pDQzNu\nWkpkcVNYVjJZQS9QRHEyOUx6RVpQVjAKKGjVrfeovCIml2hExydC9Cd7PyungtpJ\nCdXfrvzP/OtoBSiEDQGC2VafwKkZ98dQqVRnfVApDoxdVQ8vIrxmKQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBTbUNBV1NYZEtKaDlzUVBF\nU2IrZ3dSWS9keVE1ak9qWkppc1hOY3JpMjEwCmNUbVJJMFNObmptVGQ0K1ovanVm\nc1dzN0VRVThyVWxPWTFLbldPQnE3Z2sKLS0tIEdWRENRRFROSm5SQ04yTG1wZWJ2\nNDMxK1ArYmdiQWJZV0d2TElZcFZLNVEKtmVrSIOcP4Ek1WW85f2/dNVYQMz9XqZ3\n0J04kqvkHZuM8PiBDg2l2rSh0xhHz3xb1iBhAddLXEjeEfy6o9HKyg==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-12-08T12:46:12Z",
15 "mac": "ENC[AES256_GCM,data:waY6IDfabZ8B8069liXh7RXjgUTpOdr4U9VQK5xYRujAlI//Ea5lM2ODHJ7PrAkZsK0TGB9ezN8SA5QpxYZwOcpxg45jNbTALxZsZMEzrtCy4wSiBdiLvRoTXvwMZsnsaQEGk2ij2rEqNEOYYBFapBoIz2w5kbEZrrhVRHSkNME=,iv:tpU++qliONinepku/gdPJQ/h2NdyNw3GY+RV+6UM07U=,tag:yieMw3BOC134zAIqb1Fvjg==,type:str]",
16 "version": "3.11.0"
17 }
18}
diff --git a/hosts/surtr/dns/zones/li.yggdrasil.soa b/hosts/surtr/dns/zones/li.yggdrasil.soa
index 500194ae..5234576f 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 2025060700 ; serial 4 2025120800 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -109,6 +109,14 @@ _acme-challenge.kimai IN NS ns.yggdrasil.li.
109 109
110kimai IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::" 110kimai IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
111 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
112vidhar IN AAAA 2a03:4000:52:ada:4:1:: 120vidhar IN AAAA 2a03:4000:52:ada:4:1::
113vidhar IN MX 0 ymir.yggdrasil.li 121vidhar IN MX 0 ymir.yggdrasil.li
114vidhar IN TXT "v=spf1 redirect=yggdrasil.li" 122vidhar IN TXT "v=spf1 redirect=yggdrasil.li"
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 7c931559..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
@@ -35,7 +35,7 @@ class PolicyHandler(StreamRequestHandler):
35 user = self.args['ccert_subject'] 35 user = self.args['ccert_subject']
36 relay_eligible = True 36 relay_eligible = True
37 37
38 if user: 38 if user and '@' in self.args['sender']:
39 with self.server.db_pool.connection() as conn: 39 with self.server.db_pool.connection() as conn:
40 local, domain = self.args['sender'].split(sep='@', maxsplit=1) 40 local, domain = self.args['sender'].split(sep='@', maxsplit=1)
41 extension = None 41 extension = None
diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix
index c993bb18..b0e95a0e 100644
--- a/hosts/surtr/email/default.nix
+++ b/hosts/surtr/email/default.nix
@@ -124,19 +124,20 @@ in {
124 services.postfix = { 124 services.postfix = {
125 enable = true; 125 enable = true;
126 enableSmtp = false; 126 enableSmtp = false;
127 hostname = "surtr.yggdrasil.li";
128 recipientDelimiter = "";
129 setSendmail = true; 127 setSendmail = true;
130 postmasterAlias = ""; rootAlias = ""; extraAliases = ""; 128 postmasterAlias = ""; rootAlias = ""; extraAliases = "";
131 destination = []; 129 settings.main = {
132 sslCert = "/run/credentials/postfix.service/surtr.yggdrasil.li.pem"; 130 recpipient_delimiter = "";
133 sslKey = "/run/credentials/postfix.service/surtr.yggdrasil.li.key.pem"; 131 mydestination = [];
134 networks = []; 132 mynetworks = [];
135 config = let 133 myhostname = "surtr.yggdrasil.li";
136 relay_ccert = "texthash:${pkgs.writeText "relay_ccert" ""}"; 134
137 in {
138 smtpd_tls_security_level = "may"; 135 smtpd_tls_security_level = "may";
139 136
137 smtpd_tls_chain_files = [
138 "/run/credentials/postfix.service/surtr.yggdrasil.li.full.pem"
139 ];
140
140 #the dh params 141 #the dh params
141 smtpd_tls_dh1024_param_file = toString config.security.dhparams.params."postfix-1024".path; 142 smtpd_tls_dh1024_param_file = toString config.security.dhparams.params."postfix-1024".path;
142 smtpd_tls_dh512_param_file = toString config.security.dhparams.params."postfix-512".path; 143 smtpd_tls_dh512_param_file = toString config.security.dhparams.params."postfix-512".path;
@@ -171,21 +172,14 @@ in {
171 172
172 smtp_tls_connection_reuse = true; 173 smtp_tls_connection_reuse = true;
173 174
174 tls_server_sni_maps = ''texthash:${pkgs.writeText "sni" ( 175 tls_server_sni_maps = "inline:{${concatMapStringsSep ", " (domain: "{ ${domain} = /run/credentials/postfix.service/${removePrefix "." domain}.full.pem }") (concatMap (domain: [domain "mailin.${domain}" "mailsub.${domain}" ".${domain}"]) emailDomains)}}";
175 concatMapStringsSep "\n\n" (domain:
176 concatMapStringsSep "\n" (subdomain: "${subdomain} /run/credentials/postfix.service/${removePrefix "." subdomain}.full.pem")
177 [domain "mailin.${domain}" "mailsub.${domain}" ".${domain}"]
178 ) emailDomains
179 )}'';
180 176
181 smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix"; 177 smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix";
182 178
183 local_recipient_maps = ""; 179 local_recipient_maps = "";
184 180
185 # 10 GiB 181 message_size_limit = 10 * 1024 * 1024 * 1024;
186 message_size_limit = "10737418240"; 182 mailbox_size_limit = 10 * 1024 * 1024 * 1024;
187 # 10 GiB
188 mailbox_size_limit = "10737418240";
189 183
190 smtpd_delay_reject = true; 184 smtpd_delay_reject = true;
191 smtpd_helo_required = true; 185 smtpd_helo_required = true;
@@ -200,7 +194,6 @@ in {
200 dbname = email 194 dbname = email
201 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' 195 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s'
202 ''}" 196 ''}"
203 "check_ccert_access ${relay_ccert}"
204 "reject_non_fqdn_helo_hostname" 197 "reject_non_fqdn_helo_hostname"
205 "reject_invalid_helo_hostname" 198 "reject_invalid_helo_hostname"
206 "reject_unauth_destination" 199 "reject_unauth_destination"
@@ -221,7 +214,6 @@ in {
221 address_verify_sender_ttl = "30045s"; 214 address_verify_sender_ttl = "30045s";
222 215
223 smtpd_relay_restrictions = [ 216 smtpd_relay_restrictions = [
224 "check_ccert_access ${relay_ccert}"
225 "reject_unauth_destination" 217 "reject_unauth_destination"
226 ]; 218 ];
227 219
@@ -244,6 +236,37 @@ in {
244 bounce_queue_lifetime = "20m"; 236 bounce_queue_lifetime = "20m";
245 delay_warning_time = "10m"; 237 delay_warning_time = "10m";
246 238
239 failure_template_file = toString (pkgs.writeText "failure.cf" ''
240 Charset: us-ascii
241 From: Mail Delivery System <MAILER-DAEMON>
242 Subject: Undelivered Mail Returned to Sender
243 Postmaster-Subject: Postmaster Copy: Undelivered Mail
244
245 This is the mail system at host $myhostname.
246
247 I'm sorry to have to inform you that your message could not
248 be delivered to one or more recipients. It's attached below.
249
250 The mail system
251 '');
252 delay_template_file = toString (pkgs.writeText "delay.cf" ''
253 Charset: us-ascii
254 From: Mail Delivery System <MAILER-DAEMON>
255 Subject: Delayed Mail (still being retried)
256 Postmaster-Subject: Postmaster Warning: Delayed Mail
257
258 This is the mail system at host $myhostname.
259
260 ####################################################################
261 # THIS IS A WARNING ONLY. YOU DO NOT NEED TO RESEND YOUR MESSAGE. #
262 ####################################################################
263
264 Your message could not be delivered for more than $delay_warning_time_minutes minute(s).
265 It will be retried until it is $maximal_queue_lifetime_minutes minute(s) old.
266
267 The mail system
268 '');
269
247 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" '' 270 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" ''
248 # Allow DSN requests from local subnet only 271 # Allow DSN requests from local subnet only
249 192.168.0.0/16 silent-discard 272 192.168.0.0/16 silent-discard
@@ -268,7 +291,7 @@ in {
268 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp"; 291 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp";
269 smtputf8_enable = false; 292 smtputf8_enable = false;
270 293
271 authorized_submit_users = "inline:{ root= postfwd= dovecot2= }"; 294 authorized_submit_users = "inline:{ root= postfwd= ${config.services.dovecot2.user}= }";
272 authorized_flush_users = "inline:{ root= }"; 295 authorized_flush_users = "inline:{ root= }";
273 authorized_mailq_users = "inline:{ root= }"; 296 authorized_mailq_users = "inline:{ root= }";
274 297
@@ -287,7 +310,7 @@ in {
287 query = SELECT value FROM recipient_bcc_maps WHERE key = '%s' 310 query = SELECT value FROM recipient_bcc_maps WHERE key = '%s'
288 ''}''; 311 ''}'';
289 }; 312 };
290 masterConfig = { 313 settings.master = {
291 "465" = { 314 "465" = {
292 type = "inet"; 315 type = "inet";
293 private = false; 316 private = false;
@@ -355,7 +378,10 @@ in {
355 maxproc = 0; 378 maxproc = 0;
356 args = [ 379 args = [
357 "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" '' 380 "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" ''
381 if /^Received: /
382 !/by surtr\.yggdrasil\.li/ STRIP
358 /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1 383 /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1
384 endif
359 ''}" 385 ''}"
360 ]; 386 ];
361 }; 387 };
@@ -507,49 +533,49 @@ in {
507 }; 533 };
508 }; 534 };
509 535
510 users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user "dovecot2" ]; 536 users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user config.services.dovecot2.user ];
511 537
512 services.redis.servers.rspamd.enable = true; 538 services.redis.servers.rspamd.enable = true;
513 539
514 users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ]; 540 users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ];
515 541
516 environment.systemPackages = with pkgs; [ dovecot_pigeonhole dovecot_fts_xapian ]; 542 environment.systemPackages = with pkgs; [ dovecot_pigeonhole dovecot-fts-flatcurve ];
517 services.dovecot2 = { 543 services.dovecot2 = {
518 enable = true; 544 enable = true;
519 enablePAM = false; 545 enablePAM = false;
520 sslServerCert = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.pem"; 546 sslServerCert = "/run/credentials/dovecot.service/surtr.yggdrasil.li.pem";
521 sslServerKey = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.key.pem"; 547 sslServerKey = "/run/credentials/dovecot.service/surtr.yggdrasil.li.key.pem";
522 sslCACert = toString ./ca/ca.crt; 548 sslCACert = toString ./ca/ca.crt;
523 mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u"; 549 mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u";
524 mailPlugins.globally.enable = [ "fts" "fts_xapian" ]; 550 mailPlugins.globally.enable = [ "fts" "fts_flatcurve" ];
525 protocols = [ "lmtp" "sieve" ]; 551 protocols = [ "lmtp" "sieve" ];
526 sieve = { 552 sieve = {
527 extensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"]; 553 extensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation" "vacation-seconds" "vnd.dovecot.debug"];
528 globalExtensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"]; 554 globalExtensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation" "vacation-seconds" "vnd.dovecot.debug"];
529 }; 555 };
530 extraConfig = let 556 extraConfig = let
531 dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" '' 557 dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" ''
532 driver = pgsql 558 driver = pgsql
533 connect = dbname=email 559 connect = dbname=email
534 password_query = SELECT (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN NULL ELSE "password" END) as password, (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN true WHEN password IS NULL THEN true ELSE NULL END) as nopassword, "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' 560 password_query = SELECT (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN NULL ELSE "password" END) as password, (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN true WHEN password IS NULL THEN true ELSE NULL END) as nopassword, "user", quota_rule, '${config.services.dovecot2.user}' as uid, '${config.services.dovecot2.group}' as gid FROM imap_user WHERE "user" = '%n'
535 user_query = SELECT "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' 561 user_query = SELECT "user", quota_rule, '${config.services.dovecot2.user}' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n'
536 iterate_query = SELECT "user" FROM imap_user 562 iterate_query = SELECT "user" FROM imap_user
537 ''; 563 '';
538 in '' 564 in ''
539 mail_home = /var/lib/mail/%u 565 mail_home = /var/lib/mail/%u
540 566
541 mail_plugins = $mail_plugins quota 567 mail_plugins = $mail_plugins quota fts fts_flatcurve
542 568
543 first_valid_uid = ${toString config.users.users.dovecot2.uid} 569 first_valid_uid = ${toString config.users.users.${config.services.dovecot2.user}.uid}
544 last_valid_uid = ${toString config.users.users.dovecot2.uid} 570 last_valid_uid = ${toString config.users.users.${config.services.dovecot2.user}.uid}
545 first_valid_gid = ${toString config.users.groups.dovecot2.gid} 571 first_valid_gid = ${toString config.users.groups.${config.services.dovecot2.group}.gid}
546 last_valid_gid = ${toString config.users.groups.dovecot2.gid} 572 last_valid_gid = ${toString config.users.groups.${config.services.dovecot2.group}.gid}
547 573
548 ${concatMapStringsSep "\n\n" (domain: 574 ${concatMapStringsSep "\n\n" (domain:
549 concatMapStringsSep "\n" (subdomain: '' 575 concatMapStringsSep "\n" (subdomain: ''
550 local_name ${subdomain} { 576 local_name ${subdomain} {
551 ssl_cert = </run/credentials/dovecot2.service/${subdomain}.pem 577 ssl_cert = </run/credentials/dovecot.service/${subdomain}.pem
552 ssl_key = </run/credentials/dovecot2.service/${subdomain}.key.pem 578 ssl_key = </run/credentials/dovecot.service/${subdomain}.key.pem
553 } 579 }
554 '') ["imap.${domain}" domain] 580 '') ["imap.${domain}" domain]
555 ) emailDomains} 581 ) emailDomains}
@@ -570,10 +596,10 @@ in {
570 auth_debug = yes 596 auth_debug = yes
571 597
572 service auth { 598 service auth {
573 user = dovecot2 599 user = ${config.services.dovecot2.user}
574 } 600 }
575 service auth-worker { 601 service auth-worker {
576 user = dovecot2 602 user = ${config.services.dovecot2.user}
577 } 603 }
578 604
579 userdb { 605 userdb {
@@ -594,7 +620,7 @@ in {
594 args = ${pkgs.writeText "dovecot-sql.conf" '' 620 args = ${pkgs.writeText "dovecot-sql.conf" ''
595 driver = pgsql 621 driver = pgsql
596 connect = dbname=email 622 connect = dbname=email
597 user_query = SELECT DISTINCT ON (extension IS NULL, local IS NULL) "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM lmtp_mapping WHERE CASE WHEN extension IS NOT NULL AND local IS NOT NULL THEN ('%n' :: citext) = local || '+' || extension AND domain = ('%d' :: citext) WHEN local IS NOT NULL THEN (local = ('%n' :: citext) OR ('%n' :: citext) ILIKE local || '+%%') AND domain = ('%d' :: citext) WHEN extension IS NOT NULL THEN ('%n' :: citext) ILIKE '%%+' || extension AND domain = ('%d' :: citext) ELSE domain = ('%d' :: citext) END ORDER BY (extension IS NULL) ASC, (local IS NULL) ASC 623 user_query = SELECT DISTINCT ON (extension IS NULL, local IS NULL) "user", quota_rule, '${config.services.dovecot2.user}' as uid, '${config.services.dovecot2.group}' as gid FROM lmtp_mapping WHERE CASE WHEN extension IS NOT NULL AND local IS NOT NULL THEN ('%n' :: citext) = local || '+' || extension AND domain = ('%d' :: citext) WHEN local IS NOT NULL THEN (local = ('%n' :: citext) OR ('%n' :: citext) ILIKE local || '+%%') AND domain = ('%d' :: citext) WHEN extension IS NOT NULL THEN ('%n' :: citext) ILIKE '%%+' || extension AND domain = ('%d' :: citext) ELSE domain = ('%d' :: citext) END ORDER BY (extension IS NULL) ASC, (local IS NULL) ASC
598 ''} 624 ''}
599 625
600 skip = never 626 skip = never
@@ -664,7 +690,7 @@ in {
664 quota_status_success = DUNNO 690 quota_status_success = DUNNO
665 quota_status_nouser = DUNNO 691 quota_status_nouser = DUNNO
666 quota_grace = 10%% 692 quota_grace = 10%%
667 quota_max_mail_size = ${config.services.postfix.config.message_size_limit} 693 quota_max_mail_size = ${toString config.services.postfix.settings.main.message_size_limit}
668 quota_vsizes = yes 694 quota_vsizes = yes
669 } 695 }
670 696
@@ -700,13 +726,16 @@ in {
700 } 726 }
701 727
702 plugin { 728 plugin {
703 plugin = fts fts_xapian 729 fts = flatcurve
704 fts = xapian 730
705 fts_xapian = partial=3 full=20 attachments=1 verbose=1 731 fts_languages = en de
732 fts_tokenizers = generic email-address
706 733
707 fts_autoindex = yes 734 fts_tokenizer_email_address = maxlen=100
735 fts_tokenizer_generic = algorithm=simple maxlen=30
708 736
709 fts_enforced = no 737 fts_filters = normalizer-icu snowball stopwords
738 fts_filters_en = lowercase snowball stopwords
710 } 739 }
711 740
712 service indexer-worker { 741 service indexer-worker {
@@ -715,30 +744,6 @@ in {
715 ''; 744 '';
716 }; 745 };
717 746
718 systemd.services.dovecot-fts-xapian-optimize = {
719 description = "Optimize dovecot indices for fts_xapian";
720 requisite = [ "dovecot2.service" ];
721 after = [ "dovecot2.service" ];
722 startAt = "*-*-* 22:00:00 Europe/Berlin";
723 serviceConfig = {
724 Type = "oneshot";
725 ExecStart = "${getExe' pkgs.dovecot "doveadm"} fts optimize -A";
726 PrivateDevices = true;
727 PrivateNetwork = true;
728 ProtectKernelTunables = true;
729 ProtectKernelModules = true;
730 ProtectControlGroups = true;
731 ProtectHome = true;
732 ProtectSystem = true;
733 PrivateTmp = true;
734 };
735 };
736 systemd.timers.dovecot-fts-xapian-optimize = {
737 timerConfig = {
738 RandomizedDelaySec = 4 * 3600;
739 };
740 };
741
742 environment.etc = { 747 environment.etc = {
743 "dovecot/sieve_before.d/tag-junk.sieve".text = '' 748 "dovecot/sieve_before.d/tag-junk.sieve".text = ''
744 require ["imap4flags"]; 749 require ["imap4flags"];
@@ -783,28 +788,26 @@ in {
783 788
784 security.acme.rfc2136Domains = { 789 security.acme.rfc2136Domains = {
785 "surtr.yggdrasil.li" = { 790 "surtr.yggdrasil.li" = {
786 restartUnits = [ "postfix.service" "dovecot2.service" ]; 791 restartUnits = [ "postfix.service" "dovecot.service" ];
787 }; 792 };
788 } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains) 793 } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains)
789 // listToAttrs (concatMap (domain: [ 794 // listToAttrs (concatMap (domain: [
790 (nameValuePair domain { restartUnits = ["postfix.service" "dovecot2.service"]; }) 795 (nameValuePair domain { restartUnits = ["postfix.service" "dovecot.service"]; })
791 (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; }) 796 (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; })
792 (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; }) 797 (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; })
793 (nameValuePair "imap.${domain}" { restartUnits = ["dovecot2.service"]; }) 798 (nameValuePair "imap.${domain}" { restartUnits = ["dovecot.service"]; })
794 (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; }) 799 (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; })
795 ]) emailDomains); 800 ]) emailDomains);
796 801
797 systemd.services.postfix = { 802 systemd.services.postfix = {
798 serviceConfig.LoadCredential = [ 803 serviceConfig.LoadCredential = let
799 "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" 804 tlsCredential = domain: "${domain}.full.pem:${config.security.acme.certs.${domain}.directory}/full.pem";
800 "surtr.yggdrasil.li.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/fullchain.pem" 805 in [
801 ] ++ concatMap (domain: 806 (tlsCredential "surtr.yggdrasil.li")
802 map (subdomain: "${subdomain}.full.pem:${config.security.acme.certs.${subdomain}.directory}/full.pem") 807 ] ++ concatMap (domain: map tlsCredential [domain "mailin.${domain}" "mailsub.${domain}"]) emailDomains;
803 [domain "mailin.${domain}" "mailsub.${domain}"]
804 ) emailDomains;
805 }; 808 };
806 809
807 systemd.services.dovecot2 = { 810 systemd.services.dovecot = {
808 preStart = '' 811 preStart = ''
809 for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do 812 for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do
810 ${getExe' pkgs.dovecot_pigeonhole "sievec"} $f 813 ${getExe' pkgs.dovecot_pigeonhole "sievec"} $f
diff --git a/hosts/surtr/http/default.nix b/hosts/surtr/http/default.nix
index f3a7154e..4cbd3eae 100644
--- a/hosts/surtr/http/default.nix
+++ b/hosts/surtr/http/default.nix
@@ -7,14 +7,11 @@
7 config = { 7 config = {
8 services.nginx = { 8 services.nginx = {
9 enable = true; 9 enable = true;
10 package = pkgs.nginxQuic;
11 recommendedGzipSettings = false; 10 recommendedGzipSettings = false;
12 recommendedProxySettings = true; 11 recommendedProxySettings = true;
13 recommendedTlsSettings = true; 12 recommendedTlsSettings = true;
14 sslDhparam = config.security.dhparams.params.nginx.path; 13 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/postgresql/default.nix b/hosts/surtr/postgresql/default.nix
index 840b46c6..3786ea7c 100644
--- a/hosts/surtr/postgresql/default.nix
+++ b/hosts/surtr/postgresql/default.nix
@@ -334,7 +334,7 @@ in {
334 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), 334 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
335 key text NOT NULL CONSTRAINT key_not_empty CHECK (key <> '''), 335 key text NOT NULL CONSTRAINT key_not_empty CHECK (key <> '''),
336 value text NOT NULL CONSTRAINT value_not_empty CHECK (value <> '''), 336 value text NOT NULL CONSTRAINT value_not_empty CHECK (value <> '''),
337 CONSTRAINT key_unique UNIQUE (key) 337 CONSTRAINT recipient_bcc_maps_key_unique UNIQUE (key)
338 ); 338 );
339 339
340 COMMIT; 340 COMMIT;
diff --git a/hosts/surtr/tls/default.nix b/hosts/surtr/tls/default.nix
index b1c05888..2c346baa 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
@@ -62,7 +62,7 @@ in {
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} 65 RFC2136_TSIG_SECRET_FILE=/run/credentials/acme-order-renew-${domain}.service/${tsigSecretName domain}
66 RFC2136_TTL=0 66 RFC2136_TTL=0
67 RFC2136_PROPAGATION_TIMEOUT=60 67 RFC2136_PROPAGATION_TIMEOUT=60
68 RFC2136_POLLING_INTERVAL=2 68 RFC2136_POLLING_INTERVAL=2
@@ -79,12 +79,12 @@ in {
79 sops.secrets = mapAttrs' (domain: domainCfg: nameValuePair (tsigSecretName domain) { 79 sops.secrets = mapAttrs' (domain: domainCfg: nameValuePair (tsigSecretName domain) {
80 format = "binary"; 80 format = "binary";
81 sopsFile = tsigKey domain; 81 sopsFile = tsigKey domain;
82 restartUnits = [ "acme-${domain}.service" ]; 82 restartUnits = [ "acme-order-renew${domain}.service" ];
83 }) cfg.rfc2136Domains; 83 }) cfg.rfc2136Domains;
84 84
85 # Provide appropriate `tsig_key/*` to systemd service performing 85 # Provide appropriate `tsig_key/*` to systemd service performing
86 # certificate provisioning 86 # certificate provisioning
87 systemd.services = mapAttrs' (domain: domainCfg: nameValuePair "acme-${domain}" { 87 systemd.services = mapAttrs' (domain: domainCfg: nameValuePair "acme-order-renew-${domain}" {
88 after = [ "knot.service" ]; 88 after = [ "knot.service" ];
89 bindsTo = [ "knot.service" ]; 89 bindsTo = [ "knot.service" ];
90 serviceConfig = { 90 serviceConfig = {
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}