Securing Your Postfix Mail Server with Greylisting, SPF, DKIM and DMARC and TLS

28/03/2015 – UPDATE- Steve Jenkins has created a more up-to-date version of this post which is definitely worth checking out if you are looking into deploying OpenDMARC. 🙂

A few months ago, while trying to debug some SPF problems, I came across “”Domain-based Message Authentication, Reporting & Conformance” (DMARC).

DMARC basically builds on top of two existing frameworks, Sender Policy Framework (SPF), and DomainKeys Identified Mail (DKIM).

SPF is used to define who can send mail for a specific domain, while DKIM signs the message. Both of these are pretty useful on their own, and reduce incoming spam A LOT, but the problem is you don’t have any “control” over what the receiving end does with email. For example, company1’s mail server may just give the email a higher spam score if the sending mail server fails SPF authentication, while company2’s mail server might outright reject it.

DMARC gives you finer control, allowing you to dictate what should be done. DMARC also lets you publish a forensics address. This is used to send back a report from remote mail servers, and contains details such as how many mails were received from your domain, how many failed authentication, from which IPs and which authentication tests failed.

I’ve had a DMARC record published for my domains for a few months now, but I have not setup any filter to check incoming mail for their DMARC records, or sending back forensic reports.

Today, I was in the process of setting up a third backup MX for my domains, so I thought I’d clean up my configs a little, and also setup DMARC properly in my mail servers.

So in this article, I will be discussing how I setup my Postfix servers using Greylisting, SPF, DKIM and DMARC, and also using TLS for incoming/outgoing mail. I won’t be going into full details for how to setup a Postfix server, only the specifics needed for SPF/DKIM/DMARC and TLS.

We’ll start with TLS as that is easiest.

TLS

I wanted all incoming and outgoing mail to use opportunistic TLS.

To do this all you need to do is create a certificate:
[root@servah ~]# cd /etc/postfix/
[root@servah ~]# openssl genrsa -des3 -out mx1.example.org.key
[root@servah ~]# openssl rsa -in mx1.example.org.key -out mx1.example.org.key-nopass
[root@servah ~]# mv mx1.example.org.key-nopass mx1.example.org.key
[root@servah ~]# openssl req -new -key mx1.example.org.key -out mx1.example.org.csr

Now, you can either self sign it the certificate request, or do as I have and use CAcert.org. Once you have a signed certificate, dump it in mx1.example.crt, and tell postfix to use it in /etc/postfix/main.cf:
# Use opportunistic TLS (STARTTLS) for outgoing mail if the remote server supports it.
smtp_tls_security_level = may
# Tell Postfix where your ca-bundle is or it will complain about trust issues!
smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.trust.crt
# I wanted a little more logging than default for outgoing mail.
smtp_tls_loglevel = 1
# Offer opportunistic TLS (STARTTLS) to connections to this mail server.
smtpd_tls_security_level = may
# Add TLS information to the message headers
smtpd_tls_received_header = yes
# Point this to your CA file. If you used CAcert.org, this
# available at http://www.cacert.org/certs/root.crt
smtpd_tls_CAfile = /etc/postfix/ca.crt
# Point at your cert and key
smtpd_tls_cert_file = /etc/postfix/mx1.example.org.crt
smtpd_tls_key_file = /etc/postfix/mx1.example.org.key
# I wanted a little more logging than default for incoming mail.
smtpd_tls_loglevel = 1

Restart Postfix:
[root@servah ~]# service postfix restart

That should do it for TLS. I tested by sending an email from my email server, to my Gmail account, and back again, checking in the logs to see if the connections were indeed using TLS.

Greylisting

Greylisting is method of reducing spam, which is so simple, yet so effective it’s quite amazing!

Basically, incoming relay attempts are temporarily delayed with a SMTP temporary reject for a fixed amount of time. Once this time has finished, any further attempts to relay from that IP are allowed to progress further through your ACLs.

This is extremely effective, as a lot of spam bots will not have any queueing system, and will not re-try to send the message!

As EPEL already has an RPM for Postgrey, so I’ll use that for Greylisting:
[root@servah ~]# yum install postgrey

Set it to start on boot, and manually start it:

[root@servah ~]# chkconfig postgrey on
[root@servah ~]# service postgrey start

