Postfix Spam Reduction and Filtering

If your Postfix daemon is acting as an MX server receiving mail for your mailbox domain, these directive can help reduce the amount of spam receiving by rejecting a lot of it from the start. This will not stop all spam, but it will catch a lot of it.

You do not need to implement these directives if your Postfix daemon is not acting as an MX server.

Remember to run postfix reload after making any changes to your configuration.

Basic Spam Reduction

Add the following to your /etc/postfix/main.cf file:

# Basic Spam Reduction
strict_rfc821_envelopes = yes
disable_vrfy_command = yes
unknown_address_reject_code  = 554
unknown_hostname_reject_code = 554
unknown_client_reject_code   = 554
smtpd_helo_required = yes

What these directives actually do:

strict_rfc821_envelopes
When set to yes, Postfix rejects e-mail that is sent with an envelope that is not RFC 821 compliant. Such messages are almost always spam. False positives are possible but only if the sender is using a very poorly written e-mail client.
disable_vrfy_command
The SMTP VRFY command allows other systems to verify whether or not a user exists on the system. Spammers like to use it to verify e-mail addresses exist before adding the address to their spam list. Setting this to yes prevents spammers from easily verifying a particular user account exists.
unknown_{address,hostname,client}_reject_code
Setting these reject codes to 554 just tells the connecting MTA client that the transaction failed without giving specific information as to why it failed, which makes it more difficult for these clients to ascertain information about the validity of an address.
smtpd_helo_required
Proper MTA clients will always a HELO or EHLO when they connect to an MTA server. Many spambots do not. Setting this to yes rejects the transaction from clients that do not send it.

HELO Restrictions

It is a good idea to read the Postfix Documentation on this directive, these parameters should be safe but you may want to add others to fine tune your needs. Add this to your /etc/postfix/main.cf file:

#  HELO Restrictions
smtpd_helo_restrictions =
         permit_mynetworks,
         reject_non_fqdn_helo_hostname,
         reject_invalid_helo_hostname,
         permit
permit_mynetworks
Any connecting client with an IP that matches a network or network address listed in $mynetworks is permitted.
reject_non_fqdn_helo_hostname
Reject connections from clients that do not use a proper literal fully-qualified domain name in their HELO or EHLO as the RFC requires.
reject_invalid_helo_hostname
Reject connections from clients with a malformed hostname in their HELO or EHLO.
permit
Permit connections with a HELO or EHLO that did not trigger a rejection.

There are other directives it may be advantageous to add to the HELO restrictions, but I specifically advice against reject_unknown_sender_domain because it can be triggered as a false positive by a legitimate sender having a temporary DNS issue.

Sender Restrictions

This is similar to the HELO restrictions except it uses the domain name in the MAIL FROM command which is often different. Add this to your /etc/postfix/main.cf file:

#  Sender Restrictions
smtpd_sender_restrictions =
         permit_mynetworks,
         reject_non_fqdn_sender,
         reject_unknown_sender_domain,
         permit

Note in this case it does use reject_unknown_sender_domain and here it is appropriate for two reasons:

  1. The error generated tells the client MTA to try again later.
  2. If the sender domain does not resolve, sending a bounce message later will not be possible, so in the event it is a temporary DNS error, it needs to be rejected until the error is resolved in case a bounce is needed.

Relay Restrictions

The smtpd_relay_restrictions parameter is used to prevent Postfix from being a spam relay.

The default for this when not specified is permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination.

An unauth_destination is a destination your Postfix server is not configured to accept mail for. If the connecting MTA is not listed in mynetworks and is not an SASL authenticated user then we do not want our MTA to be a relay for it or spammers will relay messages through us. However I prefer to change the last directive to reject which is a permanent error rather than defer which is a temporary error. Add this to your /etc/postfix/main.cf file if you agree:

#  Relay Restrictions
smtpd_relay_restrictions =
         permit_mynetworks,
         permit_sasl_authenticated,
         reject_unauth_destination

Postscreen main.cf Configuration

Postscreen is a built-in spam filter that filters incoming messages based upon the behavior of the connection itself. It greatly reduces the load on the SMTP.

Postscreen also allows us to be a little more lenient in our use of Realtime Black Lists (RBL) reducing false positives while still catching much of what would have been caught if we were not quite so lenient.

Postscreen uses several different techniques to identify spam sent my spambots (sometimes called zombies) allowing them to often be identified and blacklisted based upon these techniques even if they are not on a specified blacklist being used.

Postscreen does not perform any content based filtering.

