
Audit Postfix for Open Relay Vulnerabilities with Bash
Before you run this
This script reads your Postfix relay-control settings with postconf and flags configurations that could let outsiders relay mail through your server — the classic "open relay" that gets you onto blocklists and turns your box into a spam cannon. It is read-only. It changes nothing, writes nothing, and reloads nothing, so there is no config to back up and nothing to roll back. It simply reports.
You do not need root. postconf reads /etc/postfix/main.cf, which is world-readable on a standard install, so the script runs fine as an unprivileged user. Run it as root only if you have locked down permissions on your Postfix config.
Even for a read-only script, read it before you run it, and run it first on a test or staging mail server if you have one — not because it can break anything, but because you want to understand what "PASS" and "FAIL" mean here before you trust the verdict on a production gateway.
One honest limitation up front: a static config audit is a strong first pass, not proof. The only definitive test of an open relay is an SMTP transaction that originates from outside your trusted networks. This script can't do that from the mail server itself, because the server sees its own connection as trusted (127.0.0.1 is almost always in mynetworks). The verification section shows how to run the real external test.
I'm assuming Debian 12 / Ubuntu 22.04 with Postfix 3.x and bash. On RHEL/Alma the Postfix package and paths are the same; only the installer differs. The relay-control parameters below have been the standard since Postfix 2.10.
What actually makes a Postfix server an open relay
Two settings do most of the work:
smtpd_relay_restrictions— added in Postfix 2.10 specifically to stop accidental open relays. Its built-in default ispermit_mynetworks, permit_sasl_authenticated, defer_unauth_destination. That trailingdefer_unauth_destinationis what refuses mail bound for domains you don't handle, from senders you don't trust.smtpd_recipient_restrictions— the older place people putreject_unauth_destination.
Relay is refused if either chain contains reject_unauth_destination or defer_unauth_destination. You become an open relay when someone empties smtpd_relay_restrictions (thinking it's redundant) and the recipient restrictions have no reject_unauth_destination — or when mynetworks is set far too wide.
The second common mistake is mynetworks. Anything listed there can relay to anywhere. A stray 0.0.0.0/0, or a mynetworks_style = subnet on a public /24, quietly trusts hosts you never meant to.
The audit script
Save this as postfix-relay-audit.sh and make it executable with chmod +x postfix-relay-audit.sh.
#!/usr/bin/env bash
# postfix-relay-audit.sh
# Read-only audit of Postfix relay-control settings. Changes nothing.
set -euo pipefail
command -v postconf >/dev/null 2>&1 || {
echo "postconf not found — is Postfix installed?"; exit 1;
}
# Print the EFFECTIVE value Postfix would use (built-in defaults included).
# -h suppresses the "name =" prefix so we get the bare value.
val() { postconf -h "$1" 2>/dev/null; }
echo "=== Postfix relay-control audit ==="
echo "Host: $(hostname -f 2>/dev/null || hostname)"
echo "Postfix: $(val mail_version)"
echo
relay_restrictions=$(val smtpd_relay_restrictions)
recipient_restrictions=$(val smtpd_recipient_restrictions)
mynetworks=$(val mynetworks)
mynetworks_style=$(val mynetworks_style)
relay_domains=$(val relay_domains)
echo "smtpd_relay_restrictions = ${relay_restrictions:-<empty>}"
echo "smtpd_recipient_restrictions = ${recipient_restrictions:-<empty>}"
echo "mynetworks_style = ${mynetworks_style:-<empty>}"
echo "mynetworks = ${mynetworks:-<empty>}"
echo "relay_domains = ${relay_domains:-<empty>}"
echo
fail=0
combined="${relay_restrictions} ${recipient_restrictions}"
# 1) Is unauthorised relaying rejected/deferred somewhere in the chain?
if echo "$combined" | grep -Eq 'reject_unauth_destination|defer_unauth_destination'; then
echo "[PASS] Unauthorised destinations are rejected or deferred."
else
echo "[FAIL] Neither smtpd_relay_restrictions nor smtpd_recipient_restrictions"
echo " contains reject_unauth_destination (or defer_unauth_destination)."
echo " This server may accept mail for arbitrary destinations."
fail=1
fi
# 2) Overly broad mynetworks — the whole Internet trusted.
if echo "$mynetworks" | grep -Eq '(^|[[:space:]])0\.0\.0\.0/0|::/0'; then
echo "[FAIL] mynetworks includes 0.0.0.0/0 or ::/0 — every host can relay."
fail=1
fi
# 3) mynetworks_style = subnet can trust more than you expect on public nets.
if [ "${mynetworks_style}" = "subnet" ]; then
echo "[INFO] mynetworks_style is 'subnet': every host in this server's local"
echo " subnet(s) can relay. Confirm that is intended on a public network."
fi
echo
if [ "$fail" -ne 0 ]; then
echo "RESULT: potential open-relay conditions found (see FAIL lines above)."
exit 2
else
echo "RESULT: no open-relay conditions in the static configuration."
echo "Confirm with an EXTERNAL relay test before trusting this."
fi
A few notes on why it's built this way:
- It uses
postconf -h <param>rather than readingmain.cfdirectly, so it sees the effective value including Postfix's built-in defaults. Reading the file would miss a protective default that isn't written out explicitly. - The checks are conservative.
[INFO]onmynetworks_styleis a prompt to think, not a failure —subnetis fine on a properly segmented private network. - It exits
2when it finds something, so you can drop it into cron or a monitoring check and act on the exit code.
If you also want to eyeball everything you've changed from stock, run postconf -n — it prints only your non-default settings, which is the fastest way to review the whole config by hand.
Verifying with a real external relay test
The static audit above tells you what the config says. To prove what the server does, attempt a relay from a host that is not in mynetworks — a VPS, your workstation on a different network, anything external.
swaks (the Swiss Army Knife for SMTP) is the standard tool. On Debian/Ubuntu it's sudo apt install swaks. From an outside host:
# Run this from a host OUTSIDE your trusted networks.
# from/to are deliberately domains your server does NOT handle.
swaks --server mail.example.com \
--from probe@not-your-domain.example \
--to probe@somewhere-else.example \
--quit-after RCPT
Replace mail.example.com with your server's hostname, and use two domains your server has no business relaying for. A correctly configured server refuses at the RCPT TO stage with something like:
554 5.7.1 <probe@somewhere-else.example>: Relay access denied
That 554 ... Relay access denied is the result you want. If instead you get a 250 acceptance at RCPT TO, the server relayed — you have an open relay and need to fix the restrictions before doing anything else.
If you'd rather not stand up an external host, the MXToolbox online SMTP diagnostics / open-relay test connects from their infrastructure and reports the same thing. Either way, the point is that the probe must come from outside your trusted ranges.
If the audit fails
Fixing is a separate job from auditing, and I won't hand you a blind copy-paste that rewrites live restriction chains. The correct fix is almost always to restore Postfix's default protection — ensure smtpd_relay_restrictions includes defer_unauth_destination (or put reject_unauth_destination in smtpd_recipient_restrictions) and tighten mynetworks to only the hosts that must relay. Check the exact ordering and semantics in the official Postfix documentation for postconf(5) — see the pages for smtpd_relay_restrictions, smtpd_recipient_restrictions, and mynetworks — then apply with postconf -e "..." and reload with sudo systemctl reload postfix. Re-run this audit and the external swaks test afterward to confirm the relay is closed.
There's nothing to undo from the audit itself — it only reads. The undo that matters is on any config change you make in response, and that's a postconf -e back to the previous value followed by systemctl reload postfix.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Detect Spam Relay Abuse from Postfix Mail Logs
This guide gives you a read-only Python script that parses a Postfix mail log and reports two things: authenticated senders (SASL users) who sent an unusually large number of messages or recipients — the classic signature of a compromised mailbox being used to blast spam — and source IPs that keep tripping "Relay access denied", which is relay probing. The script does not change anything : it reads the log, counts, and prints a report. It never touches Postfix config, never disables an account, never blocks an IP.
Automatically Ban Abusive IPs in Postfix with Fail2ban
The standard, boring way to block IPs that hammer your mail server is Fail2ban. It watches the mail log, counts matching failures per source IP inside a time window, and when a source crosses a threshold it inserts a firewall rule to drop that IP for a while. You could write a bash script that greps the log and pipes IPs into nft , and I'll say where that fits at the end — but reinventing Fail2ban is more error-prone than configuring it, so that's what this guide does.
Automate Postfix Transport Map Updates Without Downtime
This guide gives you a small bash script that rebuilds a Postfix transport map from its flat-file source, validates the configuration, and reloads Postfix so the new routing takes effect. postfix reload re-reads configuration and recycles daemons gracefully — it does not drop in-flight SMTP connections or stop the queue, which is why we use it instead of restart .
A Bash Script to Test Mail Server Deliverability End to End
This script runs a series of read-only and send checks against a mail domain you control: it looks up MX, SPF, DKIM, and DMARC records with dig , tests the STARTTLS handshake on the submission port with openssl , and then sends one real test message through your server with swaks . Its purpose is to confirm, in one pass, that mail for your domain is configured to leave and arrive correctly.




