Postfix, relay MX

From FreeBSDwiki
Jump to: navigation, search

The goal here is to set up a spam and virus filtering relay MX, capable of acting as one of potentially many relays which show as the primary MX record for a domain. Ideally, the true destination MX will not show up in DNS at all - to prevent spammers from trying to target it directly. (Also ideally, that final destination MX will ONLY accept inbound mail from your relays, for hopefully obvious reasons.)


necessary software

Build /usr/ports/mail/postfix, /usr/ports/mail/spamassassin, and /usr/ports/security/clamav from ports.

Once they're built, you'll need to enable them in /etc/rc.conf and start them:

# echo 'spamd_enable="YES"' >> /etc/rc.conf
# echo 'clamav_freshclam_enable="YES"' >> /etc/rc.conf
# echo 'clamav_clamd_enable="YES"' >> /etc/rc.conf
# echo 'sendmail_enable="NONE"' >> /etc/rc.conf
# echo 'postfix_enable="YES"' >> /etc/rc.conf
# killall sendmail
# /usr/local/etc/rc.d/postfix start
# /usr/local/etc/rc.d/clamav-clamd start 
# /usr/local/etc/rc.d/clamav-freshclam start 
# /usr/local/etc/rc.d/sa-spamd start
# /usr/local/etc/rc.d/postfix start

You'll also need Postprox, which is not in the ports tree. Get it from - untar it, get into the directory the source code is in, and ./configure and make install without parameters. This will dump the binary in /usr/local/sbin and the manpages into the appropriate places.


You'll need to add this at the top:

permit_auth_destination = yes
transport_maps = hash:/usr/local/etc/postfix/transport
relay_domains = hash:/usr/local/etc/postfix/transport
relay_recipient_maps = hash:/usr/local/etc/postfix/recipients
unknown_relay_recipient_reject_code = 550
smtpd_client_restrictions = permit_sasl_authenticated, reject_unauth_destination, reject_unlisted_recipient, reject_rbl_client

Note that relay_recipient_maps is unnecessary if you aren't going to do recipient address verification during the SMTP conversation.

Also note that if you haven't actually set up SASL (authenticated SMTP) on your server, you shouldn't permit it in the smtpd_client_restrictions line. (As written, the permit argument is in place so that if you are running SASL, SASL authenticated clients won't be denied if they are listed in an RBL. This is necessary to allow legitimate, authenticated users on residential or other dynamic networks to relay mail.)

Finally, note that reject_rbl_client is set dead last on the smtpd_client_restrictions line - this means that before checking with a remote RBL, the local server will do address verification against its local tables. This saves traffic to the RBL and should result in cheaper rejections as well.


You'll need to append this at the bottom:

# SMTP Proxy.
# inet n n n - 20 spawn
  user=filter argv=/usr/local/sbin/postprox -v -r -c /usr/local/bin/

# After-filter SMTP server. Receive mail from the content filter
# on localhost port 10026.
# inet n  -       n       -        -      smtpd
    -o smtpd_authorized_xforward_hosts=
    -o smtpd_client_restrictions=
    -o smtpd_helo_restrictions=
    -o smtpd_sender_restrictions=
    -o smtpd_recipient_restrictions=permit_mynetworks,reject
    -o smtpd_data_restrictions=
    -o smtpd_junk_command_limit=100000
    -o smtpd_soft_error_limit=10000
    -o smtpd_error_sleep_time=0
    -o smtpd_proxy_filter=
    -o mynetworks=
    -o receive_override_options=no_unknown_recipient_checks

Now you'll need a filter script:



# If you don't export a HOME, you get 554 "~/.mailrc: No match" errors.
HOME=/home/filter ; export HOME

/usr/local/bin/clamdscan --no-summary --stdout - < "$EMAIL"
[ -z "$VSTATUS" ] && VSTATUS=9999

if [ $VSTATUS -eq 1 ]; then
        echo 550 Message contains a virus 1>&2
        exit 1
elif [ $VSTATUS -ne 0 ]; then
        echo 450 Unexpected virus scanning error, please retry later 1>&2
        echo CLAMDSCAN_ERROR - clamdscan returned unexpected exit code $VSTATUS | /usr/bin/logger -p mail.notice
        exit 1

echo "X-Antivirus-Scanner: ClamAV (PASSED)" > "$OUTFILE"

cat "$EMAIL" | /usr/local/bin/spamc -E -x --headers -t 30 >> "$OUTFILE"
[ -z "$SSTATUS" ] && SSTATUS=9999

[ $SSTATUS -eq 0 ] && [ $VSTATUS -eq 0 ] && exit 0

if [ $SSTATUS -eq 1 ]; then
        echo 550 This message appears to be spam, sorry 1>&2
        exit 1
elif [ $SSTATUS -eq 74 ]; then
        echo 451 Spam filtering timeout, please retry later 1>&2
        echo SPAMC_TIMEOUT - spamc returned exit code $SSTATUS | /usr/bin/logger -p mail.notice
        exit 1
elif [ $SSTATUS -ne 0 ]; then
        echo 452 Unexpected spam filtering error, please retry later 1>&2
        echo SPAMC_ERROR - spamc returned unexpected exit code $SSTATUS | /usr/bin/logger -p mail.notice
        exit 1

echo UNEXPECTED ERROR clamdscan exit code $VSTATUS spamd exit code $SSTATUS | /usr/bin/logger -p mail.notice
exit 2

It's worth noting that there is some "belt and suspenders" logic in this script - it should be impossible ever to hit the UNEXPECTED error and the exit 2 at the bottom of the script; and it should be unnecessary to check that SSTATUS is not equal to zero in the spam filtering section (since you already have a check to exit 0 if both VSTATUS and SSTATUS are 0 prior to that). This is not accidental! Odds are relatively good that you'll decide to monkey with your filtering at some point, and "unnecessary" checks like this may make it less likely that you'll screw yourself in unexpected ways if and when you do.


Now you'll need to set up the transport file. This is a list of each of your domains, with the destination to relay mail for that domain to, in the following format:

testing.test    smtp:[]:2525
moretests.test  smtp:[]

(Note that you can append a port number, if you don't want to use the standard port 25.)

Once you've got your transport file set up, you need to hash it for fast access by Postfix: postmap transport will do the trick. Do this each time you update the transport file.


This is a list of all valid email addresses at your domains. In this example, the second domain listed accepts any address at its domain.

test@testing.test OK
@moretests.test OK

As with transport, once you've got this file set up you need to hash it. postmap recipients will do the trick; do it again any time you change the file.

add the filter user

We need an unprivileged user to run the filter script.

pw useradd filter -s /sbin/nologin -d /home/filter
mkdir /home/filter
chown /home/filter

And since we're using site-wide scanning, we want to make sure that the same database always gets used for Bayes analysis. These lines go in /usr/local/etc/mail/spamassassin/

# change to non-NFS-safe file lock on Bayes databases (for speed)
lock_method flock
bayes_path /home/filter/.spamassassin/bayes
use_bayes 1

And restart spamd; /usr/local/etc/rc.d/sa-spamd restart. Note that unless you manually train the Bayes database with ham and spam, it will only learn by the autolearn mechanism! You can issue the command sa-learn --dump magic to check and see how many messages have been learned, how many tokens exist, etc.

all done!

postfix reload and you should be good to go.

If you expect to be maintaining a lot of domains or addresses, you'll probably also want to see Postfix, maintaining relay destinations.

Personal tools