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

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 significantly, 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:

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:

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

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!

Related Posts

comments