For full documentation on postscreen, see the Postfix Postscreen Howto. There are a lot of parameters discussed there that are not discussed here.

Postscreen Access List

This is a locally maintained white and blacklist of remote SMTP IP addresses that allows postsreen to skip DNS based maps and some tests.

For a postscreen access list table, use the man 5 cidr_table format. There are three different actions that can be taken when the IP of a connecting SMTP client matches a pattern in the table. With permit the postscreen service will allow the client and pass it directly to the smtp process. With reject the postscreen service will take the action configured via the postscreen_blacklist_action parameter. With dunno the postscreen service will exit the table avoiding the results of any further IP based matches that might apply, and continue testing the client as if it had not matched anything.

When not specified, postscreen_blacklist_action ignores the result. That is useful for testing but is not what we want in production. In production we want to either use enforce (reject with 550 SMTP) or drop (drop connection immediately with 521 SMTP).

Traditionally the file is named /etc/postfix/postscreen_access.cidr and looks something like this:

#comments start with a pound hash
##### mail.deviant.email  IPv4 + IPv6 ##
45.79.159.147                     permit
2600:3c03::f03c:91ff:fe55:db96    permit
##### mail.domblogger.net IPv4 + IPv6 ##
104.200.18.67                     permit
2600:3c00::f03c:91ff:fe56:d6a2    permit
##### git.domblogger.net  IPv4 + IPv6 ##
173.255.209.112                   permit
2600:3c01::f03c:91ff:fed8:1c0a    permit
##### This entire subnet spams a lot
## except for one I do not know
192.168.0.12                      dunno
192.168.0.0/16                    reject

If you have created a cidr table, add the following to your /etc/postfix/main.cf file:

#postscreen access list
postscreen_access_list = permit_mynetworks,
    cidr:/etc/postfix/postscreen_access.cidr
postscreen_blacklist_action = drop

DNS Whitelist and Blacklist Tests

You can use DNS based whitelists and blacklists to assist postscreen in identifying spam relays using the postscreen_dnsbl_sites parameter.

What these tests do is check the IP address of the connecting SMTP to see if it matches what it is one if these lists, and assigns a score based upon what it finds.

If after passing the IP address through all the configured lists the score has reached the threshold defined in the postscreen_dnsbl_threshold parameter, the action defined in postscreen_dnsbl_action is taken.

The postscreen_dnsbl_action parameter is just like the postscreen_blacklist_action parameter. By default when not specified, the action is ignore which is good for testing but not ideal for production. I personally prefer to set it to drop.

The postscreen_dnsbl_threshold parameter defaults to a value of 1 when not specified. The proper value depends completely on your strategy, how many DNS lists you use and what values you assign if there is a match in the list.

I personally use two lists, but I use the second list twice for different purposes with different values. I do use a value of 1 for the postscreen_dnsbl_threshold.

How DNS Blacklists and Whitelists Work

The mechanism for these DNS tests is defined in RFC 5782.

In a nutshell, what it does (using IPv4 as example) is take your IP address, reverse the octets, append the blacklist domain, and request the A record. So for example, the IP address 103.57.80.51. To check it with zen.spamhaus.org the domain name 51.80.57.103.zen.spamhaus.org is queried for the A record:

[user@host ~]$ dig A 51.80.57.103.zen.spamhaus.org. +short
127.0.0.11
127.0.0.4
127.0.0.3

That one has three A records returned.

When the IP is on the DNS list, the value that is returned is one or more IP addresses on the 127.0.0.0/8 loopback block, which is re-purposed as a return code with meaning. The actual meaning of the return code varies by the list. We can use those values to determine what we want to do based upon the meaning.

Sometimes one or more TXT also exists with notes about why it is on the list:

[user@host ~]$ dig TXT 51.80.57.103.zen.spamhaus.org. +short
"https://www.spamhaus.org/sbl/query/SBLCSS"
"https://www.spamhaus.org/query/ip/103.57.80.51"

With postscreen, you define the DNS list name followed by and = followed by the A record pattern to match against followed by a * followed by the score to assign if a matching A record exists.

You can leave out the * and the score, and a score of 1 will be assumed. You can leave out the = and the record pattern, and it will match if any A exists. All four of the following are valid arguments:

rbl.example.org                 // gives a score of 1 if any A record exists
rbl.example.org*3               // gives a score of 3 if any A record exists
rbl.example.org=127.0.0.[1..16] // gives a score of 1 if an A record matches pattern
rbl.example.org=127.0.0.5*3     // gives a score of 3 if an A record matches pattern

