From a87b579eb749e6f6460423c556522d98d4da3cc8 Mon Sep 17 00:00:00 2001 From: Berwn Date: Tue, 28 Jul 2026 17:06:07 +0700 Subject: [PATCH] docs: add mail runbook page (mailbox and domain add procedures) --- docs/src/mail.md | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/src/mail.md diff --git a/docs/src/mail.md b/docs/src/mail.md new file mode 100644 index 0000000..f1e8d86 --- /dev/null +++ b/docs/src/mail.md @@ -0,0 +1,90 @@ +# Mail + +A single mail server, `mx1`, runs the [Simple NixOS Mailserver](https://nixos-mailserver.readthedocs.io/) +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](./dns.md#acme-dns-01). + +## Add a mailbox + +1. Append the address to `accounts` in `modules/mail.nix`: + ```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 `.zone` file — see [DNS](./dns.md). +2. **Declare the domain** in `modules/mail.nix`: + ```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](./monitoring.md).