River of Stars: Spam Block List Implementation


Anti-spam implementation

This page will hopefully show you how we implemented our anti-spam mail system.

We're using Weitse Venema's Postfix with Jozsef Kadlecsik's most excellent per-user UCE controls patch. We also owe a debt of gratitude to Furio Ercolessi of Spin, and to Ralf Hildebrandt for their clear configuration examples and explanations, upon which we based our (somewhat less clear) system.

We needed the following capabilities:

If your needs are similar, then, with a bit of interpretation and skull sweat on your part, you may be able to use our example to build your own selective anti-spam filter set.


Per-User Restrictions

Our per-user restrictions contain one line for each email address or alias. The classes used here are defined in main.cf.

/etc/postfix/user-restrictions:

 |  sales@example.com	class-minimal-blocking
 |  regularguy@example.com	class-regular-blocking
 |  spamshy@example.com	class-strong-blocking
 |  webmaster@example.com	class-light-blocking
 |  postmaster@example.com	class-no-blocking
 |  gone@example.com	class-nofalsenegatives-blocking
 |  bofh@example.com	class-bofh-blocking

Main configuration

Postfix's main.cf configuration file is where almost all the good stuff happens. We make extensive use of the inline rules and DUNNO response added by Jozsef Kadlecsik's patch.

