diff options
-rw-r--r-- | hosts/surtr/email/default.nix | 225 | ||||
-rw-r--r-- | hosts/surtr/postgresql.nix | 12 | ||||
-rw-r--r-- | hosts/surtr/ruleset.nft | 10 |
3 files changed, 240 insertions, 7 deletions
diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix index 68690d55..5f4d9725 100644 --- a/hosts/surtr/email/default.nix +++ b/hosts/surtr/email/default.nix | |||
@@ -2,7 +2,14 @@ | |||
2 | 2 | ||
3 | with lib; | 3 | with lib; |
4 | 4 | ||
5 | { | 5 | let |
6 | compileSieve = name: text: pkgs.runCommand name {} '' | ||
7 | mkdir $out | ||
8 | cp ${pkgs.writeText name '' | ||
9 | ''} $out/${name} | ||
10 | ${pkgs.dovecot_pigeonhole}/bin/sievec $out/${name} | ||
11 | ''}; | ||
12 | in { | ||
6 | config = { | 13 | config = { |
7 | services.postfix = { | 14 | services.postfix = { |
8 | enable = true; | 15 | enable = true; |
@@ -40,6 +47,8 @@ with lib; | |||
40 | #enable TLS logging to see the ciphers for outbound connections | 47 | #enable TLS logging to see the ciphers for outbound connections |
41 | smtp_tls_loglevel = "1"; | 48 | smtp_tls_loglevel = "1"; |
42 | 49 | ||
50 | smtpd_tls_received_header = true; | ||
51 | |||
43 | smtpd_tls_ask_ccert = true; | 52 | smtpd_tls_ask_ccert = true; |
44 | smtpd_tls_CAfile = toString ./ca/ca.crt; | 53 | smtpd_tls_CAfile = toString ./ca/ca.crt; |
45 | 54 | ||
@@ -88,8 +97,8 @@ with lib; | |||
88 | authorized_verp_clients = "$mynetworks"; | 97 | authorized_verp_clients = "$mynetworks"; |
89 | 98 | ||
90 | milter_default_action = "accept"; | 99 | milter_default_action = "accept"; |
91 | smtpd_milters = [config.services.opendkim.socket]; | 100 | smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; |
92 | non_smtpd_milters = [config.services.opendkim.socket]; | 101 | non_smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; |
93 | 102 | ||
94 | alias_maps = ""; | 103 | alias_maps = ""; |
95 | 104 | ||
@@ -113,6 +122,19 @@ with lib; | |||
113 | sender_canonical_classes = "envelope_sender"; | 122 | sender_canonical_classes = "envelope_sender"; |
114 | recipient_canonical_maps = "tcp:localhost:${toString config.services.postsrsd.reversePort}"; | 123 | recipient_canonical_maps = "tcp:localhost:${toString config.services.postsrsd.reversePort}"; |
115 | recipient_canonical_classes = ["envelope_recipient" "header_recipient"]; | 124 | recipient_canonical_classes = ["envelope_recipient" "header_recipient"]; |
125 | |||
126 | virtual_mailbox_domains = ''pgsql:${pkgs.writeText "virtual_mailbox_domains.cf" '' | ||
127 | dbname = emails | ||
128 | table = virtual_mailbox_domain | ||
129 | select_field = domain | ||
130 | where_field = domain | ||
131 | ''}''; | ||
132 | virtual_mailbox_maps = ''pgsql:${pkgs.writeText "virtual_mailbox_maps.cf" '' | ||
133 | dbname = emails | ||
134 | table = virtual_mailbox_mapping | ||
135 | select_field = mailbox | ||
136 | where_field = lookup | ||
137 | ''}''; | ||
116 | }; | 138 | }; |
117 | masterConfig = { | 139 | masterConfig = { |
118 | smtps = { | 140 | smtps = { |
@@ -155,6 +177,187 @@ with lib; | |||
155 | ''; | 177 | ''; |
156 | }; | 178 | }; |
157 | 179 | ||
180 | services.rspamd = { | ||
181 | enable = true; | ||
182 | workers = { | ||
183 | controller = {}; | ||
184 | external = { | ||
185 | type = "rspamd_proxy"; | ||
186 | bindSockets = [ | ||
187 | { mode = "0660"; | ||
188 | socket = "/run/rspamd/rspamd-milter.sock"; | ||
189 | owner = config.services.rspamd.user; | ||
190 | group = config.services.rspamd.group; | ||
191 | } | ||
192 | ]; | ||
193 | extraConfig = '' | ||
194 | milter = yes; | ||
195 | |||
196 | upstream "local" { | ||
197 | default = yes; | ||
198 | self_scan = yes; | ||
199 | } | ||
200 | ''; | ||
201 | }; | ||
202 | }; | ||
203 | locals = { | ||
204 | "milter_headers.conf".text = '' | ||
205 | use = ["authentication-results", "x-spamd-result", "x-rspamd-queue-id", "x-rspamd-server", "x-spam-level", "x-spam-status"]; | ||
206 | extended_headers_rcpt = []; | ||
207 | ''; | ||
208 | "actions.conf".text = '' | ||
209 | reject = 15; | ||
210 | add_header = 10; | ||
211 | greylist = 5; | ||
212 | ''; | ||
213 | "groups.conf".text = '' | ||
214 | symbols { | ||
215 | "BAYES_SPAM" { | ||
216 | weight = 2.0; | ||
217 | } | ||
218 | } | ||
219 | ''; | ||
220 | "dmarc.conf".text = '' | ||
221 | reporting = true; | ||
222 | send_reports = true; | ||
223 | report_settings { | ||
224 | org_name = "Yggdrasil.li"; | ||
225 | domain = "yggdrasil.li"; | ||
226 | email = "postmaster@yggdrasil.li"; | ||
227 | } | ||
228 | ''; | ||
229 | "redis.conf".text = '' | ||
230 | servers = "${config.services.redis.servers.rspamd.unixSocket}"; | ||
231 | ''; | ||
232 | "dkim_signing.conf".text = "enabled = false;"; | ||
233 | "neural.conf".text = "enabled = false;"; | ||
234 | "classifier-bayes.conf".text = '' | ||
235 | enable = true; | ||
236 | expire = 8640000; | ||
237 | new_schema = true; | ||
238 | backend = "redis"; | ||
239 | per_user = true; | ||
240 | min_learns = 0; | ||
241 | |||
242 | autolearn = [0, 10]; | ||
243 | |||
244 | statfile { | ||
245 | symbol = "BAYES_HAM"; | ||
246 | spam = false; | ||
247 | } | ||
248 | statfile { | ||
249 | symbol = "BAYES_SPAM"; | ||
250 | spam = true; | ||
251 | } | ||
252 | ''; | ||
253 | # "redirectors.inc".text = '' | ||
254 | # visit.creeper.host | ||
255 | # ''; | ||
256 | }; | ||
257 | }; | ||
258 | |||
259 | users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user ]; | ||
260 | |||
261 | services.redis.servers.rspamd = { | ||
262 | enable = true; | ||
263 | vmOverCommit = true; | ||
264 | }; | ||
265 | |||
266 | users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ]; | ||
267 | |||
268 | services.dovecot2 = { | ||
269 | enable = true; | ||
270 | enableImap = false; | ||
271 | sslServerCert = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.pem"; | ||
272 | sslServerKey = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.key.pem"; | ||
273 | sslCACert = ./ca/ca.crt; | ||
274 | mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8"; | ||
275 | modules = with pkgs; [ dovecot_pigeonhole ]; | ||
276 | protocols = [ "imaps" "lmtp" "sieve" ]; | ||
277 | extraConfig = '' | ||
278 | mail_home = /var/lib/mail/%u | ||
279 | |||
280 | local_name imap.bouncy.email { | ||
281 | ssl_cert = <$/run/credentials/dovecot2.service/imap.bouncy.email.pem | ||
282 | ssl_key = <$/run/credentials/dovecot2.service/imap.bouncy.email.key.pem | ||
283 | } | ||
284 | local_name bouncy.email { | ||
285 | ssl_cert = <$/run/credentials/dovecot2.service/bouncy.email.pem | ||
286 | ssl_key = <$/run/credentials/dovecot2.service/bouncy.email.key.pem | ||
287 | } | ||
288 | |||
289 | ssl_require_crl = yes | ||
290 | ssl_verify_client_cert = yes | ||
291 | auth_ssl_username_from_cert = yes | ||
292 | auth_mechanisms = external | ||
293 | |||
294 | userdb sql { | ||
295 | args = ${pkgs.writeText "dovecot-sql.conf" '' | ||
296 | driver = pgsql | ||
297 | connect = host=localhost dbname=email | ||
298 | user_query = SELECT mailbox AS user, quota_rule FROM mailbox WHERE mailbox = '%u' | ||
299 | ''} | ||
300 | default_fields = uid=dovecot2 gid=dovecot2 | ||
301 | } | ||
302 | |||
303 | mail_plugins = $mail_plugins quota | ||
304 | mailbox_list_index = yes | ||
305 | postmaster_address = postmaster@yggdrasil.li | ||
306 | recipient_delimiter = + | ||
307 | |||
308 | sieve_plugins = $sieve_plugins sieve_imapsieve | ||
309 | |||
310 | service lmtp { | ||
311 | vsz_limit = 1G | ||
312 | |||
313 | unix_listener /run/postfix/dovecot-lmtp { | ||
314 | mode = 0600 | ||
315 | user = postfix | ||
316 | group = postfix | ||
317 | } | ||
318 | } | ||
319 | |||
320 | namespace inbox { | ||
321 | separator = / | ||
322 | inbox = yes | ||
323 | prefix = | ||
324 | } | ||
325 | |||
326 | plugin { | ||
327 | quota = maildir | ||
328 | quota_rule = *:storage=1GB | ||
329 | quota_rule2 = Trash:storage=+10%% | ||
330 | quota_status_overquota = "552 5.2.2 Mailbox is full" | ||
331 | quota_status_success = DUNNO | ||
332 | quota_status_nouser = DUNNO | ||
333 | quota_grace = 10%% | ||
334 | } | ||
335 | |||
336 | protocol imap { | ||
337 | mail_max_userip_connections = 50 | ||
338 | mail_plugins = $mail_plugins imap_quota imap_sieve | ||
339 | } | ||
340 | |||
341 | service managesieve-login { | ||
342 | inet_listener sieve { | ||
343 | port = 4190 | ||
344 | ssl = yes | ||
345 | } | ||
346 | } | ||
347 | |||
348 | plugin { | ||
349 | sieve_redirect_envelope_from = orig_recipient | ||
350 | sieve_before = ${compileSieve "tag-junk.sieve" '' | ||
351 | require ["imap4flags"]; | ||
352 | |||
353 | if header :contains "X-Spam-Flag" "YES" { | ||
354 | addflag ["\\Junk"]; | ||
355 | } | ||
356 | ''} | ||
357 | } | ||
358 | ''; | ||
359 | }; | ||
360 | |||
158 | security.dhparams = { | 361 | security.dhparams = { |
159 | params = { | 362 | params = { |
160 | "postfix-512".bits = 512; | 363 | "postfix-512".bits = 512; |
@@ -166,6 +369,7 @@ with lib; | |||
166 | "bouncy.email" = {}; | 369 | "bouncy.email" = {}; |
167 | "mailin.bouncy.email" = {}; | 370 | "mailin.bouncy.email" = {}; |
168 | "mailsub.bouncy.email" = {}; | 371 | "mailsub.bouncy.email" = {}; |
372 | "imap.bouncy.email" = {}; | ||
169 | "surtr.yggdrasil.li" = {}; | 373 | "surtr.yggdrasil.li" = {}; |
170 | }; | 374 | }; |
171 | 375 | ||
@@ -178,5 +382,20 @@ with lib; | |||
178 | "mailsub.bouncy.email.full.pem:${config.security.acme.certs."mailsub.bouncy.email".directory}/full.pem" | 382 | "mailsub.bouncy.email.full.pem:${config.security.acme.certs."mailsub.bouncy.email".directory}/full.pem" |
179 | ]; | 383 | ]; |
180 | }; | 384 | }; |
385 | |||
386 | systemd.services.dovecot2 = { | ||
387 | serviceConfig = { | ||
388 | LoadCredential = [ | ||
389 | "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" | ||
390 | "surtr.yggdrasil.li.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/fullchain.pem" | ||
391 | "bouncy.email.key.pem:${config.security.acme.certs."bouncy.email".directory}/key.pem" | ||
392 | "bouncy.email.pem:${config.security.acme.certs."bouncy.email".directory}/fullchain.pem" | ||
393 | "imap.bouncy.email.key.pem:${config.security.acme.certs."imap.bouncy.email".directory}/key.pem" | ||
394 | "imap.bouncy.email.pem:${config.security.acme.certs."imap.bouncy.email".directory}/fullchain.pem" | ||
395 | ]; | ||
396 | |||
397 | StateDirectory = "mail"; | ||
398 | }; | ||
399 | }; | ||
181 | }; | 400 | }; |
182 | } | 401 | } |
diff --git a/hosts/surtr/postgresql.nix b/hosts/surtr/postgresql.nix index 7b3b8c74..d8f66fcc 100644 --- a/hosts/surtr/postgresql.nix +++ b/hosts/surtr/postgresql.nix | |||
@@ -30,17 +30,23 @@ in { | |||
30 | BEGIN; | 30 | BEGIN; |
31 | SELECT _v.register_patch('000-base', null, null); | 31 | SELECT _v.register_patch('000-base', null, null); |
32 | 32 | ||
33 | CREATE TABLE virtual_mailbox_mapping ( | 33 | CREATE TABLE mailbox ( |
34 | id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), | ||
35 | mailbox text NOT NULL CONSTRAINT mailbox_non_empty CHECK (mailbox <> '''), | ||
36 | quota_bytes bigint CONSTRAINT quota_bytes_positive CHECK (CASE WHEN quota_bytes IS NOT NULL THEN quota_bytes > 0 ELSE true), | ||
37 | quota_rule text GENERATED ALWAYS AS (CASE WHEN quota_bytes IS NULL THEN '*:ignore' ELSE '*:bytes=' || quota_bytes) STORED | ||
38 | ) | ||
39 | CREATE TABLE mailbox_mapping ( | ||
34 | id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), | 40 | id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), |
35 | local text CONSTRAINT local_non_empty CHECK (local IS DISTINCT FROM '''), | 41 | local text CONSTRAINT local_non_empty CHECK (local IS DISTINCT FROM '''), |
36 | domain text NOT NULL CONSTRAINT domain_non_empty CHECK (domain <> '''), | 42 | domain text NOT NULL CONSTRAINT domain_non_empty CHECK (domain <> '''), |
37 | mailbox text NOT NULL CONSTRAINT mailbox_non_empty CHECK (mailbox <> '''), | 43 | mailbox uuid REFERENCES virtual_mailbox(id) |
38 | CONSTRAINT local_domain_unique UNIQUE (local, domain) | 44 | CONSTRAINT local_domain_unique UNIQUE (local, domain) |
39 | ); | 45 | ); |
40 | CREATE UNIQUE INDEX domain_unique ON virtual_mailbox_mapping (domain) WHERE local IS NULL; | 46 | CREATE UNIQUE INDEX domain_unique ON virtual_mailbox_mapping (domain) WHERE local IS NULL; |
41 | 47 | ||
42 | CREATE VIEW virtual_mailbox_domain (domain) AS SELECT DISTINCT domain FROM virtual_mailbox_mapping; | 48 | CREATE VIEW virtual_mailbox_domain (domain) AS SELECT DISTINCT domain FROM virtual_mailbox_mapping; |
43 | CREATE VIEW virtual_mailbox (mailbox) AS SELECT DISTINCT mailbox FROM virtual_mailbox_mapping; | 49 | CREATE VIEW virtual_mailbox_mapping (mailbox, lookup) AS SELECT mailbox.mailbox as mailbox, (CASE WHEN local IS NULL THEN ''' ELSE local) || '@' || domain AS lookup FROM mailbox_mapping INNER JOIN mailbox on mailbox.id = mailbox_mapping.mailbox; |
44 | COMMIT; | 50 | COMMIT; |
45 | ''} | 51 | ''} |
46 | ''; | 52 | ''; |
diff --git a/hosts/surtr/ruleset.nft b/hosts/surtr/ruleset.nft index f5ad5769..b9f83487 100644 --- a/hosts/surtr/ruleset.nft +++ b/hosts/surtr/ruleset.nft | |||
@@ -80,6 +80,8 @@ table inet filter { | |||
80 | counter turn-rx {} | 80 | counter turn-rx {} |
81 | counter smtp-rx {} | 81 | counter smtp-rx {} |
82 | counter submissions-rx {} | 82 | counter submissions-rx {} |
83 | counter imap-rx {} | ||
84 | counter managesieve-rx {} | ||
83 | 85 | ||
84 | counter established-rx {} | 86 | counter established-rx {} |
85 | 87 | ||
@@ -105,6 +107,8 @@ table inet filter { | |||
105 | counter turn-tx {} | 107 | counter turn-tx {} |
106 | counter smtp-tx {} | 108 | counter smtp-tx {} |
107 | counter submissions-tx {} | 109 | counter submissions-tx {} |
110 | counter imap-tx {} | ||
111 | counter managesieve-tx {} | ||
108 | 112 | ||
109 | counter tx {} | 113 | counter tx {} |
110 | 114 | ||
@@ -170,8 +174,10 @@ table inet filter { | |||
170 | udp dport {3478, 5349} counter name stun-rx accept | 174 | udp dport {3478, 5349} counter name stun-rx accept |
171 | udp dport 49000-50000 counter name turn-rx accept | 175 | udp dport 49000-50000 counter name turn-rx accept |
172 | 176 | ||
173 | # tcp dport 25 counter name smtp-rx accept | 177 | tcp dport 25 counter name smtp-rx accept |
174 | tcp dport 465 counter name submissions-rx accept | 178 | tcp dport 465 counter name submissions-rx accept |
179 | tcp dport 993 counter name imaps-rx accept | ||
180 | tcp dport 4190 counter name managesieve-rx accept | ||
175 | 181 | ||
176 | ct state {established, related} counter name established-rx accept | 182 | ct state {established, related} counter name established-rx accept |
177 | 183 | ||
@@ -214,6 +220,8 @@ table inet filter { | |||
214 | 220 | ||
215 | tcp sport 25 counter name smtp-tx accept | 221 | tcp sport 25 counter name smtp-tx accept |
216 | tcp sport 465 counter name submissions-tx accept | 222 | tcp sport 465 counter name submissions-tx accept |
223 | tcp sport 993 counter name imaps-tx accept | ||
224 | tcp sport 4190 counter name managesieve-tx accept | ||
217 | 225 | ||
218 | 226 | ||
219 | counter name tx | 227 | counter name tx |