From d7b8f935cd03748f016e7e08e8397843dc04d049 Mon Sep 17 00:00:00 2001 From: Gonne Date: Tue, 25 Feb 2025 15:32:36 +0100 Subject: [PATCH] Add basic mailserver configuration --- nixos/machines/kaalut/configuration.nix | 7 + nixos/modules/mail.nix | 171 ++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 nixos/modules/mail.nix diff --git a/nixos/machines/kaalut/configuration.nix b/nixos/machines/kaalut/configuration.nix index 115091d..ac9a08b 100644 --- a/nixos/machines/kaalut/configuration.nix +++ b/nixos/machines/kaalut/configuration.nix @@ -1,12 +1,19 @@ {config, ...}: { imports = [ ./hardware-configuration.nix + ../../modules/mail.nix ../../roles ../../roles/vm.nix ../../modules/vmNetwork.nix ]; # System configuration here + services.mathebau-mail = { + enable = true; + # see passwd on azathoth for plaintext or machine secret in encoded format for HTTP Basic AUTH + stalwartAdminHash = "$argon2i$v=19$m=4096,t=3,p=1$d0hYOTkzclpzSmFTZUplWnhVeWE$I7q9uB19RWL0oZKaPlMPSlGfFp6FQ/vrx80FFKCsalg"; + }; + networking.hostName = "kaalut"; vmNetwork.ipv4 = "192.168.0.17"; system.stateVersion = "24.05"; diff --git a/nixos/modules/mail.nix b/nixos/modules/mail.nix new file mode 100644 index 0000000..d4fa331 --- /dev/null +++ b/nixos/modules/mail.nix @@ -0,0 +1,171 @@ +/* +* Using the web admin interface: Set your SSH to do portforwarding of some local port to port 80 of the VM and +* and use your personal admin account or create one using the fallback admin password. +* Create users with mail boxes: Go to the admin interface and create them. +* Stalwart mailserver docs can be found at https://stalw.art/docs +* DNS-Records: Collect the right DNS entries from the management interface and copy them to the DNS hoster. Caution: +* Not all entries are applicable since we relay via HRZ. +*/ +{ + config, + lib, + ... +}: let + inherit + (lib) + mkIf + mkEnableOption + mkOption + ; + inherit (lib.types) listOf strMatching str path; + cfg = config.services.mathebau-mail; +in { + options.services.mathebau-mail = { + enable = mkEnableOption "mathebau mail service"; + stalwartAdminHash = mkOption { + type = str; + description = "String containing the hashed fallback admin password"; + }; + }; + + config = mkIf cfg.enable { + services = { + stalwart-mail = { + enable = true; + openFirewall = true; + settings = { + server = { + lookup.default.hostname = "fb04184.mathematik.tu-darmstadt.de"; # Because the DNS PTR of 130.83.2.184 is this and this should be used in SMTP EHLO. + listener = { + "smtp" = { + bind = ["[::]:25"]; + protocol = "smtp"; + }; + "submissions" = { + bind = ["[::]:465"]; + protocol = "smtp"; + tls.implicit = true; + }; + "imaptls" = { + bind = ["[::]:993"]; + protocol = "imap"; + tls.implicit = true; + }; + "management" = { + # Cthulhu forwards requests for http://fb04184.mathematik.tu-darmstadt.de/.well-known/acme-challenge/ http://imap.mathebau.de/.well-known/acme-challenge/ and http://smtp.mathebau.de/.well-known/acme-challenge/ + # for TLS certificate challenge validation + # whereas the rest of the management interface is not available publically. + # It can be reached via SSH and portforwarding. + bind = ["[::]:80"]; + protocol = "http"; + }; + }; + }; + acme.letsencrypt = { + directory = "https://acme-v02.api.letsencrypt.org/directory"; # This setting is necessary for this block to be activated + challenge = "http-01"; + contact = ["root@mathebau.de"]; + domains = ["fb04184.mathematik.tu-darmstadt.de" "imap.mathebau.de" "smtp.mathebau.de"]; + default = true; + }; + spam.header.is-spam = "Dummyheader"; # disable moving to spam which would conflict with forwarding + auth = { + # TODO check if HRZ conforms to these standards and we can validate them strictly + dkim.verify = "relaxed"; + arc.verify = "relaxed"; + dmarc.verify = "relaxed"; + iprev.verify = "relaxed"; + spf.verify.ehlo = "relaxed"; + spf.verify.mail-from = "relaxed"; + }; + + # Forward outgoing mail to HRZ or mail VMs. + # see https://stalw.art/docs/smtp/outbound/routing/ relay host example + queue.outbound = { + next-hop = [ + { + "if" = "rcpt_domain = 'lists.mathebau.de'"; + "then" = "'mailman'"; + } + { + "if" = "is_local_domain('', rcpt_domain)"; + "then" = "'local'"; + } + {"else" = "'hrz'";} + ]; + tls = { + # we only talk to HRZ and our own VMs anyway + mta-sts = "disable"; + dane = "disable"; + starttls = "optional"; # e.g. Lobon does not offer starttls + }; + }; + remote."hrz" = { + address = "mailout.hrz.tu-darmstadt.de"; + port = 25; + protocol = "smtp"; + tls.implicit = false; # Don't assume TLS on this port but use STARTTLS + }; + remote."mailman" = { + address = "lobon.mathebau.de"; # must be created in DNS as a MX record because this field does not accept ip addresses. + port = 25; + protocol = "smtp"; + tls.implicit = false; # Don't assume TLS on this port but use STARTTLS + }; + + session.rcpt = { + # In order to accept mail that we only forward + # without having to generate an account. + # Invalid addresses are filtered by DFN beforehand. + catch-all = true; + relay = [ + { + "if" = "!is_empty(authenticated_as) || rcpt_domain == 'lists.mathebau.de' || starts_with(remote_ip, '192.168.0.')"; #TODO restrict trust by IP + "then" = true; + } + {"else" = false;} + ]; + }; + + authentication.fallback-admin = { + user = "admin"; + # see passwd on azathoth for plaintext or machine secret in encoded format for HTTP Basic AUTH + secret = cfg.stalwartAdminHash; + }; + store = { + # structured data in SQLite, blobs on filesystem + db.type = "sqlite"; + db.path = "/var/lib/stalwart-mail/data/index.sqlite3"; + fs.type = "fs"; + fs.path = "/var/lib/stalwart-mail/data/blobs"; + }; + }; + }; + }; + environment.persistence.${config.impermanence.name} = { + directories = [ + "/var/lib/stalwart-mail" + ]; + files = ["/root/.ssh/known_hosts"]; # for the backup server bragi + }; + + # Backups + services.borgbackup.jobs.mail = { + paths = [ + "/var/lib/stalwart-mail/data" + ]; + encryption.mode = "none"; # Otherwise the key is next to the backup or we have human interaction. + environment = { + BORG_RSH = "ssh -i /run/secrets/backupKey"; + # “Borg ensures that backups are not created on random drives that ‘just happen’ to contain a Borg repository.” + # https://borgbackup.readthedocs.io/en/stable/deployment/automated-local.html + # We don't want this in order to not need to persist borg cache and simplify new deployments. + BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK = "yes"; + }; + repo = "borg@192.168.1.11:kaluut"; # TODO for https://gitea.mathebau.de/Fachschaft/nixConfig/issues/33 + startAt = "daily"; + user = "root"; + group = "root"; + }; + }; +}