/etc/postfix/main.cf:

 |  ...
 |  # declare lookup classes to ease per-user restrictions
 |  smtpd_restriction_classes =
 |  	class_nofalsenegatives_blocking, class_bofh_blocking, class_strong_blocking, class_regular_blocking, class_light_blocking, class_minimal_blocking, class_no_blocking,
 |  	class_rblplusdunno,
 |  	class_multi_rbldulrss, class_multi_rbldul, class_multi_rblrss, class_multi_rbl, class_multi_rss
 |  	class_osirusoftdunno,
 |  	class_multi_osirrlydynspewhauslistformprox, class_multi_osirrlydynspewhausformprox, class_multi_osirrlyspewhausformprox, class_multi_osirrlyhausformprox, class_multi_osirspewhausformprox, class_multi_osirrlyformprox, 
 |  	class_rfciipwhoisdunno,
 |  	class_multi_rfciipwhois,
 |  	class_rfciwhoisdunno,
 |  	class_multi_rfciwhois,
 |  	class_visi,
 |  	class_ordb,
 |  	class_monkeys_prox,
 |  	class_monkeys_form,
 |  	class_bopm,
 |  	class_dsbl,
 |  	class_rfci_pm,
 |  	class_rfci_abuse,
 |  	class_rfci_dsn,
 |  	class_korea,
 |  	class_flow,
 |  	class_pm0,
 |  	class_ciberlynx_us,
 |  	class_inflow_us,
 |  	class_rackspace_us,
 |  	class_verio_us,
 |  	class_valueweb_us,
 |  	class_eli_us,
 |  	class_argentina_us,
 |  	class_korea_us,
 |  	class_china_us,
 |  	class_taiwan_us,
 |  	class_level3_us,
 |  	class_reject_unknown_client,
 |  	class_reject_invalid_hostname,
 |  	class_reject_non_fqdn_hostname,
 |  	class_reject_unknown_hostname,
 |  	class_reject_unknown_sender_domain
 |  
 |  # define what classes declared above actually do
 |  #
 |  # classes for saving space in user-restrictions -- standard loads of
 |  # block features and lists
 |  class_nofalsenegatives_blocking =
 |  	reject
 |  class_bofh_blocking =
 |  	class_client_bad_domains, class_sender_bad_domains, class_bad_networks, class_reject_invalid_hostname, class_reject_non_fqdn_hostname, class_rblplusdunno, class_multi_rbldulrss, class_osirusoftdunno, class_multi_osirrlydynspewhauslistformprox, class_visi, class_dsbl, class_ordb, class_monkeys_prox, class_bopm, class_korea, class_ciberlynx_us, class_inflow_us, class_rackspace_us, class_verio_us, class_valueweb_us, class_eli_us, class_argentina_us, class_korea_us, class_china_us, class_taiwan_us, class_level3_us, class_valueweb_us, class_eli_us, class_verio_us, class_rfci_pm, class_rfci_abuse, class_rfciwhoisdunno, class_multi_rfciwhois, class_rfciipwhoisdunno, class_multi_rfciipwhois, class_slum_domains, class_reject_unknown_hostname, class_reject_unknown_client, permit
 |  class_strong_blocking =
 |  	class_client_bad_domains, class_sender_bad_domains, class_bad_networks, class_reject_invalid_hostname, class_reject_non_fqdn_hostname, class_rblplusdunno, class_multi_rbldulrss, class_osirusoftdunno, class_multi_osirrlydynspewhausformprox, class_visi, class_dsbl, class_ordb, class_monkeys_prox, class_bopm, class_korea, class_ciberlynx_us, class_inflow_us, class_rackspace_us, class_rfci_pm, class_rfci_abuse, class_rfciwhoisdunno, class_multi_rfciwhois, class_rfciipwhoisdunno, class_multi_rfciipwhois, class_slum_domains, class_reject_unknown_hostname, class_reject_unknown_client, permit
 |  class_regular_blocking =
 |  	class_client_bad_domains, class_sender_bad_domains, class_bad_networks, class_reject_invalid_hostname, class_rblplusdunno, class_multi_rbldulrss, class_osirusoftdunno, class_multi_osirrlydynspewhausformprox, class_visi, class_dsbl, class_ordb, class_monkeys_prox, class_bopm, class_korea, class_ciberlynx_us, class_inflow_us, class_rackspace_us, class_slum_domains, permit
 |  class_light_blocking =
 |  	class_client_bad_domains, class_sender_bad_domains, class_bad_networks, class_rblplusdunno, class_multi_rbldulrss, class_osirusoftdunno, class_multi_osirrlyhausformprox, class_visi, class_dsbl, class_ordb, class_monkeys_prox, class_bopm, class_korea, class_ciberlynx_us, class_inflow_us, permit
 |  class_minimal_blocking =
 |  	class_rblplusdunno, class_multi_rss, class_osirusoftdunno, class_multi_osirrlyformprox, class_visi, class_dsbl, class_ordb, class_monkeys_prox, class_bopm, permit
 |  class_no_blocking =
 |  	permit
 |  #
 |  # classes for multi-response lookup redirection
 |  class_rblplusdunno =
 |  	check_client_access rbl:inline:lookup_rblplusdunno
 |  class_osirusoftdunno =
 |  	check_client_access rbl:inline:lookup_osirusoftdunno
 |  class_rfciipwhoisdunno =
 |  	check_client_access rbl:inline:lookup_rfciipwhoisdunno
 |  class_rfciwhoisdunno =
 |  	check_sender_access rhsbl:inline:lookup_rfciwhoisdunno
 |  #
 |  class_multi_rbldulrss =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/rblplus-rbldulrss
 |  class_multi_rbldul =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/rblplus-rbldul
 |  class_multi_rblrss =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/rblplus-rblrss
 |  class_multi_rbl =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/rblplus-rbl
 |  class_multi_rss =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/rblplus-rss
 |  class_multi_osirrlydynspewhauslistformprox =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/osirusoft-rlydynspewhauslistformprox
 |  class_multi_osirrlydynspewhausformprox =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/osirusoft-rlydynspewhausformprox
 |  class_multi_osirrlyspewhausformprox =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/osirusoft-rlyspewhausformprox
 |  class_multi_osirrlyhausformprox =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/osirusoft-rlyhausformprox
 |  class_multi_osirspewhausformprox =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/osirusoft-spewhausformprox
 |  class_multi_osirrlyformprox =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/osirusoft-rlyformprox
 |  class_multi_rfciipwhois =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/rfci-ipwhois
 |  class_multi_rfciwhois =
 |  	check_access \$rbl_ip hash:$config_directory/dnsbls/rfci-whois
 |  #
 |  # classes for atomic lists
 |  class_visi =
 |  	check_client_access rbl:inline:lookup_visi
 |  class_dsbl =
 |  	check_client_access rbl:inline:lookup_dsbl
 |  class_ordb =
 |  	check_client_access rbl:inline:lookup_ordb
 |  class_monkeys_prox =
 |  	check_client_access rbl:inline:lookup_monkeys_prox
 |  class_monkeys_form =
 |  	check_client_access rbl:inline:lookup_monkeys_form
 |  class_bopm =
 |  	check_client_access rbl:inline:lookup_bopm
 |  class_rfci_pm =
 |  	check_sender_access rhsbl:inline:lookup_rfci_pm
 |  class_rfci_abuse =
 |  	check_sender_access rhsbl:inline:lookup_rfci_abuse
 |  class_rfci_dsn =
 |  	check_sender_access rhsbl:inline:lookup_rfci_dsn
 |  class_korea =
 |  	check_client_access rbl:inline:lookup_korea
 |  class_flow =
 |  	check_client_access rbl:inline:lookup_flow
 |  class_pm0 =
 |  	check_client_access rbl:inline:lookup_pm0
 |  class_ciberlynx_us =
 |  	check_client_access rbl:inline:lookup_ciberlynx_us
 |  class_inflow_us =
 |  	check_client_access rbl:inline:lookup_inflow_us
 |  class_rackspace_us =
 |  	check_client_access rbl:inline:lookup_rackspace_us
 |  class_verio_us =
 |  	check_client_access rbl:inline:lookup_verio_us
 |  class_valueweb_us =
 |  	check_client_access rbl:inline:lookup_valueweb_us
 |  class_eli_us =
 |  	check_client_access rbl:inline:lookup_eli_us
 |  class_korea_us =
 |  	check_client_access rbl:inline:lookup_korea_us
 |  class_argentina_us =
 |  	check_client_access rbl:inline:lookup_argentina_us
 |  class_china_us =
 |  	check_client_access rbl:inline:lookup_china_us
 |  class_taiwan_us =
 |  	check_client_access rbl:inline:lookup_taiwan_us
 |  class_level3_us =
 |  	check_client_access rbl:inline:lookup_level3_us
 |  #
 |  # classes for optional features
 |  # 450 non-resolving client IPs
 |  class_reject_unknown_client =
 |  	reject_unknown_client
 |  # 5xx syntactically incorrect HELO names
 |  class_reject_invalid_hostname =
 |  	reject_invalid_hostname
 |  # 5xx non-FQDN HELO names
 |  class_reject_non_fqdn_hostname =
 |  	reject_non_fqdn_hostname
 |  # 450 non-resolving HELO names
 |  class_reject_unknown_hostname =
 |  	reject_unknown_hostname
 |  # 450 non-resolving sender RHS
 |  class_reject_unknown_sender_domain =
 |  	reject_unknown_sender_domain
 |  
 |  # define inline lookup methods referred to by above class definitions
 |  #
 |  # multi-response services, seed the variables ($rbl_ip...) only, to
 |  # be used by later per-user default overrides
 |  lookup_rblplusdunno = rbl-plus.mail-abuse.org	DUNNO
 |  lookup_osirusoftdunno = relays.osirusoft.com	DUNNO
 |  lookup_rfciipwhoisdunno = ipwhois.rfci-ignorant.com	DUNNO
 |  lookup_rfciwhoisdunno = whois.rfci-ignorant.com	DUNNO
 |  #
 |  # atomic services (no further redirection, listed is listed)
 |  lookup_visi = relays.visi.com	554 \$client_address listed by Visi RSL. See <http://relays.visi.com/>. See <http://www.example.com/nospam/>
 |  lookup_dsbl = list.dsbl.org	554 \$client_address listed by DSBL. See <http://www.dsbl.org/>. See <http://www.example.com/nospam/>
 |  lookup_ordb = relays.ordb.org	554 \$client_address listed by ORDB. See <http://www.ordb.org/>. See <http://www.example.com/nospam/>
 |  lookup_monkeys_prox = proxies.relays.monkey.com	554 \$client_address listed by Monkeys Proxies. See <http://www.monkeys.com/anti-spam/filtering/proxies.html>. See <http://www.example.com/nospam/>
 |  lookup_monkeys_form = formmail.relays.monkey.com	554 \$client_address listed by Monkeys Formmail. See <http://www.monkeys.com/anti-spam/filtering/formmail.html>. See <http://www.example.com/nospam/>
 |  lookup_bopm = opm.blitzed.org	554 \$client_address listed by BOPM. See <http://www.blitzed.org/proxy/>. See <http://www.example.com/nospam/>
 |  lookup_rfci_pm = postmaster.rfc-ignorant.org	554 \$sender_domain listed by RFCI Postmaster, does not accept mail to postmaster@. See <http://www.rfc-ignorant.org/>. See <http://www.example.com/nospam/>
 |  lookup_rfci_abuse = postmaster.rfc-ignorant.org	554 \$sender_domain listed by RFCI Abuse, does not accept mail to abuse@. See <http://www.rfc-ignorant.org/>. See <http://www.example.com/nospam/>
 |  lookup_rfci_dsn = dsn.rfc-ignorant.org	554 \$sender_domain does not accept bounces (DSNs). See <http://www.rfc-ignorant.org/>. See <http://www.example.com/nospam/>
 |  lookup_korea = korea.services.net	554 \$client_address listed by Services Korea as Korean network likely to send spam. See <http://korea.services.net/>. See <http://www.example.com/nospam/>
 |  lookup_flow = flowgoaway.com	554 \$client_address listed by Flowgoaway. See <http://www.example.com/nospam/>
 |  lookup_pm0 = pm0-no-more.compu.net	554 \$client_address listed by PM0-No-More. See <http://www.example.com/nospam/>
 |  lookup_ciberlynx_us = ciberlynx.blackholes.us	554 \$client_address listed by Blackholes Ciberlynx. See <http://www.noflow.org/> and <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_inflow_us = inflow.blackholes.us	554 \$client_address listed by Blackholes Inflow. See <http://www.noflow.org/> and <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_rackspace_us = rackspace.blackholes.us	554 \$client_address listed by Blackholes Rackspace. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_verio_us = verio.blackholes.us	554 \$client_address listed by Blackholes Verio. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_valueweb_us = valueweb.blackholes.us	554 \$client_address listed by Blackholes Valueweb. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_eli_us = eli.blackholes.us	554 \$client_address listed by Blackholes ELI. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_korea_us = korea.blackholes.us	554 \$client_address listed by Blackholes Korea. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_argentina_us = argentina.blackholes.us	554 \$client_address listed by Blackholes Argentina. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_china_us = china.blackholes.us	554 \$client_address listed by Blackholes China. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_taiwan_us = taiwan.blackholes.us	554 \$client_address listed by Blackholes Taiwan. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  lookup_level3_us = level3.blackholes.us	554 \$client_address listed by Blackholes Level3. See <http://www.blackholes.us/>. See <http://www.example.com/nospam/>
 |  
 |  # handle everything in smtpd_recipient_restrictions to enforce
 |  # lookup order plus record sender and victim email addresses
 |  # whenever we refuse mail (this record is useful for automated
 |  # reporting to users regarding mail that was refused, along with
 |  # why)
 |  smtpd_client_restrictions = 
 |  smtpd_helo_restrictions =
 |  smtpd_sender_restrictions =
 |  smtpd_recipient_restrictions =
 |  ... [various mynetworks, fqdn, reject_unauth_pipelining, return path
 |       resolution, relay, reject_unauth_destination, etc. checks]...
 |  # if they don't accept bounces, they can't send
 |      class_rfci_dsn,
 |  # user-exceptions and user-exceptions-regexp are where per-user
 |  # sender address whitelisting goes
 |      check_access \${sender}|\${recipient} hash:$config_directory/user-exceptions,
 |      check_access \${sender}|\${recipient} pcre:$config_directory/user-exceptions-regexp,
 |  # user-restrictions is where per-user dnsbl & rhsbl selections go,
 |  # end each line with 'permit' to skip subsequent default lookups
 |      check_recipient_access hash:$config_directory/user-restrictions,
 |  # default features, local lookups, and dnsbl/rhsbl lookups
 |  	class_reject_invalid_hostname,
 |  	class_reject_non_fqdn_hostname,
 |  	class_rblplusdunno, class_multi_rbldulrss,
 |  	class_osirusoftdunno, class_multi_osirrlydynspewhausformprox,
 |  	class_visi,
 |  	class_dsbl,
 |  	class_ordb,
 |  	class_monkeys_prox,
 |  	class_bopm,
 |  	class_korea,
 |  	class_ciberlynx_us,
 |  	class_inflow_us,
 |  	class_rackspace_us,
 |  	class_verio_us,
 |  	class_valueweb_us,
 |  	class_eli_us,
 |  	class_argentina_us,
 |  	class_korea_us,
 |  	class_china_us,
 |  	class_taiwan_us,
 |  	class_level3_us,
 |  	class_rfci_pm,
 |  	class_rfci_abuse,
 |  	class_rfciwhoisdunno, class_multi_rfciwhois,
 |  	class_rfciipwhoisdunno, class_multi_rfciipwhois,
 |  	class_slum_domains,
 |  	class_reject_unknown_client,
 |  # reject unresolvable HELO here so as to let any previous hard
 |  # errors stop endless 450 retries
 |  	class_reject_unknown_hostname,
 |  # permit authorized destinations (in $inet_interfaces,
 |  # $mydestination, $virtual_maps, and $relay_domains or subdomains of
 |  # $relay_domains without user-specified routing)
 |      permit_auth_destination,
 |  # accept nothing else
 |      reject
 |  ...

