Nethence NEWDOC OLDDOC Lab Webmail Your IP BBDock  

Setting up a strong anti-spam MX with Postfix


The Postfix config syntax changes depending on the version. I am using 3.1.0 here. Check with,

postconf -d|grep ^mail_ver

In this guide, the user’s Maildir will be stored in a shared Docker volume, however the user db is still on the container itself into /etc/passwd. The best would be to switch the mappings to MariaDB or something for the application to be independent from the data.

Liberating port 25 from the Docker host

We are going to setup an MX inside a container, therefore the smtp port needs to be freed from the Docker host.

You should see e.g. that port 25 is taken,

netstat -antpe --inet --inet6 | grep LISTEN

Assuming it’s an RHEL/CentOS docker host, disable Postfix from it,

service postfix stop
chkconfig postfix off

and check again,

netstat -antpe --inet --inet6 | grep LISTEN

Launching the container

Make sure that either the shared volume doesn’t or does exist whether it’s a fresh installation or not,

sudo ls -alhF /data/postfixprod/

Run the container based on the custom Ubuntu Docker image that containers additional packages,

docker ps -a | grep $app
docker run -d --name $app -h $app \
    -p 25:25 -p 587:587 -p 143:143 \
    -v /data/$app:/home \
    custom/ubuntu bash

apt update
apt full-upgrade

Note. in case you need to link some Postfix & Dovecot mappings to MariaDB,

    --link mariadbprod:mariadb \

Note. (optional) also if you like to,

    -v /data/postfixprod.conf:/etc/postfix \
    -v /data/postfixprod.spool:/var/spool/postfix \

Postfix prep

Make some handy symlinks for operations,

cd ~/
ln -s ../home
ln -s ../etc/postfix
ln -s /etc/aliases
ln -s /var/log/mail.log
ln -s /var/log/mail.err


cd /etc/postfix/
cp -pi
cp -pi

and edit the configurations,

smtpd_helo_required     = yes
strict_rfc821_envelopes = yes
disable_vrfy_command = yes
smtpd_delay_reject = yes

# PUBLIC NETWORK restrictions
#postmap /etc/postfix/client_access
smtpd_client_restrictions = permit_mynetworks,
        check_policy_service unix:private/policy,
        check_client_access hash:/etc/postfix/client_access,

# apews is blocking (
#       reject_rbl_client,

# not found
#       reject_rbl_client,

#policy_time_limit = 3600
# smtpd_policy_service_request_limit = 1

#reject_unknown_client_hostname --> unknown_client_reject_code
#reject_unknown_reverse_client_hostname --> unknown_client_reject_code
unknown_client_reject_code   = 554

# HELO/EHLO restrictions
smtpd_helo_restrictions = permit_mynetworks,

#reject_unknown_helo_hostname --> unknown_hostname_reject_code
unknown_hostname_reject_code = 554

# MAIL FROM restrictions
#postmap /etc/postfix/sender_access
smtpd_sender_restrictions = permit_mynetworks,
        check_sender_access hash:/etc/postfix/sender_access,

#        warn_if_reject,
unverified_sender_reject_code = 550
unverified_sender_reject_reason = Address verification failed
# Postfix 2.6 and later.
# unverified_sender_defer_code = 250

#proxy_write_maps = $smtp_sasl_auth_cache_name $lmtp_sasl_auth_cache_name $address_verify_map $postscreen_cache_map
address_verify_map = proxy:btree:$data_directory/verify_cache
address_verify_cache_cleanup_interval = 49h

# RCPT TO restrictions
smtpd_recipient_restrictions = permit_mynetworks,

#reject_unknown_sender_domain --> unknown_address_reject_code
#reject_unknown_recipient_domain --> unknown_address_reject_code
unknown_address_reject_code  = 554

# DATA restrictions
# Block clients that speak too early.
smtpd_data_restrictions = reject_unauth_pipelining

#smtpd_relay_restrictions (default: permit_mynetworks, permit_sasl_authenticated, defer_unauth_destination)
smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination

smtpd_error_sleep_time = 1s
smtpd_soft_error_limit = 10
smtpd_hard_error_limit = 20

#(default to myhostname)
myorigin = $myhostname
#(default to FQDN minus the first component)
mydomain =
myhostname =
mydestination = $mydomain, $myhostname,,, localhost.localdomain, localhost
mynetworks = [::ffff:]/104 [::1]/128

message_size_limit = 30720000
home_mailbox = Maildir/

#smtpd_banner (default: $myhostname ESMTP $mail_name)
smtpd_banner = $myhostname ESMTP
biff = no
append_dot_mydomain = no
readme_directory = no
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
relayhost =
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all
compatibility_level = 2

Note that the docker network is added to mynetworks so e.g. the RBL checks won’t be performed against the MX server itself.

The helo.regexp file may look like,

/^.*nethence\.com$/             550 This is not your FQDN

The sender_access file may look like,       OK

Make sure your DNS settings are SPF ready e.g.,

@ IN TXT "v=spf1 +mx ?a:yoursmarthost1 ?a:yoursmarthost2 -all"

and enable SPF checks,

apt-get install postfix-policyd-spf-python