Next we need to tell Postfix to pass messages through Postgrey. By default, the RPM provided init scripts setup a unix socket in /var/spool/postfix/postgrey/socket so we’ll use that. Edit /etc/postfix/main.cf, and in your smtpd_recipient_restrictions, add “check_policy_service unix:postgrey/socket”, like I have:

smtpd_recipient_restrictions=
permit_mynetworks,
reject_invalid_hostname,
reject_unknown_recipient_domain,
reject_non_fqdn_recipient,
permit_sasl_authenticated,
reject_unauth_destination,
check_policy_service unix:postgrey/socket,
reject_rbl_client dnsbl.sorbs.net,
reject_rbl_client zen.spamhaus.org,
reject_rbl_client bl.spamcop.net,
reject_rbl_client cbl.abuseat.org,
reject_rbl_client b.barracudacentral.org,
reject_rbl_client dnsbl-1.uceprotect.net,
permit

As you can see, I am also using various RBLs.

Next, we restart Postfix:

[root@servah ~]# service postfix restart

All done. Greylisting is now in effect!

SPF

Next we’ll setup SPF.

There are many different SPF filters available, and probably the most popular one to use with Postfix would be pypolicyd-spf, which is also included in EPEL, but I was unable to get OpenDMARC to see the Recieved-SPF headers. I think this is due to the order in which a message is passed through a milter and through a postfix policy engine, and I was unable to find a workaround. So instead I decided to use smf-spf, which is currently unmaintained, but from what I understand it is quite widely used, and quite stable.

I did apply some patches to smf-spf which were posted by Andreas Schulze on the the OpenDMARC mailing lists. They are mainly cosmetic patches, and aren’t necessary but I liked them so I applied them.

I was going to write a RPM spec file for smf-spf, but I noticed that Matt Domsch has kindly already submitted packages for smf-spf and libspf2 for review.

I did have to modify both packages a little. For smf-spf I pretty much only added the patches I mentioned eariler, and a few minor changes I wanted. For libspf2 I had to re-run autoreconf and update Matt Domsch’s patch as it seemed to break on EL6 boxes due to incompatible autoconf versions. I will edit this post later and add links to the SRPMS later.

I build the RPMs, signed them with my key and published it in my internal RPM repo.
I won’t go into detail into that, and will continue from installation:

[root@servah ~]# yum install smf-spf

Next, I edited /etc/mail/smfs/smf-spf.conf, set smf-spf to start on boot and started smf-spf:

[root@servah ~]# cat /etc/mail/smfs/smf-spf.conf|grep -v "^#" | grep -v "^$"
WhitelistIP 127.0.0.0/8
RefuseFail on
AddHeader on
User smfs
Socket inet:8890@localhost

Set smf-spf to start on boot, and also start it manually:
[root@servah ~]# chkconfig smf-spf on
[root@servah ~]# service smf-spf start

Now we edit the Postfix config again, and add the following to the end of main.cf:
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8890

Restart Postfix:
[root@servah ~]# service postfix restart

Your mail server should now be checking SPF records! 🙂
You can test this by trying to forge an email from Gmail or something.

DKIM

DKIM was a little more complicated to setup as I have multiple domains. Luckily, OpenDKIM is already in EPEL, so I didn’t have to do any work to get an RPM for it! 🙂

Install it using yum:
[root@servah ~]# yum install opendkim

Next, edit the OpenDKIM config file. I’ll just show what I done using a diff:
[root@servah ~]# diff /etc/opendkim.conf.stock /etc/opendkim.conf
20c20
< Mode v
---
> Mode sv
58c58
< Selector default
---
> #Selector default
70c70
< #KeyTable /etc/opendkim/KeyTable
---
> KeyTable /etc/opendkim/KeyTable
75c75
< #SigningTable refile:/etc/opendkim/SigningTable
---
> SigningTable refile:/etc/opendkim/SigningTable
79c79
< #ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
---
> ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
82c82
< #InternalHosts refile:/etc/opendkim/TrustedHosts
---
> InternalHosts refile:/etc/opendkim/TrustedHosts

Next, I created a key:
[root@servah ~]# cd /etc/opendkim/keys
[root@servah ~]# opendkim-genkey --append-domain --bits=2048 --domain example.org --selector=dkim2k --restrict --verbose

This will give you two files in /etc/opendkim/keys:

  • dkim2k.txt – Contains your public key which can be published in DNS. It’s already in a BIND compatible format, so I won’t explain how to publish this in DNS.
  • dkim2k.private – Contains your private key.

