48bf7fb250
web01 terminates TLS for grafana.cnx.network and proxies to Grafana on control over the mesh. Caddy serves a *.cnx.network wildcard cert obtained via ACME DNS-01, using a dedicated acme_web01 TSIG key scoped on ns1 to _acme-challenge on the cnx.network zone only. Ports 80/443 are the only public exposure (80 just redirects); admin and the backend ride ZeroTier. Also reload Caddy on cert renewal for both web01 and mx1, since both reference the cert via explicit tls file paths and would otherwise keep serving a stale cert after a silent renewal.
150 lines
5.9 KiB
Nix
150 lines
5.9 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" ];
|
|
# Caddy serves the MTA-STS endpoint from explicit cert file paths, so it
|
|
# won't notice a renewal on its own — reload it whenever the cert changes.
|
|
# (Merges with the postfix/dovecot reloads SNM wires up for this cert.)
|
|
reloadServices = [ "caddy.service" ];
|
|
};
|
|
};
|
|
|
|
# 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 ];
|
|
}
|