Subsidiary lookups

Our selective use of multiplexed return codes didn't make sense (or fit) in inline rule files. For the non-inline lookups, we use files in a subdirectory we just happened to call dnsbls.

/etc/postfix/dnsbls/rfci-whois:

 |  127.0.0.5	554 $client_address has improper NIC contact records. See <http://www.rfc-ignorant.org/> and <http://www.rfc-ignorant.org/policy-whois.html>. See <http://www.example.com/nospam/>
 |  127.0.0.7	DUNNO

/etc/postfix/dnsbls/osirusoft-rlydynspewhauslistformprox:

 |  127.0.0.2	554 $client_address listed as open relay. See <http://relays.osirusoft.com/> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.3	554 $client_address listed as dynamic ip. See <http://relays.osirusoft.com/> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.4	554 $client_address listed as spam source. See <http://www.spews.org/> or <http://relays.osirusoft.com/> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.5	DUNNO
 |  127.0.0.6	554 $client_address listed as spamhaus or spamsite. See <http://www.spamhaus.org/> or <http://www.spamsites.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.7	554 $client_address listed as unconfirmed list sender. See <http://relays.osirusoft.com/> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.8	554 $client_address listed as open formmail. See <http://relays.osirusoft.com/> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.9	554 $client_address listed as open proxy. See <http://relays.osirusoft.com> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>

