diff --git a/secrets/secrets.nix b/secrets/secrets.nix index 32c3e24..295e53f 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -41,4 +41,6 @@ in "etna/vmauthEnv.age".publicKeys = main ++ [ etna ]; "etna/upsdUserPass.age".publicKeys = main ++ [ etna ]; "etna/cobaltTokens.age".publicKeys = main ++ [ etna ]; + + "vesuvio/maddyEnv.age".publicKeys = main ++ [ vesuvio ]; } diff --git a/secrets/vesuvio/maddyEnv.age b/secrets/vesuvio/maddyEnv.age new file mode 100644 index 0000000..b892d73 --- /dev/null +++ b/secrets/vesuvio/maddyEnv.age @@ -0,0 +1,14 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3cUNLWjYzZnV2eHFoT3cr +ZjVnd1ZiTVN6UXNGMmZvOVhoVW9VUFUxc2xRCkFtWjVPWGJRaDliWWZJbHZGU3k0 +TmVFZWdKcUpDeVNRcHk2ZXkvNENrcGsKLT4gWDI1NTE5IFgyenhNdWZkcnpPa2pW +ekN5bXRWVDdIUk41c01uNHJFMVlkR1lBenM1QmMKZnBOeERqMWxHU25qVklWYjlW +WElLcHVkMDlyRk5iSUd0NnVoeGpEZFZsVQotPiBYMjU1MTkgbCtsUHpjWUQrZFdL +ckZKN0ozNlBrUmprV0drbElDMGNSaEdnU0swV1JsSQpGVVBHQVgwd2tiaC9wOXNH +dXN6a1I4U0IwdnR1Mmh1OVo2Z2Nka2tBTXBnCi0+IFgyNTUxOSBtQWdSVUJBRzhz +dHZhYVMyUUZPUG1sbm1Nbk1Tb2RpbURnQVI0WUFzUlNFCnJ0ekpvdjBxaXRyR2pl +amE1VFl0SkxCUEF6SzhCN2JRcWY2OWpMWkFsNjAKLS0tIFNpT21weEQwUjQ3VzVj +ZjVyaUZHOVRYU0lrYlVORDROM2tJbGlJbTdxYjQKsk04W9FOBWj2it7o+ecEM72l +ezacJhtUWObiYe5PiMAaumhINJVr8GN7HYRgzTAAIlsn05aTT5WkYgwJQ9XRJphb +cSx24vSfrZ5BGoizwg== +-----END AGE ENCRYPTED FILE----- diff --git a/systems/etna/postgresql.nix b/systems/etna/postgresql.nix index cb32d4b..7f1f1e2 100644 --- a/systems/etna/postgresql.nix +++ b/systems/etna/postgresql.nix @@ -7,6 +7,14 @@ settings.port = 5432; enableTCPIP = true; + + ensureDatabases = [ + "maddy" + ]; + + authentication = '' + host maddy maddy vesuvio.fossa-macaroni.ts.net scram-sha-256 + ''; }; postgresqlBackup = { diff --git a/systems/vesuvio/default.nix b/systems/vesuvio/default.nix index 3ebeff1..6c49190 100644 --- a/systems/vesuvio/default.nix +++ b/systems/vesuvio/default.nix @@ -4,6 +4,7 @@ ./certificates.nix ./frp.nix ./hetzner.nix + ./mail ./nginx.nix ]; diff --git a/systems/vesuvio/mail/default.nix b/systems/vesuvio/mail/default.nix new file mode 100644 index 0000000..d9dc0ef --- /dev/null +++ b/systems/vesuvio/mail/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./maddy.nix + ./mta-sts.nix + ]; +} diff --git a/systems/vesuvio/mail/maddy.nix b/systems/vesuvio/mail/maddy.nix new file mode 100644 index 0000000..ef70405 --- /dev/null +++ b/systems/vesuvio/mail/maddy.nix @@ -0,0 +1,203 @@ +{ config, _utils, ... }: +let + hostname = "mx1.uku3lig.net"; + certLocation = config.security.acme.certs.${hostname}.directory; + + env = _utils.setupSingleSecret config "maddyEnv" { }; +in +{ + imports = [ env.generate ]; + + security.acme.certs.${hostname} = { + group = config.services.maddy.group; + extraLegoRenewFlags = [ "--reuse-key" ]; # soopyc said its more secure + }; + + services.maddy = { + enable = true; + inherit hostname; + primaryDomain = "uku3lig.net"; + localDomains = [ + "$(primary_domain)" + "uku.moe" + ]; + + tls = { + loader = "file"; + certificates = [ + { + certPath = "${certLocation}/fullchain.pem"; + keyPath = "${certLocation}/key.pem"; + } + ]; + }; + + config = '' + ## common stuff + + auth.pass_table local_authdb { + table sql_table { + driver postgres + dsn "host=etna password={env:POSTGRES_PASSWORD} dbname=maddy sslmode=disable" + table_name passwords + } + } + + storage.imapsql local_mailboxes { + driver postgres + dsn "host=etna password={env:POSTGRES_PASSWORD} dbname=maddy sslmode=disable" + # TODO: imap_filter https://maddy.email/reference/storage/imap-filters/ + } + + # chain of steps applied to recipients + # each step is a lookup table + table.chain local_rewrites { + # this removes the +suffix part from the address + optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3" + optional_step static { + entry postmaster postmaster@$(primary_domain) + } + } + + ## message reception + + msgpipeline local_routing { + # TODO: checks (rspamd) + + modify { + replace_rcpt &local_rewrites + } + + # catch-all setup inspired by https://github.com/foxcpp/maddy/issues/243#issuecomment-1406567636 + # we don't have a destination_in clause because there is only one imap account + destination $(local_domains) { + modify { + replace_rcpt regexp ".*" "hi@uku.moe" + } + + deliver_to &local_mailboxes + } + + default_destination { + reject 550 5.1.1 "User doesn't exist" + } + } + + smtp tcp://0.0.0.0:25 { + limits { + # Up to 20 msgs/sec across max. 10 SMTP connections. + all rate 20 1s + all concurrency 10 + } + + dmarc yes + check { + require_mx_record + dkim + spf + } + + source $(local_domains) { + reject 501 5.1.8 "Use internal submission port for outgoing SMTP" + } + + default_source { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + reject 550 5.1.1 "User doesn't exist" + } + } + } + + ## message sending + + target.remote outbound_delivery { + limits { + # Up to 20 msgs/sec across max. 10 SMTP connections for each recipient domain. + destination rate 20 1s + destination concurrency 10 + } + + mx_auth { + dane + mtasts + local_policy { + min_tls_level encrypted + min_mx_level none + } + } + } + + target.queue remote_queue { + target &outbound_delivery + + # sender domain for DSNs (delivery status notifications) + autogenerated_msg_domain $(primary_domain) + + # pipeline to know where to send DSNs + bounce { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + reject 550 5.0.0 "Refusing to send DSNs to non-local addresses" + } + } + } + + submission tls://0.0.0.0:465 tcp://0.0.0.0:587 { + limits { + # Up to 50 msgs/sec across any amount of SMTP connections. + all rate 50 1s + } + + auth &local_authdb + + source $(local_domains) { + # make sure the sender is allowed to send from this server + # local_rewrites allows us to use aliases as sender + check { + authorize_sender { + prepare_email &local_rewrites + user_to_email identity + } + } + + # just loop back if we are sending an email to ourselves + destination postmaster $(local_domains) { + deliver_to &local_routing + } + + default_destination { + modify { + dkim $(primary_domain) $(local_domains) default + } + + deliver_to &remote_queue + } + } + + default_source { + reject 501 5.1.8 "Non-local sender domain" + } + } + + ## IMAP + imap tls://0.0.0.0:993 { + auth &local_authdb + storage &local_mailboxes + } + ''; + }; + + systemd.services.maddy.serviceConfig.EnvironmentFile = env.path; + + networking.firewall.allowedTCPPorts = [ + 25 # smtp + 465 # submissions + 587 # submission (starttls) + 993 # imaps + ]; +} diff --git a/systems/vesuvio/mail/mta-sts.nix b/systems/vesuvio/mail/mta-sts.nix new file mode 100644 index 0000000..c872e6d --- /dev/null +++ b/systems/vesuvio/mail/mta-sts.nix @@ -0,0 +1,18 @@ +{ _utils, ... }: +{ + services.nginx.virtualHosts."mta-sts.uku3lig.net" = { + forceSSL = true; + enableACME = true; + serverAliases = [ "mta-sts.uku.moe" ]; + + locations."/.well-known/" = _utils.mkNginxFile { + filename = "mta-sts.txt"; + content = '' + version: STSv1 + mode: enforce + mx: mx1.uku3lig.net + max_age: 604800 + ''; + }; + }; +}