Confirm you are not an open relay¶
This is the single highest-priority check for any internet-facing Postfix server (see the DNS/routing fundamentals doc for why an open relay is such a serious problem). Critically, test from outside your own trusted network — connecting from inside mynetworks will always succeed and tells you nothing about how the server behaves to the rest of the internet.
Practical ways to test:
- Third-party open-relay testing services (search for "test mail server open relay" — the specific services referenced in the original notes are no longer reliably available, so use a currently-operating checker rather than a specific decade-old URL).
- Manually, from an external host/network you control, via telnet — attempt to send mail through your server, from an external sender, to an external (non-local) recipient:
telnet your-server.example.com 25
EHLO test.example.org
MAIL FROM:<someone@completely-unrelated-domain.com>
RCPT TO:<someone-else@another-unrelated-domain.com>
If that RCPT TO gets accepted (250), and you're not authenticated and not connecting from a trusted network, you are an open relay — go fix smtpd_recipient_restrictions/smtpd_relay_restrictions immediately (see the relaying and virtual-domains docs for what a proper restriction chain looks like, anchored on reject_unauth_destination).
Greylisting¶
Covered conceptually in the email-fundamentals anti-spam doc — here's the Postfix-side mechanics, via postgrey:
sudo yum install postgrey # or: sudo apt install postgrey
Postgrey runs as its own daemon with a Unix socket that's wired into Postfix's smtpd_recipient_restrictions as a policy service (typically check_policy_service unix:postgrey/socket, added to the restriction chain). Once running, you'll see log pairs like:
postfix/smtpd[31801]: connect from mail.example.net[203.0.113.92]
postgrey[31483]: action=greylist, reason=new, client_name=mail.example.net, client_address=203.0.113.92, sender=someone@example.net, recipient=you@yourdomain.com
postfix/smtpd[31801]: NOQUEUE: reject: RCPT from mail.example.net[203.0.113.92]: 450 4.2.0 <you@yourdomain.com>: Recipient address rejected: Greylisted
That 450 4.2.0 temporary rejection is expected and correct behavior on first contact — a legitimate sender's MTA retries later and gets through.
Whitelisting known-good senders¶
Some legitimate senders use large, rotating pools of outbound IPs for a single message (retrying from a different IP than the original attempt), which breaks greylisting's core assumption. For known senders like this, whitelist explicitly:
# /etc/postfix/postgrey_whitelist_clients.local
smtp.knownsender.example.com
service postgrey restart
If whitelisting doesn't seem to take effect, verify postgrey's startup options actually reference the whitelist file — this needs to be passed as a startup argument, not just have the file exist:
# postgrey init/service config
options="--unix=/var/spool/postfix/postgrey/socket --whitelist-clients=/etc/postfix/postgrey_whitelist_clients.local"
Header/body content filtering¶
For blocking a specific, identifiable pattern of unwanted mail (e.g. a spam campaign using a consistent subject line) without the overhead of a full content-scanning engine:
# /etc/postfix/main.cf
header_checks = regexp:/etc/postfix/header_checks
mime_header_checks = regexp:/etc/postfix/mime_header_checks
# /etc/postfix/header_checks
/^Subject:.*SALE OFF*/ REJECT **SPAM** matched sale-spam pattern
/^Subject:.*UPS notification/ REJECT **SPAM** matched UPS-notification-spam pattern
service postfix restart
tail -F /var/log/maillog
A matching message produces a log line like:
reject: header Subject: SALE OFF: Pharmacy store! from ...: 5.7.1 **SPAM** matched sale-spam pattern
and the sender receives a bounce containing your custom rejection text.
This approach is a blunt, reactive instrument — good for stamping out one specific, currently-active spam pattern quickly, bad as a general anti-spam strategy (it does nothing against anything that doesn't match your specific regexes, and needs manual upkeep as patterns change). For general-purpose filtering, a real content-analysis engine (SpamAssassin, rspamd) wired in as a content filter is the appropriate tool — header_checks is worth keeping as a fast, targeted escape valve alongside it, not a replacement for it.
General hardening checklist¶
Beyond the specific mechanisms above, a reasonable baseline for exposing Postfix to the internet:
- [ ] Confirmed not an open relay (test from outside your network, per above)
- [ ] smtpd_recipient_restrictions includes reject_unauth_destination and rejects on reject_non_fqdn_sender / reject_non_fqdn_recipient / reject_invalid_hostname at minimum
- [ ] Greylisting (or equivalent) enabled for unauthenticated inbound mail
- [ ] At least one RBL check in the restriction chain (see the email-fundamentals doc)
- [ ] TLS enabled both directions, smtpd_tls_security_level = may at minimum (see the TLS doc)
- [ ] SASL authentication required for any relaying beyond mynetworks (permit_sasl_authenticated alongside permit_mynetworks, never a bare permit before reject_unauth_destination)
- [ ] SPF/DKIM/DMARC published correctly for every domain this server sends as (see the email-fundamentals doc — this is a DNS-side check, not a Postfix config check, but it's just as important to your actual deliverability)
- [ ] Logs actually monitored — see the troubleshooting doc's egrep '(warning|error|fatal|panic):' pattern as a starting point for a regular/automated check, rather than only looking when something's visibly broken