Confirm you are not an open relay¶
This is the highest-priority check for any internet-facing Postfix (see DNS and Mail Routing for why). Test from outside your trusted networks. Connecting from inside mynetworks always succeeds and proves nothing.
From an external host you control:
swaks --server mail.domain.tld --from [email protected] --to [email protected] --quit-after RCPT
or by hand:
telnet mail.domain.tld 25
EHLO test.example.org
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
The correct answer is 554 5.7.1 … Relay access denied. If the RCPT TO is accepted with 250 while you're unauthenticated and outside mynetworks, you are an open relay. Fix smtpd_relay_restrictions immediately:
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
Also check that mynetworks doesn't include more than you think (postconf mynetworks). mynetworks_style = subnet on a cloud VM can trust a whole provider network. Online open-relay checkers exist, but they come and go. A test from any external machine is just as good.
A sensible inbound restriction chain¶
Postfix evaluates restrictions at each SMTP stage. With smtpd_delay_reject = yes (the default), everything is evaluated at RCPT TO, so you can put most rules in one list:
smtpd_helo_required = yes
disable_vrfy_command = yes
strict_rfc821_envelopes = yes
smtpd_recipient_restrictions =
permit_mynetworks,
permit_sasl_authenticated,
reject_unauth_pipelining,
reject_non_fqdn_helo_hostname,
reject_invalid_helo_hostname,
reject_non_fqdn_sender,
reject_unknown_sender_domain,
reject_non_fqdn_recipient,
reject_unknown_recipient_domain,
check_policy_service unix:postgrey/socket # only if using postgrey (below)
Old guides use reject_invalid_hostname and reject_non_fqdn_hostname. Those are the pre-2.3 names of the *_helo_hostname restrictions above. reject_unknown_client_hostname (requires FCrDNS of the client) is effective but strict; test it in warn_if_reject mode first.
postscreen: stop bots before smtpd¶
Most connections to port 25 come from spambots. postscreen sits in front of smtpd, holds each new client for a few seconds, and rejects those that talk before the greeting, appear on DNSBLs, or break protocol rules. It does this without spending an smtpd process on them. Legitimate servers are remembered and pass straight through afterwards.
# master.cf: replace the plain "smtp inet … smtpd" line with:
smtp inet n - y - 1 postscreen
smtpd pass - - y - - smtpd
dnsblog unix - - y - 0 dnsblog
tlsproxy unix - - y - 0 tlsproxy
# main.cf
postscreen_greet_action = enforce
postscreen_dnsbl_action = enforce
postscreen_dnsbl_threshold = 3
postscreen_dnsbl_sites =
zen.spamhaus.org*3
bl.spamcop.net*2
b.barracudacentral.org*2
list.dnswl.org=127.0.[0..255].[1..3]*-4 # allow-list known-good senders
postscreen_access_list = permit_mynetworks, cidr:/etc/postfix/postscreen_access.cidr
The weights let one strong listing (Spamhaus) reject on its own, while weaker lists only reject in combination. Use a local resolver: Spamhaus answers public-resolver queries with an error code instead of a listing.
Greylisting with postgrey¶
See Email Authentication and Anti-Spam for the concept and its trade-offs. With postscreen in place, many setups no longer need it. If you do want it:
sudo dnf install postgrey # EPEL on RHEL-family
sudo apt install postgrey
sudo systemctl enable --now postgrey
Wire it in as a policy service at the end of smtpd_recipient_restrictions (after the permit_* entries, so your own users are never greylisted). The socket differs by distribution:
check_policy_service unix:postgrey/socket # RHEL-family default
check_policy_service inet:127.0.0.1:10023 # Debian-family default
A first-contact delivery then logs:
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, [email protected], [email protected]
postfix/smtpd[31801]: NOQUEUE: reject: RCPT from mail.example.net[203.0.113.92]: 450 4.2.0 <[email protected]>: Recipient address rejected: Greylisted
The 450 4.2.0 temporary rejection is expected. A legitimate MTA retries after the delay (5 minutes by default) and gets through.
Allow-listing senders¶
Large senders retry from a different IP than the first attempt, which defeats greylisting's triplet matching and can delay their mail for hours. Postgrey ships a maintained allow-list. Add your own entries to the local file:
# /etc/postgrey/whitelist_clients.local (Debian)
# /etc/postfix/postgrey_whitelist_clients.local (RHEL-family)
smtp.knownsender.example
/^outbound\d+\.bulkmailer\.example$/
Restart postgrey after editing. If entries seem ignored, check that the daemon is actually started with that file. The options live in /etc/default/postgrey (POSTGREY_OPTS) on Debian and in /etc/sysconfig/postgrey on RHEL, e.g. --whitelist-clients=/etc/postfix/postgrey_whitelist_clients.local. The running command line (ps -o args -C postgrey) shows which files it loaded.
Header and body checks¶
These are a fast, blunt instrument for stamping out one identifiable campaign without a full content filter:
header_checks = regexp:/etc/postfix/header_checks
mime_header_checks = regexp:/etc/postfix/header_checks
# /etc/postfix/header_checks
/^Subject:.*\bSALE OFF\b/i REJECT 5.7.1 Message rejected (spam pattern)
/^Subject:.*UPS notification/i REJECT 5.7.1 Message rejected (phishing pattern)
/^Content-(Type|Disposition):.*name\s*=\s*"?.*\.(exe|scr|js|vbs|bat|cmd|com|iso|img)"?\s*$/i
REJECT 5.7.1 Executable attachments are not accepted
postmap -q "Subject: SALE OFF today" regexp:/etc/postfix/header_checks # test a rule
sudo postfix reload
A match logs reject: header Subject: SALE OFF … 5.7.1 Message rejected (spam pattern), and the sender gets a bounce with your text. Use WARN instead of REJECT to dry-run a new rule.
You can also use header checks to strip internal routing details from mail your users send. Use it carefully, because removing too much breaks DKIM and troubleshooting:
# /etc/postfix/submission_header_cleanup (applied only on submission via cleanup_service_name)
/^Received:.*with ESMTPSA/ IGNORE
/^X-Originating-IP:/ IGNORE
/^User-Agent:/ IGNORE
Regex checks are reactive and need constant upkeep. For general filtering, use a real engine.
Spam and virus filtering¶
rspamd is the modern default. It's fast, runs as a milter, and in one daemon covers scoring, Bayes, DNSBL/URIBL checks, SPF/DKIM/DMARC/ARC verification, DKIM signing, greylisting only of suspicious mail, and ratelimits:
smtpd_milters = inet:127.0.0.1:11332
non_smtpd_milters = $smtpd_milters
milter_default_action = accept
milter_protocol = 6
The alternative stack is amavisd-new + SpamAssassin + ClamAV as a content_filter. It's mature but heavier. Either way, keep ClamAV (or similar) for attachments if your users are on Windows.
Protecting SMTP AUTH¶
Submission ports attract constant password guessing.
- Offer
AUTHonly on 587/465 (smtpd_sasl_auth_enable = noglobally,yesin the submission services), and only after TLS (smtpd_tls_auth_only = yes). - Ban repeat offenders with fail2ban (the
postfix-sasljail, orpostfix[mode=aggressive]on newer fail2ban), plus thedovecotjail for IMAP:
ini
# /etc/fail2ban/jail.d/mail.local
[postfix-sasl]
enabled = true
[dovecot]
enabled = true
- Limit how fast any single client can connect, send or fail. Defaults come from the
anvilservice:
ini
smtpd_client_connection_rate_limit = 30
smtpd_client_message_rate_limit = 100
smtpd_client_auth_rate_limit = 10 # AUTH attempts per client per anvil_rate_time_unit
smtpd_error_sleep_time = 1s
smtpd_soft_error_limit = 10
smtpd_hard_error_limit = 20
- Stop authenticated users from forging other senders with
smtpd_sender_login_maps+reject_sender_login_mismatch(see Virtual Domains and Mailboxes). That limits the damage a single compromised password can do.
Information leakage¶
smtpd_banner = $myhostname ESMTP # no software name or version
disable_vrfy_command = yes # no address probing
smtpd_sasl_authenticated_header = no # don't name the user in Received:
show_user_unknown_table_name = no
Hardening checklist¶
- [ ] Confirmed not an open relay, tested from outside your network
- [ ]
smtpd_relay_restrictionsends inreject_unauth_destination;mynetworksis minimal and explicit - [ ] postscreen (or equivalent) with weighted DNSBLs, resolved through a local caching resolver
- [ ] HELO and envelope sanity restrictions (
reject_non_fqdn_*,reject_invalid_helo_hostname,reject_unknown_*_domain) - [ ] TLS in both directions at
mayon port 25 at minimum;encrypt+smtpd_tls_auth_onlyon submission (see TLS Encryption) - [ ] SASL only on 587/465, behind TLS; fail2ban or rate limits for brute force;
reject_sender_login_mismatch - [ ] A content filter (rspamd or amavis + SpamAssassin + ClamAV) for anything with real users
- [ ] SPF, DKIM, DMARC (and ideally MTA-STS and TLS-RPT) published for every domain sent from (see Email Authentication and Anti-Spam)
- [ ]
myhostnamematches the sending IP's PTR record, and the IP isn't on blocklists - [ ] Logs monitored routinely for
warning|error|fatal|panic(see Troubleshooting), not only when something breaks - [ ] OS and Postfix kept updated;
postfix checkclean