aboutsummaryrefslogtreecommitdiffstats

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=<PUBLIC_KEY_BASE64>"

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

# 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

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:

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

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

# 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

# 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

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:

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):

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:

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