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.netetc.) 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):
- OVH Manager → Bare Metal Cloud → VPS → IP
- Click the gear icon next to
164.132.246.44→ Modify reverse DNS - Set to:
meshbay.org.(trailing dot) - 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 showip4: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-onlymeans it does not accept connections from outside. No inbound port 25 in UFW. - No relay.
mynetworksis 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 byopendkim:opendkim, mode0600.
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):
p=none→p=quarantine— wait 2 weeks, checkruareportsp=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