Next, we edit /etc/opendkim/KeyTable. Comment out any of the default keys that are there and add your own:
[root@servah ~]# cat /etc/opendkim/KeyTable
dkim2k._domainkey.example.org example.org:dkim2k:/etc/opendkim/keys/dkim2k.private

(Thank you to andrewgdotcom for spotting the typo here)

Now edit /etc/opendkim/SigningTable, again commenting out the default entries and entering our own:
[root@servah ~]# cat /etc/opendkim/SigningTable
*@example.org dkim2k._domainkey.example.org

Repeat this process for as many domains as you want. It would also be quite a good idea to use different keys for different domains.

We can now start opendkim, and set it to start on boot:
[root@servah ~]# chkconfig opendkim on
[root@servah ~]# service opendkim start

Almost done with DKIM!
We just need to tell Postfix to pass mail through OpenDKIM to verify signatures of incoming mail, and to sign outgoing mail. To do this, edit /etc/postfix/main.cf again:
# Pass SMTP messages through smf-spf first, then OpenDKIM
smtpd_milters = inet:localhost:8890, inet:localhost:8891
# This line is so mail received from the command line, e.g. using the sendmail binary or mail() in PHP
# is signed as well.
non_smtpd_milters = inet:localhost:8891

Restart Postfix:
[root@servah ~]# service postfix restart

Done with DKIM!
Now your mail server will verify incoming messages that have a DKIM header, and sign outgoing messages with your own!

OpenDMARC

Now it’s the final part of the puzzle.

OpenDMARC is not yet in EPEL, but again I did find an RPM spec waiting review, so I used it.

Again, I won’t go into the process of how to build an RPM, lets assume you have already published it in your own internal repos and continue from installation:
[root@servah ~]# yum install opendmarc

First I edited /etc/opendmarc.conf:
15c15
< # AuthservID name
---
> AuthservID mx1.example.org
121c121
< # ForensicReports false
---
> ForensicReports true
144,145c144
< HistoryFile /var/run/opendmarc/opendmarc.dat/;
< s
---
> HistoryFile /var/run/opendmarc/opendmarc.dat
221c220
< # ReportCommand /usr/sbin/sendmail -t
---
> ReportCommand /usr/sbin/sendmail -t -F 'Example.org DMARC Report" -f 'sysops@example.org'
236c235
< # Socket inet:8893@localhost
---
> Socket inet:8893@localhost
246c245
< # SoftwareHeader false
---
> SoftwareHeader true
253c252
< # Syslog false
---
> Syslog true
261c260
< # SyslogFacility mail
---
> SyslogFacility mail
301c300
< # UserID opendmarc
---
> UserID opendmarc

Next, set OpenDMARC to start on boot and manually start it:
[root@servah ~]# chkconfig opendmarc on
[root@servah ~]# service opendmarc start

Now we tell postfix to pass messages through OpenDMARC. To do this, we edit /etc/postfix/main.cf once again:
# Pass SMTP messages through smf-spf first, then OpenDKIM, then OpenDMARC
smtpd_milters = inet:localhost:8890, inet:localhost:8891, inet:localhost:8893

Restart Postfix:
[root@servah ~]# service postfix restart

That’s it! Your mail server will now check the DMARC record of incoming mail, and check the SPF and DKIM results.

I confirmed that OpenDMARC is working by sending a message from Gmail to my own email, and checking the message headers, then also sending an email back and checking the headers on the Gmail side.

You should see that SPF, DKIM and DMARC are all being checked when receiving on either side.

Finally, we can also setup forensic reporting for the benefit of others who are using DMARC.

DMARC Forensic Reporting

I  found OpenDMARC’s documentation to be extremely limited and quite vague, so there was a lot of guess work involved.

As I didn’t want my mail servers to have access to my DB server, I decided to run the reporting scripts on a different box I use for running cron jobs.

First I created a MySQL database and user for opendmarc:
[root@mysqlserver ~]# mysql -p
Enter password:
Welcome to the MariaDB monitor. Commands end with ; or \g.
Your MariaDB connection id is 1474392
Server version: 5.5.34-MariaDB-log MariaDB Server

