Files
cnx-network-clan/docs/src/mail.md
T

4.0 KiB

Mail

A single mail server, mx1, runs the Simple NixOS Mailserver stack (Postfix + Dovecot + Rspamd + OpenDKIM) for the cnx.email domain. All of it is declared in modules/mail.nix, imported by machines/mx1/configuration.nix.

Mailboxes are virtual (not system users): each address is a login account whose password is a four-word passphrase auto-generated by a clan vars generator (e.g. otter-lantern-cobalt-driftwood-42). The generator stores both the passphrase (handed to the user) and its sha-512 hash (consumed by SNM); plaintext never lands in the repo.

The mail cert is obtained by mx1 itself via ACME DNS-01 against ns1 (key acme_mx1), and carries mx1.cnx.email plus the mta-sts.cnx.email and mail.cnx.email SANs. See DNS → ACME DNS-01.

Add a mailbox

  1. Append the address to accounts in modules/mail.nix:
    accounts = [
      "postmaster@cnx.email"
      "alice@cnx.email"      # new
    ];
    
  2. Mint its generated password (user-run, YubiKey touch):
    clan vars generate mx1
    
  3. Redeploy: clan machines update mx1.
  4. Hand the passphrase to the user. The generator name replaces @-at- and .-:
    clan vars get mx1 mail-passwd-alice-at-cnx-email/passphrase
    

The DMARC report inbox (dmarc@cnx.email) is special: its password comes from the shared mail-dmarc-cred generator (not the per-machine set) so parsedmarc on control can read the same passphrase over the mesh.

Add a domain

Adding a second mail domain is more than a config line — it needs its own DNS records and DKIM key. Steps:

  1. Serve the zone (if not already): add it in modules/dns/domains.nix and drop a <domain>.zone file — see DNS.
  2. Declare the domain in modules/mail.nix:
    mailserver.domains = [ "cnx.email" "newdomain.tld" ];
    
    Add at least a postmaster@newdomain.tld account (RFC 5321 requires it) the same way as above.
  3. Publish the mail DNS records in the new zone, mirroring cnx.email's ; ---- Mail ---- block:
    • MX 10 mx1.cnx.email. — point at the existing MX; do not stand up a new hostname unless you really want a separate server.
    • SPF: @ TXT "v=spf1 mx -all".
    • DKIM: SNM generates a key per domain on first start under mailserver.dkimKeyDirectory (selector mail). After deploy, read the public key off mx1 and paste it as mail._domainkey TXT. The private keys are declared as clan state and backed up.
    • DMARC: _dmarc TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@cnx.email; ..." — reports can keep flowing to the existing dmarc@cnx.email inbox.
  4. DANE / MTA-STS are tied to mx1.cnx.email, the MX hostname — they do not need duplicating per sending-domain. A new domain that uses mx1.cnx.email as its MX inherits the existing TLSA and MTA-STS policy. Only add new TLSA/MTA-STS records if you introduce a new MX hostname.
  5. clan vars generate mx1 (if you added accounts), then clan machines update mx1.

DKIM, DANE, MTA-STS reminders

These three are easy to get subtly wrong; the live records for cnx.email and the exact commands to regenerate them are in modules/dns/zones/cnx.email.zone:

  • DKIM key rotation = regenerate under dkimKeyDirectory, then republish the mail._domainkey TXT.
  • DANE TLSA (_25._tcp.mx1 TLSA 3 1 1 …) is the SHA-256 of mx1's cert public key. It stays valid across renewals because lego runs with --reuse-key; only recompute it if the key changes. The openssl one-liner is in the zone file.
  • MTA-STS: bump the _mta-sts TXT id every time the policy in modules/mail.nix changes, or senders keep the cached policy.

DMARC reporting

mx1 delivers aggregate/forensic DMARC reports to dmarc@cnx.email; parsedmarc on control polls that mailbox over the mesh and feeds the results into Grafana. See Monitoring.