diff options
| -rw-r--r-- | docs/MAIL-SERVER.md | 209 |
1 files changed, 175 insertions, 34 deletions
diff --git a/docs/MAIL-SERVER.md b/docs/MAIL-SERVER.md index c5a0d17..cbad53d 100644 --- a/docs/MAIL-SERVER.md +++ b/docs/MAIL-SERVER.md @@ -1,6 +1,7 @@ # Mail Server — Postfix send-only with SPF/DKIM/DMARC -> Target: meshbay.org (Ubuntu 26.04 LTS, OVH VPS 164.132.246.44) +> Target: meshbay.org (Ubuntu 26.04 LTS, OVH VPS 164.132.246.44 / +> 2001:41d0:20a:900::2389) > > Purpose: send confirmation codes to validate user email addresses at > registration. The hub sends mail; it does not receive it. OVH MX servers @@ -8,16 +9,18 @@ --- -## 1. Current DNS state (before setup) +## 1. DNS state -| Record | Value | Status | -|---|---|---| -| A | `164.132.246.44` | OK | -| MX | `1 mx1.mail.ovh.net` / `5 mx2` / `100 mx3` | OK — receiving stays OVH | -| SPF | `v=spf1 include:mx.ovh.com -all` | Must add VPS IP | -| DKIM | (none) | Must create | -| DMARC | (none) | Must create | -| PTR (rDNS) | `vps-7148538e.vps.ovh.net` | Must change to `meshbay.org` | +| Record | Value | +|---|---| +| A / AAAA | `164.132.246.44` / `2001:41d0:20a:900::2389` | +| MX | `1 mx1.mail.ovh.net` / `5 mx2` / `100 mx3` — inbound mail and redirections stay at OVH | +| SPF | `v=spf1 ip4:164.132.246.44 ip6:2001:41d0:20a:900::2389 include:mx.ovh.com -all` | +| DKIM | `meshbay._domainkey` — RSA 2048, `h=sha256` | +| DMARC | `v=DMARC1; p=quarantine; sp=quarantine; adkim=s; aspf=s; pct=100; rua=mailto:postmaster@meshbay.org` | +| PTR (rDNS) | `meshbay.org.` for both the IPv4 and the IPv6 address | + +DMARC is at the intermediate `quarantine` step; `reject` is the target (§7). ## 2. What must happen @@ -28,25 +31,28 @@ Set via the **OVH control panel** (not DNS zone): 1. OVH Manager → Bare Metal Cloud → VPS → IP 2. Click the gear icon next to `164.132.246.44` → Modify reverse DNS 3. Set to: `meshbay.org.` (trailing dot) +4. Same for `2001:41d0:20a:900::2389` — Postfix sends over IPv6 whenever the + receiver has an AAAA MX (Gmail does) Most receiving MTAs reject or score down mail from an IP whose PTR does not match the HELO/EHLO hostname. ### 2.2 DNS zone records (OVH DNS zone editor) -**Update SPF** — add the VPS IP alongside the existing OVH include: +**Update SPF** — add both VPS addresses alongside the existing OVH include: ``` -meshbay.org. TXT "v=spf1 ip4:164.132.246.44 include:mx.ovh.com -all" +meshbay.org. TXT "v=spf1 ip4:164.132.246.44 ip6:2001:41d0:20a:900::2389 include:mx.ovh.com -all" ``` -**Add DKIM** — after running the install script which generates the key: +**Add DKIM** — once the key is generated (§3.3): ``` meshbay._domainkey.meshbay.org. TXT "v=DKIM1; h=sha256; k=rsa; p=<PUBLIC_KEY_BASE64>" ``` -The script prints the exact record content. The selector is `meshbay`. +The `p=` value is in `/etc/opendkim/keys/meshbay.org/meshbay.txt`, written by +`opendkim-genkey`. The selector is `meshbay`. **Add DMARC** — start with `none` policy (monitoring), tighten to `quarantine` then `reject` after confirming deliverability: @@ -60,12 +66,13 @@ Target (after validation): _dmarc.meshbay.org. TXT "v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; rua=mailto:postmaster@meshbay.org" ``` -### 2.3 Server-side (automated by the install script) +### 2.3 Server-side | Component | Role | |---|---| | **Postfix** | MTA — sends mail directly to recipient MX servers | | **OpenDKIM** | Signs outgoing mail with the domain's DKIM private key | +| **systemd-resolved drop-in** | Lets the domain's MX resolve on a host named after the domain (§9.2) | Postfix is configured as **send-only** (no listening on port 25 from outside). @@ -73,27 +80,112 @@ Postfix is configured as **send-only** (no listening on port 25 from outside). ## 3. Installation -Run the provided script on the server: +### 3.1 Packages ```bash -# From the local machine: -scp QE/deploy/setup-mailserver.sh cbesson@meshbay.org:/tmp/ -ssh cbesson@meshbay.org 'sudo bash /tmp/setup-mailserver.sh' +# Ubuntu — answer "Internet Site", mail name meshbay.org +sudo apt install postfix opendkim opendkim-tools mailutils +# Fedora +sudo dnf install postfix opendkim opendkim-tools mailx +``` + +### 3.2 Postfix — `/etc/postfix/main.cf` + +``` +smtpd_banner = $myhostname ESMTP +biff = no +append_dot_mydomain = no + +smtp_tls_security_level = may +smtp_tls_CApath = /etc/ssl/certs +smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache +smtp_tls_loglevel = 1 + +myhostname = meshbay.org +myorigin = meshbay.org +mydestination = localhost.localdomain, localhost +mynetworks = 127.0.0.0/8 [::1]/128 + +inet_interfaces = loopback-only +inet_protocols = all + +relayhost = +smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination + +mailbox_size_limit = 0 +recipient_delimiter = + + +milter_protocol = 6 +milter_default_action = accept +smtpd_milters = local:opendkim/opendkim.sock +non_smtpd_milters = $smtpd_milters + +compatibility_level = 3.9 ``` -Or directly on the server: +`inet_interfaces = loopback-only` makes the server send-only; `meshbay.org` is +absent from `mydestination` so that mail to its OVH mailboxes and redirections +goes through the MX lookup (§9.1). + +### 3.3 DKIM key + +```bash +sudo mkdir -p /etc/opendkim/keys/meshbay.org +sudo opendkim-genkey -b 2048 -d meshbay.org -s meshbay -D /etc/opendkim/keys/meshbay.org/ +sudo chown -R opendkim:opendkim /etc/opendkim +sudo chmod 600 /etc/opendkim/keys/meshbay.org/meshbay.private +``` + +### 3.4 OpenDKIM + +`/etc/opendkim.conf`: + +``` +Syslog yes +SyslogSuccess yes +LogWhy yes +Canonicalization relaxed/simple +Mode s +SubDomains no +KeyTable /etc/opendkim/KeyTable +SigningTable refile:/etc/opendkim/SigningTable +InternalHosts /etc/opendkim/TrustedHosts +OversignHeaders From +Socket local:/var/spool/postfix/opendkim/opendkim.sock +PidFile /run/opendkim/opendkim.pid +UMask 007 +UserID opendkim +``` + +| File | Content | +|---|---| +| `/etc/opendkim/KeyTable` | `meshbay._domainkey.meshbay.org meshbay.org:meshbay:/etc/opendkim/keys/meshbay.org/meshbay.private` | +| `/etc/opendkim/SigningTable` | `*@meshbay.org meshbay._domainkey.meshbay.org` | +| `/etc/opendkim/TrustedHosts` | `127.0.0.1`, `::1`, `localhost`, `meshbay.org` — one per line | + +The socket lives inside the Postfix spool so that Postfix reaches it: ```bash -sudo bash setup-mailserver.sh +sudo mkdir -p /var/spool/postfix/opendkim +sudo chown opendkim:postfix /var/spool/postfix/opendkim +sudo chmod 750 /var/spool/postfix/opendkim +sudo usermod -aG opendkim postfix ``` -The script: -1. Installs Postfix + OpenDKIM -2. Configures Postfix as send-only (inet_interfaces = loopback-only) -3. Generates a 2048-bit RSA DKIM key with selector `meshbay` -4. Wires OpenDKIM into Postfix via milter -5. Prints the DKIM TXT record to add to DNS -6. Sends a test email (if an address is given as argument) +### 3.5 systemd-resolved + +Install the drop-in described in §9.2, then +`sudo systemctl daemon-reload && sudo systemctl restart systemd-resolved`. + +### 3.6 Start + +```bash +sudo systemctl enable --now opendkim postfix +sudo systemctl restart opendkim postfix +dig +short MX meshbay.org # must list the OVH MX (§9.2) +``` + +Then add the DNS records of §2.2 and set the PTR (§2.1). ## 4. Verification @@ -210,9 +302,58 @@ Once deliverability is confirmed (SPF pass, DKIM pass on test emails): ## 8. Fedora 44 notes -The install script auto-detects Fedora and uses `dnf` instead of `apt`. -OpenDKIM is in the `opendkim` package, same as Ubuntu. The main difference is -the Postfix service name (`postfix` on both) and that SELinux may need a policy -for the OpenDKIM socket. The script handles this with `setsebool -P -dkim_milter_enable on` if SELinux is enforcing, and falls back to a custom -module if the boolean does not exist. +Packages come from `dnf` (§3.1); OpenDKIM is in the `opendkim` package and the +Postfix service is `postfix`, as on Ubuntu. When SELinux is enforcing, Postfix +needs permission to connect to the OpenDKIM socket: +`sudo setsebool -P dkim_milter_enable on`. If that boolean does not exist, a +local module granting `postfix_smtpd_t` `connectto` on `opendkim_t` +`unix_stream_socket` does the same. + +## 9. Hostname equal to the mail domain + +The server's hostname is `meshbay.org`, which is also the domain whose mailboxes +and redirections live at OVH. Two settings keep mail addressed to `@meshbay.org` +(for example `devel@meshbay.org`, an OVH redirection) going to OVH's MX instead +of being handled on the server. + +### 9.1 `mydestination` does not list the domain + +``` +mydestination = localhost.localdomain, localhost +``` + +Postfix delivers locally every domain listed in `mydestination`, without an MX +lookup. With `meshbay.org` in the list — which is also what the default +`$myhostname` expands to — a message to `devel@meshbay.org` is refused with +`550 5.1.1 User unknown in local recipient table`, or bounced by `postfix/local` +into the local mailbox of the sender, and never reaches OVH. + +Consequence: mail for `root` and `cbesson` (cron output, bounces) is qualified +with `myorigin = meshbay.org` and goes to OVH too. It is delivered only if the +address exists there as a mailbox or a redirection. + +### 9.2 systemd-resolved does not answer for the hostname + +``` +# /etc/systemd/system/systemd-resolved.service.d/no-synthesize-hostname.conf +[Service] +Environment=SYSTEMD_RESOLVED_SYNTHESIZE_HOSTNAME=0 +``` + +systemd-resolved answers queries for the local hostname itself: A/AAAA with the +machine's addresses, and an authoritative empty answer (`aa`, `NOERROR`, no +records) for every other type, MX included. Postfix then falls back to the A +record and connects to its own port 25, which is loopback-only: +`status=deferred (connect to meshbay.org[164.132.246.44]:25: Connection refused)`. +The drop-in turns the synthesis off, so the query goes to the upstream resolver +and returns OVH's MX. Hostname resolution for `sudo` and other local tools is +unaffected: it goes through the NSS `myhostname` module, not through resolved. + +After `systemctl daemon-reload && systemctl restart systemd-resolved`: + +```bash +dig +short MX meshbay.org # must list mx1/mx2/mx3.mail.ovh.net +echo test | mailx -s test devel@meshbay.org +sudo journalctl -u postfix@- --since "1 min ago" | grep status= +# Expected: relay=mx1.mail.ovh.net[...]:25, status=sent +``` |