nixConfig/nixos/modules/mail.nix

239 lines
8.9 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
config,
lib,
pkgs,
...
}: let
inherit
(lib)
mkIf
mkEnableOption
mkOption
;
inherit (lib.types) listOf str;
cfg = config.services.mathebau-mail;
in {
options.services.mathebau-mail = {
enable = mkEnableOption "mathebau mail service";
domains = mkOption {
type = listOf (lib.types.submodule {
options = {
domain = mkOption {
type = str;
};
allowlistPass = mkOption {
type = str;
};
};
});
};
};
config = mkIf cfg.enable {
environment.systemPackages = [pkgs.alias-to-sieve];
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" = {
# Enabling sending from these domains privately blocked on https://github.com/stalwartlabs/mail-server/issues/618
bind = ["[::]:465"];
protocol = "smtp";
tls.implicit = true;
};
"imaptls" = {
bind = ["[::]:993"];
protocol = "imap";
tls.implicit = true;
};
"management" = {
bind = ["[::]:80"]; # This must also bind publically for ACME to work.
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 = {
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; # somehow this is needed here
};
remote."mailman" = {
address = "lobon.mathebau.de"; # must be created in DNS as a MX record
port = 25;
protocol = "smtp";
tls.implicit = false; # somehow this is needed here
};
# In order to accept mail that we only forward
# without having to generate an account.
# Invalid addresses are filtered by DFN beforehand.
session.rcpt = {
catch-all = true;
relay = [
{
"if" = "!is_empty(authenticated_as) || rcpt_domain == 'lists.mathebau.de'";
"then" = true;
}
{"else" = false;}
];
};
config.local-keys =
[
"store.*"
"directory.*"
"tracer.*"
"server.*"
"!server.blocked-ip.*"
"authentication.fallback-admin.*"
"cluster.node-id"
"storage.data"
"storage.blob"
"storage.lookup"
"storage.fts"
"storage.directory"
"lookup.default.hostname"
"certificate.*"
] # the default ones
++ ["sieve.trusted.scripts.*"]; #for marcos to be able to include our redirection script
sieve.trusted.scripts.redirects.contents = "%{file:/run/secrets/mailForwardSieve}%";
session.data.script = "'redirects'";
authentication.fallback-admin = {
user = "admin";
secret = "$argon2i$v=19$m=4096,t=3,p=1$d0hYOTkzclpzSmFTZUplWnhVeWE$I7q9uB19RWL0oZKaPlMPSlGfFp6FQ/vrx80FFKCsalg";
};
};
};
};
environment.persistence.${config.impermanence.name} = {
directories = [
"/var/lib/stalwart-mail"
];
files = ["/root/.ssh/known_hosts"]; # for the backup server bragi
};
# Update HRZ allowlist
# For account details see https://www-cgi.hrz.tu-darmstadt.de/mail/
# will stop working if no valid TUIDs are associated to our domain.
systemd.timers."mailAllowlist" = {
wantedBy = ["timers.target"];
timerConfig = {
OnBootSec = "5m"; # Run every 5 minutes
OnUnitActiveSec = "5m";
RandomizedDelaySec = "2m"; # prevent overload on regular intervals
Unit = "mailAllowlist.service";
};
};
# ${pkgs.gnugrep}/bin/grep -o -e "[A-Za-z0-9.!#\$%&'*+-/=?^_`{|}~]+@koma89.tu-darmstadt.de" /run/secrets/mailForwardSieve >> /tmp/addresses # This doesn't catch all RFC conform local parts. Improve if you need.
systemd.services."mailAllowlist" = {
description = "Allowlist update: Post the mail addresses to the HRZ allowllist";
script = let
scriptTemplate = {
domain,
allowlistPass,
}: ''
# Get the mail addresses' local-part
${pkgs.stalwart-mail}/bin/stalwart-cli --url http://localhost:80 -c $(cat /run/secrets/stalwartAdmin) account list | grep '@${domain}' | sed 's/| //' | sed 's/ |//' | tee /tmp/addresses
${pkgs.stalwart-mail}/bin/stalwart-cli --url http://localhost:80 -c $(cat /run/secrets/stalwartAdmin) list list | grep '@${domain}' | sed 's/| //' | sed 's/ |//' | tee -a /tmp/addresses
${pkgs.stalwart-mail}/bin/stalwart-cli --url http://localhost:80 -c $(cat /run/secrets/stalwartAdmin) group list | grep '@${domain}' | sed 's/| //' | sed 's/ |//' | tee -a /tmp/addresses
${pkgs.gnugrep}/bin/grep -o -e "[A-Za-z0-9.!#\$%&'*+-/=?^_{|}~]*@${domain}" /run/secrets/mailForwardSieve | tee -a /tmp/addresses # This doesn't catch all RFC conform local parts. Improve if you need.
# Post local-parts to HRZ
${pkgs.curl}/bin/curl https://www-cgi.hrz.tu-darmstadt.de/mail/whitelist-update.php -F emaildomain=${domain} -F password=$(cat ${allowlistPass}) -F emailliste=@/tmp/addresses -F meldungen=voll
# Cleanup
rm /tmp/addresses
'';
in
lib.strings.concatStringsSep "" (map scriptTemplate cfg.domains);
serviceConfig = {
Type = "oneshot";
User = "stalwart-mail";
NoNewPrivileges = true;
# See https://www.man7.org/linux/man-pages/man5/systemd.exec.5.html
PrivateTmp = true;
ProtectHome = true;
ReadOnlyPaths = "/";
ReadWritePaths = "/tmp";
InaccessiblePaths = "-/lost+found";
PrivateDevices = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
};
};
# 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";
};
};
}