# Mail Server — Postfix send-only with SPF/DKIM/DMARC > 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 > (`mx1.mail.ovh.net` etc.) continue handling inbound mail for the domain. --- ## 1. DNS state | 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 ### 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) 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 both VPS addresses alongside the existing OVH include: ``` meshbay.org. TXT "v=spf1 ip4:164.132.246.44 ip6:2001:41d0:20a:900::2389 include:mx.ovh.com -all" ``` **Add DKIM** — once the key is generated (§3.3): ``` meshbay._domainkey.meshbay.org. TXT "v=DKIM1; h=sha256; k=rsa; p=" ``` 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: ``` _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 | 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). --- ## 3. Installation ### 3.1 Packages ```bash # 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 ``` `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 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 ``` ### 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 ### 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 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 ```