# Mail Server — Postfix send-only with SPF/DKIM/DMARC > Target: meshbay.org (Ubuntu 26.04 LTS, OVH VPS 164.132.246.44) > > Purpose: send confirmation codes to validate user email addresses at > registration. The hub sends mail; it does not receive it. OVH MX servers > (`mx1.mail.ovh.net` etc.) continue handling inbound mail for the domain. --- ## 1. Current DNS state (before setup) | 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` | ## 2. What must happen ### 2.1 Reverse DNS (PTR) 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) 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: ``` meshbay.org. TXT "v=spf1 ip4:164.132.246.44 include:mx.ovh.com -all" ``` **Add DKIM** — after running the install script which generates the key: ``` meshbay._domainkey.meshbay.org. TXT "v=DKIM1; h=sha256; k=rsa; p=" ``` The script prints the exact record content. The selector is `meshbay`. **Add DMARC** — start with `none` policy (monitoring), tighten to `quarantine` then `reject` after confirming deliverability: ``` _dmarc.meshbay.org. TXT "v=DMARC1; p=none; sp=none; adkim=s; aspf=s; rua=mailto:postmaster@meshbay.org" ``` 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) | Component | Role | |---|---| | **Postfix** | MTA — sends mail directly to recipient MX servers | | **OpenDKIM** | Signs outgoing mail with the domain's DKIM private key | Postfix is configured as **send-only** (no listening on port 25 from outside). --- ## 3. Installation Run the provided script on the server: ```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' ``` Or directly on the server: ```bash sudo bash setup-mailserver.sh ``` 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) ## 4. Verification ### 4.1 Local checks ```bash # Postfix is running and only on loopback sudo ss -tlnp | grep :25 # Expected: 127.0.0.1:25, [::1]:25 only # OpenDKIM is running sudo systemctl status opendkim # Send a test echo "MeshBay mail test" | mail -s "Test from meshbay.org" your@email.com ``` ### 4.2 Check DKIM signing ```bash # Examine the Postfix log for DKIM signing confirmation sudo journalctl -u postfix@- --since "5 minutes ago" | grep -i dkim ``` ### 4.3 External validation After DNS propagation (up to 24h, usually 1-2h on OVH): - **SPF:** `dig TXT meshbay.org` — must show `ip4:164.132.246.44` - **DKIM:** `dig TXT meshbay._domainkey.meshbay.org` — must return the public key - **DMARC:** `dig TXT _dmarc.meshbay.org` — must return the policy - **Full test:** send an email to `check-auth@verifier.port25.com` — the auto-reply shows SPF/DKIM/DMARC pass/fail for each - **Alternative:** https://www.mail-tester.com — send to the address shown, get a score out of 10 ### 4.4 PTR check ```bash dig -x 164.132.246.44 +short # Expected: meshbay.org. ``` ## 5. Integration with MeshBay hub The hub sends email via `localhost:25` (Postfix). No authentication needed — Postfix listens only on loopback. Python code uses `smtplib`: ```python import smtplib from email.message import EmailMessage def send_confirmation(to: str, code: str) -> None: msg = EmailMessage() msg["From"] = "noreply@meshbay.org" msg["To"] = to msg["Subject"] = "MeshBay — Confirm your email" msg.set_content( f"Your confirmation code is: {code}\n\n" "This code expires in 30 minutes.\n" "If you did not create a MeshBay account, ignore this email.\n" ) with smtplib.SMTP("localhost", 25) as s: s.send_message(msg) ``` The `From` address must be `@meshbay.org` — it must match SPF and DKIM signing domain, or the message will fail authentication at the receiver. ## 6. Security considerations - **Postfix is send-only.** `inet_interfaces = loopback-only` means it does not accept connections from outside. No inbound port 25 in UFW. - **No relay.** `mynetworks` is loopback only. The server cannot be used as an open relay. - **Rate limiting.** Not configured at the Postfix level (low volume, confirmation codes only). Rate limiting should be done at the application level — the hub should enforce per-IP and per-account rate limits on the confirmation endpoint. - **DKIM private key.** Stored at `/etc/opendkim/keys/meshbay.org/meshbay.private`, owned by `opendkim:opendkim`, mode `0600`. ## 7. Maintenance ### Rotate DKIM key If the key is compromised or as a routine rotation (yearly is common): ```bash sudo opendkim-genkey -b 2048 -d meshbay.org -s meshbay-2025 -D /etc/opendkim/keys/meshbay.org/ # Update /etc/opendkim/KeyTable with the new selector # Add the new DKIM DNS record (new selector) # Keep the old record for 48h so in-flight mail still verifies # Remove the old record sudo systemctl restart opendkim postfix ``` ### Tighten DMARC **Why this matters.** `p=none` tells receiving servers: "if a mail fails SPF+DKIM, deliver it anyway — just report it to me." It protects nobody. An attacker can send a phishing email `From: noreply@meshbay.org` from their own server and it will land in the recipient's inbox normally. `p=reject` tells receivers to **refuse** such mail outright — a fake MeshBay confirmation email with a link to a credential-harvesting site never reaches the user. This is the defense against someone impersonating the domain to steal user passphrases. The gradual rollout exists only to verify that legitimate mail is not accidentally blocked before locking the policy down. Once deliverability is confirmed (SPF pass, DKIM pass on test emails): 1. `p=none` → `p=quarantine` — wait 2 weeks, check `rua` reports 2. `p=quarantine` → `p=reject` — the final target ## 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.