diff options
| author | Gregor Kleen <gkleen@yggdrasil.li> | 2022-05-05 19:13:41 +0200 |
|---|---|---|
| committer | Gregor Kleen <gkleen@yggdrasil.li> | 2022-05-05 19:13:41 +0200 |
| commit | aefbe2d5a0cd10daa555433b14230ede07225372 (patch) | |
| tree | 4a6d082ec4a973f8e2be7d645a8a16948fdf05d0 | |
| parent | 845d0340201db81e6c605537ef27ed13f946d892 (diff) | |
| download | nixos-aefbe2d5a0cd10daa555433b14230ede07225372.tar nixos-aefbe2d5a0cd10daa555433b14230ede07225372.tar.gz nixos-aefbe2d5a0cd10daa555433b14230ede07225372.tar.bz2 nixos-aefbe2d5a0cd10daa555433b14230ede07225372.tar.xz nixos-aefbe2d5a0cd10daa555433b14230ede07225372.zip | |
surtr: ...
| -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 |
