Audit Your Own Mail Server for Open Relay and Open Proxy Before Spammers Do
Open relay isn't a 1990s problem sendmail solved. In 2026 it hides in wide mynetworks, unrestricted SASL users, and sidecar containers that bypass Postfix entirely. Here's how to probe your own IP the way a spam bot does, then close the gaps from the inside out.
EvilMail TeamJuly 24, 202610 min read
Every mail admin has said some version of it: "open relay is a solved problem, Postfix ships secure by default, I'm fine." Then their /24 lands on the Spamhaus SBL, Gmail starts returning 550 5.7.1, and a week of legitimate mail bounces while they discover that a Docker container nobody remembered was quietly relaying to yahoo.co.jp all night.
The relay you have to worry about in 2026 is almost never the classic permit-everything sendmail box. It's the misconfigured submission port, the mynetworks that trusts a whole hosting subnet, the one mailbox with a weak password and no rate limit, and the sidecar service that never touched Postfix at all. This is a self-pentest playbook: probe your IP from the outside the way an attacker does, then close the gaps from the inside. Reputation takes an hour to lose and a month to earn back — act like it.
Two failure modes, and why "I use defaults" isn't a defense
There are two distinct things you're auditing for, and they fail differently.
An open relay accepts mail from a foreign sender to a foreign recipient without authentication — you are neither the sender's nor the recipient's mail system, yet you carry the message. An open proxy
Auditing Your Mail Server for Open Relay & Open Proxy (2026 Playbook) — EvilMail Blog
is broader and nastier: any service on the box that lets an attacker tunnel a connection through your IP — a Squid with
CONNECT
allowed, a SOCKS listener, an app that "forwards" mail, a compromised WordPress calling PHP
mail()
. The proxy doesn't have to be your MTA, which is exactly why "my Postfix is clean" boxes still get listed.
The dangerous modern cases:
Port 25 relaying because `mynetworks` is too wide.mynetworks_style = subnet on a hosting VLAN silently trusts every neighbor on the same subnet. A shared Docker bridge like 172.16.0.0/12 in the trusted list means any container — including a compromised one — relays freely.
Authenticated relay with no limits. Your relay policy is airtight, but permit_sasl_authenticated has no rate or recipient cap. One phished mailbox becomes a spam cannon that sends 40,000 messages before you wake up.
A second, unhardened service. Dev SMTP, mailpit, an exposed proxy, a cron script — anything that can originate SMTP on the public IP outside Postfix's restriction engine.
The stakes are deliverability, not just tidiness. One hour as an open relay typically means weeks on the CBL/Spamhaus XBL, SNDS turning red, and 5xx rejects from Gmail and Microsoft. Delisting is fast; rebuilding sender reputation after your IP pumped spam is the part that runs for weeks.
Attack the box from the outside first
Testing from localhost proves nothing — the loopback is almost always trusted. You need to hit your public IP from an IP that is not in your mynetworks. Spin up a $4 VPS in a different ASN and probe from there. That's the exact vantage point a spam bot has.
The fastest clean test is swaks. You're checking whether the server accepts a foreign-to-foreign transaction with no auth:
bash
# Foreign sender -> foreign recipient, no auth. This is THE open-relay test.
swaks --server mail.example.com:25 \
--from [email protected] \
--to [email protected] \
--quit-after RCPT
# A hardened server rejects at RCPT TO:
# <** 554 5.7.1 <[email protected]>: Relay access denied
The line that matters is the response to RCPT TO. 554 5.7.1 Relay access denied (the message behind reject_unauth_destination) is what you want. A 250 Ok there means you are an open relay — stop reading and pull the box off the internet.
Do the same by hand so you can see every code, and run the legacy tricks that still catch lazy configs — the percent hack and source routing, where an attacker disguises a foreign recipient as local:
For the encrypted ports, wrap the same probe in TLS. Submission and smtps should demand AUTH before they'll accept any recipient:
bash
openssl s_client -connect mail.example.com:587 -starttls smtp
# then: MAIL FROM / RCPT TO to a foreign domain -> expect 530 5.7.1 Authentication required
Online testers (MXToolbox's open relay check, mailradar) are fine for a quick sanity pass, but know their limit: they only touch port 25. They won't tell you a thing about an authenticated relay or a rogue proxy on 3128.
The tree below is exactly what Postfix walks for every inbound transaction. Knowing where each failure mode lives makes the config review faster:
Read your own config like an auditor
Dump the running config and look at the handful of directives that actually decide relay behavior:
`smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination` — this is the modern gate (Postfix 2.10+). The critical token is reject_unauth_destination. If it's missing, or if a bare permit sits anywhere in the list, everything after it is dead and you're likely wide open.
`mynetworks = 127.0.0.0/8 [::1]/128` plus only CIDRs you genuinely control. Never a provider subnet.
`relay_domains =` empty, unless you're a legitimate backup MX. The classic footgun here is relay_domains = $mydestination copied from an old guide — it turns your box into a near-relay for anything resembling a local domain.
Two settings cause most self-inflicted relays. mynetworks_style = subnet tells Postfix to trust the entire subnet of every local interface — on a cloud host that can mean hundreds of strangers. Set mynetworks_style = host, or better, drop the style directive and list explicit CIDRs. And check master.cf: the submission (587) and smtps (465) stanzas should carry -o smtpd_relay_restrictions=permit_sasl_authenticated,reject — auth required, foreign recipients allowed only for authenticated users. That override is correct on 587/465 and a catastrophe if it ever leaks onto port 25.
The authenticated open relay — the 2026 version
Here's the failure that gets clean-looking servers listed: your relay policy is perfect, so the attacker just logs in. One brute-forced or phished mailbox with permit_sasl_authenticated and no limits relays unlimited mail, fully "authenticated," straight past every restriction you wrote.
Cap it with real numbers:
# main.cf — anvil-based per-client and per-message limits
smtpd_recipient_limit = 100
smtpd_client_message_rate_limit = 30 # msgs per 60s anvil window, per client
# Stop authed users from spoofing arbitrary From addresses:
smtpd_sender_login_maps = hash:/etc/postfix/sender_login
Add reject_sender_login_mismatch to your submission restrictions so an authenticated user can only send as addresses their login owns. For volume caps that survive across the whole account, run postfwd or a policy service with a rule like "200 recipients per hour per sasl_username" — tight enough to stop a spam run, loose enough for real users. Then watch the logs; a single account suddenly topping the sender table is your early-warning siren:
Wire that into alerting and auto-disable the account on a spike. The difference between "we caught it in 200 messages" and "we caught it on the Spamhaus listing" is whether that query runs on a cron or never.
The proxy blind spots the MTA can't see
Now the part Postfix can't help you with. Anything on the public IP that can originate or relay SMTP outside the MTA is a live risk:
Squid with http_access allow all and CONNECT reachable — an attacker tunnels to any :25 through you.
SOCKS on :1080, or a dev HTTP forward proxy left listening on 0.0.0.0.
PHP `mail()` / sendmail from a compromised WordPress — it doesn't relay through Postfix's smtpd at all, it injects locally.
A dev SMTP container — mailpit/mailhog on :1025, or a test Postfix — with a published Docker port.
Redis/Elasticsearch bound to 0.0.0.0, used as a pivot to reach internal services.
Find every listener, then treat egress filtering as the real backstop:
bash
ss -tlnp | grep -E ':(25|465|587|1025|3128|1080)' # who is listening on the box
id postfix # note the uid (e.g. 100)
# nftables: only the postfix uid may make outbound SMTP connections.
nft add rule inet filter output tcp dport 25 skuid != 100 drop
That one rule is the difference-maker. Even if a container is compromised, or WordPress starts spraying mail(), or someone leaves a proxy open, the packets never leave the box on port 25 unless they came from Postfix. Config hygiene stops the known paths; egress filtering stops the ones you forgot about.
Confirm you're not already burned
Audit config and reputation in the same hour — if you're already listed, config fixes are step two. Check the majors by IP, querying from a resolver Spamhaus still answers (a self-hosted resolver, not 8.8.8.8 — they block queries from big public resolvers):
bash
# Reversed IP against Spamhaus ZEN (SBL+XBL+PBL). An A record = listed.
dig +short 210.69.22.212.zen.spamhaus.org
dig +short 210.69.22.212.cbl.abuseat.org
Enroll in the feedback loops that give you data before the blocklists do: Microsoft SNDS + JMRP, Google Postmaster Tools, and read your own outbound volume for anomalies with the sasl_username query above. A sender you've never heard of at the top of that list is a compromised account mid-run.
Lock it down in 20 minutes
mynetworks = 127.0.0.0/8 [::1]/128 plus only real trusted CIDRs — no provider subnets, no mynetworks_style = subnet.
smtpd_relay_restrictions ends in reject_unauth_destination; no bare permit anywhere in any restriction list.
relay_domains empty unless you're a backup MX; kill any = $mydestination.
Submission/smtps in master.cf: -o smtpd_relay_restrictions=permit_sasl_authenticated,reject. Auth-only, never on 25.
SASL limits: smtpd_recipient_limit = 100, per-user rate caps via postfwd (e.g. 200 rcpt/hour), plus reject_sender_login_mismatch and smtpd_sender_login_maps.
nftables egress: drop outbound :25 for every uid except Postfix.
Bind dev SMTP (mailpit/mailhog) to 127.0.0.1; audit ss -tlnp for stray proxies.
Enroll SNDS, JMRP, Google Postmaster; add a DNSBL self-check to cron.
Weekly cron re-runs the external swaks relay probe from an off-net VPS and alerts on 250 at RCPT.
Log alerting on NOQUEUE: reject spikes and unfamiliar sasl_username senders.
Run the external probe from another ASN right now, before you finish reading. Reputation takes an hour to lose and a month to earn back — the audit is the cheap part.