1dd3aadb97
A mail.cnx.email CNAME (-> mx1.cnx.email) lets clients (Thunderbird etc.) use a friendly hostname for submission/IMAP. To avoid a TLS name mismatch the cert now carries mail.cnx.email as a SAN, so the acme_mx1 key is authorized to write _acme-challenge.mail too. The MX still points at mx1.cnx.email and --reuse-key keeps the DANE TLSA digest valid across the re-issue.
146 lines
5.6 KiB
Nix
146 lines
5.6 KiB
Nix
# Declarative mail stack for mx1 (Simple NixOS Mailserver: Postfix + Dovecot +
|
|
# Rspamd + OpenDKIM). Imported by machines/mx1 alongside the SNM flake module.
|
|
#
|
|
# Mailboxes are virtual (not system users): each address below is a login account
|
|
# whose password is auto-generated by a clan vars generator as a four-word
|
|
# passphrase with a trailing number (e.g. otter-lantern-cobalt-driftwood-42). The
|
|
# generator stores both the passphrase and its sha-512 hash. To add a mailbox:
|
|
# append the address to `accounts`, run `clan vars generate mx1`, redeploy mx1,
|
|
# then hand the passphrase to the user:
|
|
# clan vars get mx1 mail-passwd-<addr>/passphrase
|
|
# (addr with @ and . replaced by -at- and -, e.g. mail-passwd-postmaster-at-cnx-email)
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
hosts = import ./hosts.nix;
|
|
fqdn = "mx1.cnx.email";
|
|
mtaStsHost = "mta-sts.cnx.email";
|
|
# Client-facing alias (CNAME -> mx1) so Thunderbird etc. can use mail.cnx.email
|
|
# for submission/IMAP; added as a cert SAN so TLS validates against that name.
|
|
clientHost = "mail.cnx.email";
|
|
|
|
# MTA-STS policy served at https://mta-sts.cnx.email/.well-known/mta-sts.txt.
|
|
# enforce = a sending MTA that fetched this must use a valid, MX-matching TLS
|
|
# cert or refuse to deliver. Bump the _mta-sts TXT id (in the zone) whenever
|
|
# this changes.
|
|
mtaStsPolicy = pkgs.writeText "mta-sts.txt" ''
|
|
version: STSv1
|
|
mode: enforce
|
|
mx: ${fqdn}
|
|
max_age: 604800
|
|
'';
|
|
|
|
# The mailboxes mx1 serves. postmaster is required by RFC 5321.
|
|
accounts = [
|
|
"postmaster@cnx.email"
|
|
];
|
|
|
|
genName = addr: "mail-passwd-" + lib.replaceStrings [ "@" "." ] [ "-at-" "-" ] addr;
|
|
|
|
passwdGenerators = lib.listToAttrs (
|
|
map (addr: {
|
|
name = genName addr;
|
|
value = {
|
|
files."passphrase".secret = true; # retrievable to hand to the user
|
|
files."hash".secret = true; # consumed by SNM's hashedPasswordFile
|
|
runtimeInputs = [
|
|
pkgs.xkcdpass
|
|
pkgs.mkpasswd
|
|
];
|
|
script = ''
|
|
pass="$(xkcdpass --numwords=4 --delimiter=- --case=lower)-$((RANDOM % 90 + 10))"
|
|
printf '%s' "$pass" > "$out"/passphrase
|
|
printf '%s' "$pass" | mkpasswd -s -m sha-512 > "$out"/hash
|
|
'';
|
|
};
|
|
}) accounts
|
|
);
|
|
|
|
loginAccounts = lib.listToAttrs (
|
|
map (addr: {
|
|
name = addr;
|
|
value.hashedPasswordFile = config.clan.core.vars.generators.${genName addr}.files."hash".path;
|
|
}) accounts
|
|
);
|
|
in
|
|
{
|
|
imports = [ ./dns/acme-mx1-secret.nix ];
|
|
|
|
clan.core.vars.generators = passwdGenerators // {
|
|
# Render the shared acme_mx1 TSIG secret into a lego rfc2136 env file. lego
|
|
# (via security.acme below) uses it to write the _acme-challenge.mx1.cnx.email
|
|
# TXT record to ns1, which authorizes the acme_mx1 key for exactly that owner.
|
|
dns-acme-rfc2136 = {
|
|
files."rfc2136.env".secret = true; # root-owned; systemd reads it as root
|
|
dependencies = [ "dns-acme-mx1-secret" ];
|
|
script = ''
|
|
printf 'RFC2136_NAMESERVER=${hosts.ns1.ipv4}:53\nRFC2136_TSIG_ALGORITHM=hmac-sha256.\nRFC2136_TSIG_KEY=acme_mx1\nRFC2136_TSIG_SECRET=%s\n' \
|
|
"$(cat "$in"/dns-acme-mx1-secret/secret)" > "$out"/rfc2136.env
|
|
'';
|
|
};
|
|
};
|
|
|
|
mailserver = {
|
|
enable = true;
|
|
# Fresh install: declare the latest layout the nixos-25.11 branch ships (3),
|
|
# so SNM uses the current dovecot mail directory layout with nothing to migrate.
|
|
stateVersion = 3;
|
|
inherit fqdn;
|
|
domains = [ "cnx.email" ];
|
|
inherit loginAccounts;
|
|
|
|
# Consume a security.acme cert we obtain ourselves via DNS-01 (below); no
|
|
# web server and no inbound HTTP needed, so port 80 stays closed. Add the
|
|
# MTA-STS host as a SAN so the one cert also covers the policy endpoint.
|
|
certificateScheme = "acme";
|
|
certificateDomains = [
|
|
mtaStsHost
|
|
clientHost
|
|
];
|
|
|
|
dkimSelector = "mail";
|
|
};
|
|
|
|
security.acme = {
|
|
acceptTerms = true;
|
|
defaults.email = "postmaster@cnx.email";
|
|
certs.${fqdn} = {
|
|
dnsProvider = "rfc2136";
|
|
environmentFile = config.clan.core.vars.generators.dns-acme-rfc2136.files."rfc2136.env".path;
|
|
# ns1 is the only nameserver that accepts the acme_mx1 UPDATE; check
|
|
# propagation against it directly rather than a public resolver.
|
|
dnsResolver = "${hosts.ns1.ipv4}:53";
|
|
# Keep the private key fixed across renewals so the DANE TLSA "3 1 1"
|
|
# record (public-key digest, published in the zone) stays valid.
|
|
extraLegoRenewFlags = [ "--reuse-key" ];
|
|
};
|
|
};
|
|
|
|
# The mail cert is owned group=acme (SNM adds postfix/dovecot); Caddy serves the
|
|
# MTA-STS endpoint from the same cert, so it needs to read the key too.
|
|
users.users.caddy.extraGroups = [ "acme" ];
|
|
|
|
# MTA-STS policy endpoint, served by Caddy (same web server as control's docs).
|
|
# The explicit `tls cert key` points at the lego-issued mail cert (which carries
|
|
# mta-sts.cnx.email as a SAN) and disables Caddy's automatic ACME, so no extra
|
|
# issuance happens and the DANE TLSA key stays stable. Only :443 is opened.
|
|
services.caddy = {
|
|
enable = true;
|
|
virtualHosts.${mtaStsHost}.extraConfig = ''
|
|
tls /var/lib/acme/${fqdn}/cert.pem /var/lib/acme/${fqdn}/key.pem
|
|
root * ${pkgs.writeTextDir ".well-known/mta-sts.txt" (builtins.readFile mtaStsPolicy)}
|
|
file_server
|
|
'';
|
|
};
|
|
|
|
# DKIM private keys are generated on first start under this dir. They're
|
|
# regenerable (rotate + republish the TXT), but declaring the path as clan
|
|
# state lets a borg client back it up to avoid a needless DNS round-trip on
|
|
# restore. Wiring mx1 into the borgbackup instance is a separate step.
|
|
clan.core.state.mail-dkim.folders = [ config.mailserver.dkimKeyDirectory ];
|
|
}
|