...

/etc/postfix/dnsbls/osirusoft-spewhausformprox:

 |  127.0.0.2	DUNNO
 |  127.0.0.3	DUNNO
 |  127.0.0.4	554 $client_address listed as spam source. See <http://www.spews.org/> or <http://relays.osirusoft.com/> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.5	DUNNO
 |  127.0.0.6	554 $client_address listed as spamhaus or spamsite. See <http://www.spamhaus.org/> or <http://www.spamsites.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.7	DUNNO
 |  127.0.0.8	554 $client_address listed as open formmail. See <http://relays.osirusoft.com/> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>
 |  127.0.0.9	554 $client_address listed as open proxy. See <http://relays.osirusoft.com> or <http://www.openrbl.org/>. See <http://www.example.com/nospam/>

/etc/postfix/dnsbls/rbldulrss:

 |  127.1.0.1	554 $client_address listed by MAPS RBL. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.2	554 $client_address listed by MAPS DUL. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.3	554 $client_address listed by MAPS RBL and DUL. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.4	554 $client_address listed by MAPS RSS. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.5	554 $client_address listed by MAPS RBL and RSS. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.6	554 $client_address listed by MAPS DUL and RSS. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.7	554 $client_address listed by MAPS RBL and DUL and RSS. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>

...

/etc/postfix/dnsbls/rbl:

 |  127.1.0.1	554 $client_address listed by MAPS RBL. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.2	DUNNO
 |  127.1.0.3	554 $client_address listed by MAPS RBL and DUL. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.4	DUNNO
 |  127.1.0.5	554 $client_address listed by MAPS RBL and RSS. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>
 |  127.1.0.6	DUNNO
 |  127.1.0.7	554 $client_address listed by MAPS RBL and DUL and RSS. See <http://www.mail-abuse.org/>. See <http://www.example.com/nospam/>