Copyright (c) 2000, 2013, Oracle, Monty Program Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE opendmarc;
MariaDB [(none)]> GRANT ALL PRIVILEGES ON opendmarc.* TO opendmarc@'script-server.example.org' IDENTIFIED BY 'supersecurepassword';

Next, we import the schema into the database:

[root@scripty ~]# mysql -h mysql.example.org -u opendmarc -p opendmarc < /usr/share/doc/opendmarc-1.1.3/schema.mysql

Now, to actually import the data from my mail servers into the DB, and send out the forensics reports, I have the following script running daily:

#!/bin/bash

set -e

cd /home/mhamzahkhan/dmarc/

HOSTS=”mx1.example.org mx2.example.org mx3.example.org”
DBHOST=’mysql.example.org’
DBUSER=’opendmarc’
DBPASS=’supersecurepassword’
DBNAME=’opendmarc’

for HOST in $HOSTS; do
# Pull the history from each host
scp -i /home/mhamzahkhan/.ssh/dmarc root@${HOST}:/var/run/opendmarc/opendmarc.dat ${HOST}.dat
# Purge history on each each host.
ssh -i /home/mhamzahkhan/.ssh/dmarc root@${HOST} “cat /dev/null > /var/run/opendmarc/opendmarc.dat”

# Merge the history files. Not needed, but this way opendmarc-import only needs to run once.
cat ${HOST}.dat >> merged.dat
done

/usr/sbin/opendmarc-import –dbhost=${DBHOST} –dbuser=${DBUSER} –dbpasswd=${DBPASS} –dbname=${DBNAME} –verbose < merged.dat
/usr/sbin/opendmarc-reports –dbhost=${DBHOST} –dbuser=${DBUSER} –dbpasswd=${DBPASS} –dbname=${DBNAME} –verbose –interval=86400 –report-email ‘sysops@example.org’ –report-org ‘Example.org’
/usr/sbin/opendmarc-expire –dbhost=${DBHOST} –dbuser=${DBUSER} –dbpasswd=${DBPASS} –dbname=${DBNAME} –verbose

rm -rf *.dat

That’s it! Run that daily, and you’ll send forensic reports to those who want them. 🙂

You now have a nice mail server that checks SPF, DKIM, and DMARC for authentication, and sends out forensic reports!

With this setup, I haven’t received any spam in the last two months! That’s just as far as I can remember, but I’m sure it’s been a lot longer than that! 🙂

Any comments, and suggestions welcome!