policy  unix  -       n       n       -       0       spawn
     user=nobody argv=/usr/bin/policyd-spf


Allow Postfix to resolve hosts from its chroot land,

cd /var/spool/postfix/etc/
cp -pf /etc/resolv.conf /etc/hosts /etc/services ./

mkdir -p /var/spool/postfix/lib/x86_64-linux-gnu/
cd /var/spool/postfix/lib/x86_64-linux-gnu/
cp -vl /lib/x86_64-linux-gnu/libnss_* ./
ls -alhF

Make sure the mail users don’t get the dotfile skeletons,

mv -i /etc/skel/ /etc/skel.dist/
mkdir /etc/skel/

Don’t forget to tweak the container’s aliases e.g.,

cd /etc/
cp -pi aliases aliases.dist
vi aliases

root: mailuser
wheeleduser: root
abuse: root
contact: root
info: root
sales: root
hostmaster: root
www: root
webmaster: root


Dovecot prep

Maybe you would run the IMAP server on another system, but as the passwords are managed locally in this guide, there’s not much other choice right now, and it needs to point on the same Maildir location anyway,

apt install dovecot-imapd
cd /etc/dovecot
cp -pi dovecot.conf dovecot.conf.dist
cd conf.d
cp -pi 10-auth.conf.dist 10-auth.conf
vi 10-auth.conf

disable_plaintext_auth = yes

cp -pi 10-mail.conf 10-mail.conf.dist
vi 10-mail.conf

mail_location = maildir:~/Maildir

cp -pi 10-ssl.conf 10-ssl.conf.dist
vi 10-ssl.conf

ssl = yes
ssl_cert = </etc/ssl/certs/ssl-cert-snakeoil.pem
ssl_key = </etc/ssl/private/ssl-cert-snakeoil.key

cp -pi 20-imap.conf 20-imap.conf.dist
vi 20-imap.conf

imap_client_workarounds = tb-extra-mailbox-sep

Ready to go

Write the real init for the Docker container to start the daemons and start it,

cd ~/
cat > init.bash <<-EOF
cp -pf /etc/resolv.conf /etc/hosts /etc/services /var/spool/postfix/etc/
tail -F /var/log/mail.err &
/usr/lib/postfix/sbin/master -w
while true; do sleep 120; done
chmod +x init.bash

Note. master without -w if you want it to be the remaining process of the container’s entrypoint (CMD). See man 8 master. I prefer to use a fake init as remaining process so I can eventually really restart the postfix daemon while keeping the container up and running (e.g. for dovecot).

Make sure the unix socker for SPF is up and running,

netstat -an |grep policy

Read the logs,

tail -F /var/log/mail.*

while doing some testing from remote hosts (try with an authorized IP as well as from a should-be-blocked IP e.g. ADSL connection),

telnet 25
helo my.real.resolvable.remote.fqdn
mail from:<some.real@email>
rcpt to:<>

and also try to send an email to yourself with an email client of course (is your smarthost is in the SPF?). And don’t forget to do a last telnet check against an SMTP open proxy service, just in case you messed up smtpd_relay_restrictions.

Everything’s fine? Then commit to an image from the Docker host,

docker commit --change='CMD ["/root/init.bash"]' -p postfixprod postfixprod.`date +%s`.ready

or if the good entrypoint already has been applied, simply,

docker commit -p postfixprod postfixprod.`date +%s`.ready


You can then launch the container with its real init,

docker ps -a | grep $app
docker run -d --name $app -h $app \
    -p 25:25 -p 587:587 -p 143:143 \
    -v /data/$app:/home \
docker logs $app
docker exec -ti $app ps auxfw
#docker exec -ti $app bash


Creating / Removing Mailboxes

On the Docker container, create a new mailbox,

#useradd -g users -m -s /sbin/nologin $new
useradd -g users -m -s /dev/null $new
passwd $new
unset new

Note. The Maildir/ folder into the user’s homedir will be created by Postfix when the first mail arrives.


Deal with spam that eventually came through the hereby protections (wow, so that’s clean one!): look at the headers what fqdn connected to your MX and reject it manually so it doesn’t spam you again,

vi /etc/postfix/client_access           REJECT spammers are not welcome          REJECT spammers are not welcome

postmap /etc/postfix/client_access


Deal with further false-positive on some domain that cannot deal with the brutal verify-sender-against-all setting,

vi /etc/postfix/client_access       OK

postmap /etc/postfix/sender_access


If you get this error quite often in the logs,

close database /var/lib/postfix/verify_cache.db: No such file or directory (possible Berkeley DB bug)

==> use proxy: in the address_verify_map statement as shown above.



References about anti-spam & RFC compliance

References for MariaDB mappings


Host Integration

Configure the System D service,

cd /etc/systemd/system
vi postfixprod.service

Description=postfixprod container

ExecStart=/usr/bin/docker start -a postfixprod
ExecStop=/usr/bin/docker stop -t 30 postfixprod


systemctl daemon-reload

Eventually restart the shit using System D now,

#docker stop postfixprod
#systemctl start postfixprod
#systemctl status postfixprod

Enable the shit at startup,

systemctl enable postfixprod
systemctl status postfixprod