Some of the DNS lists will return an error code as an A record, for example if you have made more DNS queries than the free usage allows in a day. So I actually recommend against leaving the pattern out and just querying for the existence of a record as in the first two examples above.

The DNSWL.org White List

DNSWL.org maintains the DNS based whitelist I use. As long as your mail server needs less than 100,000 queries per day and you are not reselling their data, you can use their public DNS whitelist for free.

Large volume users and resellers need one of their paid options, see their website.

Unless you send a high volume of mail, in my opinion do not bother trying to get onto their whitelist. They do not publish their method for determining trust but from everyone I have communicated with who has submitted their mail servers for inclusion, unless you do a decent amount of outgoing mail your reputation will never grow beyond the default trust of None. To me that seems really illogical, systems that do not send a lot of mail are obviously not spammers, but logic is not what Silicon Valley values. Not anymore.

I have an idea for a whitelist that does not discriminate against low-volume servers, but I have not implemented it yet. Anyway, using their white list is beneficial.

Anyway details on the meaning of their return codes are at https://www.dnswl.org/?page_id=15. The first two octets are always 127.0. The third octet is used to define different categories for the type of business, information I find to be useless as it is not related to whether or not unsolicited commercial e-mail or malware is sent, so I choose to match all with [0..255]. For the fourth octet, there are four levels of trustworthiness. 0 indicates no trust, 1 for low trust, 2 for medium trust, and 3 for high trust. I like to match [1..3]. The A record pattern I want to match against is thus 127.0.[0..255].[1..3].

Since this is a white list, I give matches on this query a score of -1.

When crafting the argument to the postscreen_dnsbl_sites I would thus craft it as:
list.dnswl.org=127.0.[0..255].[1..3]*-1

The Spamhaus Zen RBL

Black-lists are a necessary evil. They are evil because they are poorly implemented. As a result of Snowshoe Spamming the blacklists tend to block an entire subnet when they detect one or two spammers on the subnet. This however results in a denial of service for legitimate hosts on the same subnet that do not spam. Punishing legitimate users for not being wealthy enough to afford their own subnet is, well, evil. However the sheer overwhelming volume of spam an MX server receives when a black-list is not used really does make them necessary.

The blacklist I use is Spamhaus Zen. It is free for lower volume non-commercial use, you need to buy a license for commercial use.

For their A record return code, they always use 127.0.0 for the first three octets. The fourth octet is used to distinguish between four different lists that are combined into the single list:

127.0.0.2
This means the IP address is on their SBL list. My personal experience is that list has a lot of false positives.
127.0.0.3
This means the IP address is on their CSS list. My personal experience is that list has a lot of false positives.
127.0.0.4-7
This means the IP address is on their XBL list. I am not aware of false positives very often on that list.
127.0.0.10-11
This means the IP address is on their PBL list. I am not aware of false positives very often on that list.

It appears they are not using 127.0.0.[8-9] at this time.

I like to give hits from the first two a score of 1 so it is negated if the IP is also on the DNSWL.org white list, and hits from the last two a score of 2 so it still triggers even if on the DNSWL.org white list.

I accomplish this by defining two different pattern matches:
zen.spamhaus.org=127.0.0.[2..3], zen.spamhaus.org=127.0.0.[4..11]*2

Barracuda RBL

This is another popular RBL that is free to use, at least under most circumstances.

It does require registration so I will not provide a copypasta string to use when configuring it with the postscreen service but the concept of how to use it is very similar. For specific details on return code values, please see the homepage at .

Their website for registration is not secure and I do not feel comfortable submitting the information they want (e.g. creating a password) for registration over an insecure connection so I have not actually tried their product. I hear it is a good list though.

The Full postscreen_dnsbl_sites Parameter

Using all three arguments combined, this is what I have added to my /etc/postfix/main.cf file:

#DNS list tests
postscreen_dnsbl_sites = list.dnswl.org=127.0.[0..255].[1..3]*-1,
    zen.spamhaus.org=127.0.0.[2..3],
    zen.spamhaus.org=127.0.0.[4..11]*2
postscreen_dnsbl_threshold = 1
postscreen_dnsbl_action = drop

Secret Key Warning

DNS was never designed for the transmission of authorization keys so their use is kind of hacked into non-free whitelists and blacklists.

If you have purchased a license from a DNS list, you license key is generally part of the list domain. For example, if the blacklist is rbl.example.org and your license key is cus1337 then you might use cus1337.rbl.example.org.

Postfix may expose this when rejecting connections that match, so you need to create a texthash table to filter it.