17 thoughts on “Securing Your Postfix Mail Server with Greylisting, SPF, DKIM and DMARC and TLS”

  1. There is a small “bug” in your DMARC-RECORD for hamzahkhan.com or better g3nius.net:

    v=DMARC1; aspf=r; adkim=r; p=reject; pct=100; rua=mailto:aggrep@g3nius.net; ruf=mailto:authfail@g3nius.net;

    ### http://www.dmarc.org/faq.html#s_11 ###
    If you indicate that reports should be sent to an address outside your domain, you may need to request that the receiving party publish a special DMARC report DNS record:
    _dmarc.example.com TXT “v=DMARC1; p=none; rua=mailto:aggregate@thirdparty.com”
    example.com._report._dmarc.thirdparty.com TXT “v=DMARC1”
    ### http://www.dmarc.org/faq.html#s_11 ###

    For example you need something like this in die g3nuis.net DNS-ZONE:
    hamzahkhan.com._report._dmarc.g3nius.net TXT “v=DMARC1”

    You publish a “reject-policy” can you tell me something about your experiences with mailinglists?

    So long..

    1. Yep, the *._report._dmarc.thirdparty.com, requirement was added in a new draft of the DMARC specification, and I’ve been a bit lazy to add it since so far it is working anyway. (I know that’s bad! I’ll fix it some time :))

      As for the reject-policy and mailing-lists, it’s a pain.
      My messages get rejected due to being forwarded, so I’ve been using a different email address for mailing lists.

  2. Thanks for this! I already use postfix with everything except dmarc so it was really helpful to see how it integrates into such a system.

  3. Thanks. This was helpful. One minor nit: your script for loading DMARC data into mysql has a race condition, since you load the file and then erase it. The opendmarc-importstats script simply renames the file using an atomic “mv” command before loading it into the database with opendmarc-import. That should be a safer approach.

    Also, the new opendmarc 1.3.0 release seems to have the ability to do its own SPF checks. I haven’t tried this myself, but the SPFIgnoreResults and SPFSelfValidate options seem to control this behavior. This might make it possible to run without using a separate SPF milter like smf-spf.

    Did you make any attempt to get this running unix sockets instead of inet sockets? I worry about having a problem binding the ports in case one or more was randomly assigned to another process.

    1. Hi! Thank you for pointing out the race condition! I didn’t think about that.

      I’ve actually been running the OpenDMARC 1.3.0 beta for a while now, but I haven’t tried using the built in SPF checker either. Let me know if you try it! It’d be nice to cut down the number of components.

      I haven’t tried to use unix sockets instead of inet sockets. The machines I have this setup on, only function as my inbound mail relays and DNS servers, so I haven’t ever really needed to worry about port collisions. I may play with it and give it a shot later. There isn’t any real need for it to be using inet sockets.

  4. Hi, one other question — why aren’t you using postscreen, and why do you use postgrey instead of simply enabling the postscreen deep protocol tests?

    1. In all honesty, purely because this is my first time hearing about postscreen, while greylisting I have been using for quite sometime other MTAs as well. 🙂

      Are you using it? How does it compare to greylisting?

  5. postscreen is great. You should definitely check it out. It has a variety of checks to stop spam before even delivering the mail to postfix’s smtp processing. If you choose to enable the deep protocol tests, that is the equivalent of greylisting. I currently have not enabled the deep protocol tests, since I am not certain that I want to accept the initial greylisting retry delay. I am leaning towards finding a way to use greylisting only for emails that do not pass DMARC tests. I haven’t figured out how to do this yet, but I suspect I may be able to patch this feature into the milter-greylist code.

    You can learn more about postscreen here:
    http://www.postfix.org/POSTSCREEN_README.html

  6. Your DKIM KeyTable example is broken.

    The field you have populated with “:key:” should instead contain the name of the selector, in this case “:dkim2k:”. This is included in each signature (…; s=dkim2k; …) and is how the receiving host determines what DNS RR to look up to find your key. Good luck getting your signatures verified without it…!

    Instead, the example configuration implies that the selector is defined using the FQDN of the RR in both the KeyTable and SigningTable files. But you don’t need to use the FQDN – any labels can be used, so long as they define a mapping between the files. The FQDN is far too verbose for this purpose.

    1. Your DKIM KeyTable example is broken.

      Ah! That’s a typo! Thank you for pointing it out. I’ll put a note in. 🙂

      Instead, the example configuration implies that the selector is defined using the FQDN of the RR in both the KeyTable and SigningTable files. But you don’t need to use the FQDN – any labels can be used, so long as they define a mapping between the files. The FQDN is far too verbose for this purpose.

      Thank you for explaining that. It’s been a while since I set this up.

  7. Hi, Hamzah. Just an FYI that OpenDMARC is now available via Yum in the Fedora and EPEL repos. I’m the package maintainer for the Fedora project, and wrote an article on installing and configuring the installed package here:

    http://www.stevejenkins.com/blog/2015/03/installing-opendmarc-rpm-via-yum-with-postfix-or-sendmail-for-rhel-centos-fedora/

    I linked to this post from the article, and also gave you credit in the two scripts I included in the post.

    I second the earlier recommendation to drop smf-spf, sinceas OpenDMARC will now do SPF checks internally. I also second using Postscreen. It will do wonders for your inbound mail queue!

    Thanks!

    Steve

    P.S. I’m also the maintainer for OpenDKIM, so thank you for your mention and support of that package, too. 🙂

    1. Hi Steve,

      I already saw your article through the ping-back! 🙂
      I already put a note at the top of this post mentioning it, it’s a great write up!

      Thank you for your work in getting OpenDMARC into EPEL! It’ll make life a lot easier.

      I am actually using the built in SPF checks now, but I haven’t had time to update the article to mention it.

      Any plans on doing a post about Postscreen? I haven’t still had a chance to look at it to be honest.

  8. Hi, Hamzah. Awesome – thank you! FYI – I’m now the co-maintainer of libspf2, so I just finished submitting (as in minutes ago) an updated version of the OpenDMARC package that builds against libspf2-devel, which by all reports is much more robust than the SPF library currently built into OpenDMARC. And yes, I really should fire up a Postscreen article. My Postfix config (including Postscreen) has been very stable for the past few years, so it’s probably fine to tell others to rely on it. 🙂

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.