The texthash table would be called something like /etc/postfix/dnsbl_reply and contain, in this example, the following:

cus1337.rbl.example.org      rbl.example.org

Then add the following to your /etc/postfix/main.cf file:

postscreen_dnsbl_reply_map = texthash:/etc/postfix/dnsbl_reply

Caching Only Resolver Warning

If you are using one or more of the free lists, they generally limit how many DNS queries you can make in a 24 hour period. What they actually count is how often your DNS recursive resolver queries their authoritative DNS resolver.

If you are either not running a recursive resolver on your localhost or if you have Unbound configured to forward all queries to another recursive resolver, it is that recursive resolver being counted and you may be sharing the daily limit with many other users.

Make sure you followed the instructions on the Unbound Page but without the optional forward-zone that sends all queries upstream to another recursive resolver.

Postscreen master.cf Configuration

Now that the main.cf has been configured for the postscreen service, master.cf needs some changes.

In the LibreLAMP default master.cf file near the top are five lines that reads:

smtp      inet  n       -       n       -       -       smtpd
#smtp      inet  n       -       n       -       1       postscreen
#smtpd     pass  -       -       n       -       -       smtpd
#dnsblog   unix  -       -       n       -       0       dnsblog
#tlsproxy  unix  -       -       n       -       0       tlsproxy

Change that so it looks like the following:

#smtp      inet  n       -       n       -       -       smtpd
smtp      inet  n       -       n       -       1       postscreen
smtpd     pass  -       -       n       -       -       smtpd
dnsblog   unix  -       -       n       -       0       dnsblog
tlsproxy  unix  -       -       n       -       0       tlsproxy

Notice the first of five is now commented out and the other two are no longer commented out.

¡MUY IMPORTANTE!

If your master.cf file had any -o parameter=value lines after the top smtp line that you commented out (that line that ends with smtpd), move them so they are now directly after the now un-commented line that starts with smtpd pass (and also ends with smtpd)

Do not forget to reload Postfix:

[root@host ~]# postfix reload

SpamAssassin in Postfix

The postscreen service will eliminate a huge amount of spam. However it is not a content based filter.

You really should have a content based filter to help mitigate spam that is not caught by postscreen.

SpamAssassin is not the only option, but it is free.

To install SpamAssassin:

[root@host ~]# yum install spamassassin-postfix

Unless you have a lot of perl modules already installed, this will likely bring in a huge list of perl dependencies.

For Postfix to run all incoming mail through SpamAssassin, SpamAssassing has to be running in daemon mode as a service. Enable the service and start it:

[root@host #] systemctl enable spamassassin.service
[root@host #] systemctl start spamassassin.service

Postfix Integration

Edit the master.cf file. Find the line that reads:

smtpd     pass  -       -       n       -       -       smtpd

Directly after that line, add -o content_filter=spamassassin so that it now becomes:

smtpd     pass  -       -       n       -       -       smtpd
   -o content_filter=spamassassin

There must be at least one space before the -o.

Now at the very bottom of the master.cf file, we need to add four lines to define the spamassassin content filter:

# SpamAssassin Filter
spamassassin
          unix  -       n       n       -       -       pipe
   user=spamd argv=/usr/libexec/spamfilter.sh -oi -f ${sender} ${recipient}

Again note the spaces at the beginning of the last two lines. They let Postfix know to interpret those lines as a continuation of the line directly above. Reload Postfix:

[root@host ~]# postfix reload

All incoming mail that makes it past the postscreen filter will now pass through SpamAssassin, which will add a header called X-Spam-Flag: that will either have a value of YES or NO. That header can be used to filter spam into the Junk folder.

Dovecot Integration

If Dovecot Pigeonhole is installed and configured, create the sieve file /var/lib/dovecot/sieve.d/10-spamassassin.sieve containing

require "fileinto";
if header :contains "X-Spam-Flag" "YES" {
  fileinto "Spam";
}

Then in the file /etc/dovecot/conf.d/90-sieve.conf make sure the following line is un-commented:

sieve_before = /var/lib/dovecot/sieve.d/

Testing SpamAssassin

Using an e-mail account at a different server, send an e-mail to an account on the server you are configuring to use SpamAssassing using the precise subject line:
 
XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X

That is a test subject that SpamAssassin will always flag as spam if it is working properly. When you look at the headers in the e-mail, you should see a header that reads X-Spam-Flag: YES

If you are using Dovecot with Pigeonhole support properly configured and you have the previously mentioned sieve in place, that test e-mail should be sorted straight to the Spam folder of the account it was sent to.