{"id":6173,"date":"2016-08-19T08:05:07","date_gmt":"2016-08-19T00:05:07","guid":{"rendered":"http:\/\/rmohan.com\/?p=6173"},"modified":"2016-08-19T08:05:07","modified_gmt":"2016-08-19T00:05:07","slug":"securing-your-postfix-mail-server-with-greylisting-spf-dkim-and-dmarc-and-tls","status":"publish","type":"post","link":"https:\/\/mohan.sg\/?p=6173","title":{"rendered":"Securing Your Postfix Mail Server with Greylisting, SPF, DKIM and DMARC and TLS"},"content":{"rendered":"<p>\u201dDomain-based Message Authentication, Reporting &amp; Conformance\u201d (DMARC).<\/p>\n<p>DMARC basically builds on top of two existing frameworks, Sender Policy Framework (SPF), and DomainKeys Identified Mail (DKIM).<\/p>\n<p>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\u2019t have any \u201ccontrol\u201d over what the receiving end does with email. For example, company1\u2019s mail server may just give the email a higher spam score if the sending mail server fails SPF authentication, while company2\u2019s mail server might outright reject it.<\/p>\n<p>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.<\/p>\n<p>I\u2019ve 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.<\/p>\n<p>Today, I was in the process of setting up a third backup MX for my domains, so I thought I\u2019d clean up my configs a little, and also setup DMARC properly in my mail servers.<\/p>\n<p>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\u2019t be going into full details for how to setup a Postfix server, only the specifics needed for SPF\/DKIM\/DMARC and TLS.<\/p>\n<p>We\u2019ll start with TLS as that is easiest.<br \/>\nTLS<\/p>\n<p>I wanted all incoming and outgoing mail to use opportunistic TLS.<\/p>\n<p>To do this all you need to do is create a certificate:<br \/>\n[root@servah ~]# cd \/etc\/postfix\/<br \/>\n[root@servah ~]# openssl genrsa -des3 -out mx1.example.org.key<br \/>\n[root@servah ~]# openssl rsa -in mx1.example.org.key -out mx1.example.org.key-nopass<br \/>\n[root@servah ~]# mv mx1.example.org.key-nopass mx1.example.org.key<br \/>\n[root@servah ~]# openssl req -new -key mx1.example.org.key -out mx1.example.org.csr<\/p>\n<p>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:<br \/>\n# Use opportunistic TLS (STARTTLS) for outgoing mail if the remote server supports it.<br \/>\nsmtp_tls_security_level = may<br \/>\n# Tell Postfix where your ca-bundle is or it will complain about trust issues!<br \/>\nsmtp_tls_CAfile = \/etc\/ssl\/certs\/ca-bundle.trust.crt<br \/>\n# I wanted a little more logging than default for outgoing mail.<br \/>\nsmtp_tls_loglevel = 1<br \/>\n# Offer opportunistic TLS (STARTTLS) to connections to this mail server.<br \/>\nsmtpd_tls_security_level = may<br \/>\n# Add TLS information to the message headers<br \/>\nsmtpd_tls_received_header = yes<br \/>\n# Point this to your CA file. If you used CAcert.org, this<br \/>\n# available at http:\/\/www.cacert.org\/certs\/root.crt<br \/>\nsmtpd_tls_CAfile = \/etc\/postfix\/ca.crt<br \/>\n# Point at your cert and key<br \/>\nsmtpd_tls_cert_file = \/etc\/postfix\/mx1.example.org.crt<br \/>\nsmtpd_tls_key_file = \/etc\/postfix\/mx1.example.org.key<br \/>\n# I wanted a little more logging than default for incoming mail.<br \/>\nsmtpd_tls_loglevel = 1<\/p>\n<p>Restart Postfix:<br \/>\n[root@servah ~]# service postfix restart<\/p>\n<p>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.<br \/>\nGreylisting<\/p>\n<p>Greylisting is method of reducing spam, which is so simple, yet so effective it\u2019s quite amazing!<\/p>\n<p>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.<\/p>\n<p>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!<\/p>\n<p>As EPEL already has an RPM for Postgrey, so I\u2019ll use that for Greylisting:<br \/>\n[root@servah ~]# yum install postgrey<\/p>\n<p>Set it to start on boot, and manually start it:<\/p>\n<p>[root@servah ~]# chkconfig postgrey on<br \/>\n[root@servah ~]# service postgrey start<\/p>\n<p>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\u2019ll use that. Edit \/etc\/postfix\/main.cf, and in your smtpd_recipient_restrictions, add \u201ccheck_policy_service unix:postgrey\/socket\u201d, like I have:<\/p>\n<p>smtpd_recipient_restrictions=<br \/>\npermit_mynetworks,<br \/>\nreject_invalid_hostname,<br \/>\nreject_unknown_recipient_domain,<br \/>\nreject_non_fqdn_recipient,<br \/>\npermit_sasl_authenticated,<br \/>\nreject_unauth_destination,<br \/>\ncheck_policy_service unix:postgrey\/socket,<br \/>\nreject_rbl_client dnsbl.sorbs.net,<br \/>\nreject_rbl_client zen.spamhaus.org,<br \/>\nreject_rbl_client bl.spamcop.net,<br \/>\nreject_rbl_client cbl.abuseat.org,<br \/>\nreject_rbl_client b.barracudacentral.org,<br \/>\nreject_rbl_client dnsbl-1.uceprotect.net,<br \/>\npermit<\/p>\n<p>As you can see, I am also using various RBLs.<\/p>\n<p>Next, we restart Postfix:<\/p>\n<p>[root@servah ~]# service postfix restart<\/p>\n<p>All done. Greylisting is now in effect!<br \/>\nSPF<\/p>\n<p>Next we\u2019ll setup SPF.<\/p>\n<p>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.<\/p>\n<p>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\u2019t necessary but I liked them so I applied them.<\/p>\n<p>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.<\/p>\n<p>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\u2019s 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.<\/p>\n<p>I build the RPMs, signed them with my key and published it in my internal RPM repo.<br \/>\nI won\u2019t go into detail into that, and will continue from installation:<\/p>\n<p>[root@servah ~]# yum install smf-spf<\/p>\n<p>Next, I edited \/etc\/mail\/smfs\/smf-spf.conf, set smf-spf to start on boot and started smf-spf:<\/p>\n<p>[root@servah ~]# cat \/etc\/mail\/smfs\/smf-spf.conf|grep -v &#8220;^#&#8221; | grep -v &#8220;^$&#8221;<br \/>\nWhitelistIP 127.0.0.0\/8<br \/>\nRefuseFail on<br \/>\nAddHeader on<br \/>\nUser smfs<br \/>\nSocket inet:8890@localhost<\/p>\n<p>Set smf-spf to start on boot, and also start it manually:<br \/>\n[root@servah ~]# chkconfig smf-spf on<br \/>\n[root@servah ~]# service smf-spf start<\/p>\n<p>Now we edit the Postfix config again, and add the following to the end of main.cf:<br \/>\nmilter_default_action = accept<br \/>\nmilter_protocol = 6<br \/>\nsmtpd_milters = inet:localhost:8890<\/p>\n<p>Restart Postfix:<br \/>\n[root@servah ~]# service postfix restart<\/p>\n<p>Your mail server should now be checking SPF records! ????<br \/>\nYou can test this by trying to forge an email from Gmail or something.<br \/>\nDKIM<\/p>\n<p>DKIM was a little more complicated to setup as I have multiple domains. Luckily, OpenDKIM is already in EPEL, so I didn\u2019t have to do any work to get an RPM for it! ????<\/p>\n<p>Install it using yum:<br \/>\n[root@servah ~]# yum install opendkim<\/p>\n<p>Next, edit the OpenDKIM config file. I\u2019ll just show what I done using a diff:<br \/>\n[root@servah ~]# diff \/etc\/opendkim.conf.stock \/etc\/opendkim.conf<br \/>\n20c20<br \/>\n&lt; Mode v<br \/>\n&#8212;<br \/>\n&gt; Mode sv<br \/>\n58c58<br \/>\n&lt; Selector default<br \/>\n&#8212;<br \/>\n&gt; #Selector default<br \/>\n70c70<br \/>\n&lt; #KeyTable \/etc\/opendkim\/KeyTable<br \/>\n&#8212;<br \/>\n&gt; KeyTable \/etc\/opendkim\/KeyTable<br \/>\n75c75<br \/>\n&lt; #SigningTable refile:\/etc\/opendkim\/SigningTable<br \/>\n&#8212;<br \/>\n&gt; SigningTable refile:\/etc\/opendkim\/SigningTable<br \/>\n79c79<br \/>\n&lt; #ExternalIgnoreList refile:\/etc\/opendkim\/TrustedHosts<br \/>\n&#8212;<br \/>\n&gt; ExternalIgnoreList refile:\/etc\/opendkim\/TrustedHosts<br \/>\n82c82<br \/>\n&lt; #InternalHosts refile:\/etc\/opendkim\/TrustedHosts<br \/>\n&#8212;<br \/>\n&gt; InternalHosts refile:\/etc\/opendkim\/TrustedHosts<\/p>\n<p>Next, I created a key:<br \/>\n[root@servah ~]# cd \/etc\/opendkim\/keys<br \/>\n[root@servah ~]# opendkim-genkey &#8211;append-domain &#8211;bits=2048 &#8211;domain example.org &#8211;selector=dkim2k &#8211;restrict &#8211;verbose<\/p>\n<p>This will give you two files in \/etc\/opendkim\/keys:<\/p>\n<p>dkim2k.txt \u2013 Contains your public key which can be published in DNS. It\u2019s already in a BIND compatible format, so I won\u2019t explain how to publish this in DNS.<br \/>\ndkim2k.private \u2013 Contains your private key.<\/p>\n<p>Next, we edit \/etc\/opendkim\/KeyTable. Comment out any of the default keys that are there and add your own:<br \/>\n[root@servah ~]# cat \/etc\/opendkim\/KeyTable<br \/>\ndkim2k._domainkey.example.org example.org:dkim2k:\/etc\/opendkim\/keys\/dkim2k.private<\/p>\n<p>(Thank you to andrewgdotcom for spotting the typo here)<\/p>\n<p>Now edit \/etc\/opendkim\/SigningTable, again commenting out the default entries and entering our own:<br \/>\n[root@servah ~]# cat \/etc\/opendkim\/SigningTable<br \/>\n*@example.org dkim2k._domainkey.example.org<\/p>\n<p>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.<\/p>\n<p>We can now start opendkim, and set it to start on boot:<br \/>\n[root@servah ~]# chkconfig opendkim on<br \/>\n[root@servah ~]# service opendkim start<\/p>\n<p>Almost done with DKIM!<br \/>\nWe 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:<br \/>\n# Pass SMTP messages through smf-spf first, then OpenDKIM<br \/>\nsmtpd_milters = inet:localhost:8890, inet:localhost:8891<br \/>\n# This line is so mail received from the command line, e.g. using the sendmail binary or mail() in PHP<br \/>\n# is signed as well.<br \/>\nnon_smtpd_milters = inet:localhost:8891<\/p>\n<p>Restart Postfix:<br \/>\n[root@servah ~]# service postfix restart<\/p>\n<p>Done with DKIM!<br \/>\nNow your mail server will verify incoming messages that have a DKIM header, and sign outgoing messages with your own!<br \/>\nOpenDMARC<\/p>\n<p>Now it\u2019s the final part of the puzzle.<\/p>\n<p>OpenDMARC is not yet in EPEL, but again I did find an RPM spec waiting review, so I used it.<\/p>\n<p>Again, I won\u2019t 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:<br \/>\n[root@servah ~]# yum install opendmarc<\/p>\n<p>First I edited \/etc\/opendmarc.conf:<br \/>\n15c15<br \/>\n&lt; # AuthservID name<br \/>\n&#8212;<br \/>\n&gt; AuthservID mx1.example.org<br \/>\n121c121<br \/>\n&lt; # ForensicReports false<br \/>\n&#8212;<br \/>\n&gt; ForensicReports true<br \/>\n144,145c144<br \/>\n&lt; HistoryFile \/var\/run\/opendmarc\/opendmarc.dat\/;<br \/>\n&lt; s<br \/>\n&#8212;<br \/>\n&gt; HistoryFile \/var\/run\/opendmarc\/opendmarc.dat<br \/>\n221c220<br \/>\n&lt; # ReportCommand \/usr\/sbin\/sendmail -t<br \/>\n&#8212;<br \/>\n&gt; ReportCommand \/usr\/sbin\/sendmail -t -F &#8216;Example.org DMARC Report&#8221; -f &#8216;sysops@example.org&#8217;<br \/>\n236c235<br \/>\n&lt; # Socket inet:8893@localhost<br \/>\n&#8212;<br \/>\n&gt; Socket inet:8893@localhost<br \/>\n246c245<br \/>\n&lt; # SoftwareHeader false<br \/>\n&#8212;<br \/>\n&gt; SoftwareHeader true<br \/>\n253c252<br \/>\n&lt; # Syslog false<br \/>\n&#8212;<br \/>\n&gt; Syslog true<br \/>\n261c260<br \/>\n&lt; # SyslogFacility mail<br \/>\n&#8212;<br \/>\n&gt; SyslogFacility mail<br \/>\n301c300<br \/>\n&lt; # UserID opendmarc<br \/>\n&#8212;<br \/>\n&gt; UserID opendmarc<\/p>\n<p>Next, set OpenDMARC to start on boot and manually start it:<br \/>\n[root@servah ~]# chkconfig opendmarc on<br \/>\n[root@servah ~]# service opendmarc start<\/p>\n<p>Now we tell postfix to pass messages through OpenDMARC. To do this, we edit \/etc\/postfix\/main.cf once again:<br \/>\n# Pass SMTP messages through smf-spf first, then OpenDKIM, then OpenDMARC<br \/>\nsmtpd_milters = inet:localhost:8890, inet:localhost:8891, inet:localhost:8893<\/p>\n<p>Restart Postfix:<br \/>\n[root@servah ~]# service postfix restart<\/p>\n<p>That\u2019s it! Your mail server will now check the DMARC record of incoming mail, and check the SPF and DKIM results.<\/p>\n<p>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.<\/p>\n<p>You should see that SPF, DKIM and DMARC are all being checked when receiving on either side.<\/p>\n<p>Finally, we can also setup forensic reporting for the benefit of others who are using DMARC.<br \/>\nDMARC Forensic Reporting<\/p>\n<p>I\u00a0 found OpenDMARC\u2019s documentation to be extremely limited and quite vague, so there was a lot of guess work involved.<\/p>\n<p>As I didn\u2019t 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.<\/p>\n<p>First I created a MySQL database and user for opendmarc:<br \/>\n[root@mysqlserver ~]# mysql -p<br \/>\nEnter password:<br \/>\nWelcome to the MariaDB monitor. Commands end with ; or \\g.<br \/>\nYour MariaDB connection id is 1474392<br \/>\nServer version: 5.5.34-MariaDB-log MariaDB Server<\/p>\n<p>Copyright (c) 2000, 2013, Oracle, Monty Program Ab and others.<\/p>\n<p>Type &#8216;help;&#8217; or &#8216;\\h&#8217; for help. Type &#8216;\\c&#8217; to clear the current input statement.<\/p>\n<p>MariaDB [(none)]&gt; CREATE DATABASE opendmarc;<br \/>\nMariaDB [(none)]&gt; GRANT ALL PRIVILEGES ON opendmarc.* TO opendmarc@&#8217;script-server.example.org&#8217; IDENTIFIED BY &#8216;supersecurepassword&#8217;;<\/p>\n<p>Next, we import the schema into the database:<\/p>\n<p>[root@scripty ~]# mysql -h mysql.example.org -u opendmarc -p opendmarc &lt; \/usr\/share\/doc\/opendmarc-1.1.3\/schema.mysql<\/p>\n<p>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:<\/p>\n<p>#!\/bin\/bash<\/p>\n<p>set -e<\/p>\n<p>cd \/home\/rmohan.com\/dmarc\/<\/p>\n<p>HOSTS=\u201dmx1.example.org mx2.example.org mx3.example.org\u201d<br \/>\nDBHOST=\u2019mysql.example.org\u2019<br \/>\nDBUSER=\u2019opendmarc\u2019<br \/>\nDBPASS=\u2019supersecurepassword\u2019<br \/>\nDBNAME=\u2019opendmarc\u2019<\/p>\n<p>for HOST in $HOSTS; do<br \/>\n# Pull the history from each host<br \/>\nscp -i \/home\/rmohan.com\/.ssh\/dmarc root@${HOST}:\/var\/run\/opendmarc\/opendmarc.dat ${HOST}.dat<br \/>\n# Purge history on each each host.<br \/>\nssh -i \/home\/rmohan.com\/.ssh\/dmarc root@${HOST} \u201ccat \/dev\/null &gt; \/var\/run\/opendmarc\/opendmarc.dat\u201d<\/p>\n<p># Merge the history files. Not needed, but this way opendmarc-import only needs to run once.<br \/>\ncat ${HOST}.dat &gt;&gt; merged.dat<br \/>\ndone<\/p>\n<p>\/usr\/sbin\/opendmarc-import \u2013dbhost=${DBHOST} \u2013dbuser=${DBUSER} \u2013dbpasswd=${DBPASS} \u2013dbname=${DBNAME} \u2013verbose &lt; merged.dat<br \/>\n\/usr\/sbin\/opendmarc-reports \u2013dbhost=${DBHOST} \u2013dbuser=${DBUSER} \u2013dbpasswd=${DBPASS} \u2013dbname=${DBNAME} \u2013verbose \u2013interval=86400 \u2013report-email \u2018sysops@example.org\u2019 \u2013report-org \u2018Example.org\u2019<br \/>\n\/usr\/sbin\/opendmarc-expire \u2013dbhost=${DBHOST} \u2013dbuser=${DBUSER} \u2013dbpasswd=${DBPASS} \u2013dbname=${DBNAME} \u2013verbose<\/p>\n<p>rm -rf *.dat<\/p>\n<p>That\u2019s it! Run that daily, and you\u2019ll send forensic reports to those who want them. ????<\/p>\n<p>You now have a nice mail server that checks SPF, DKIM, and DMARC for authentication, and sends out forensic reports!<\/p>\n<p>With this setup, I haven\u2019t received any spam in the last two months! That\u2019s just as far as I can remember, but I\u2019m sure it\u2019s been a lot longer than that! ????<\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u201dDomain-based Message Authentication, Reporting &amp; Conformance\u201d (DMARC).<\/p>\n<p>DMARC basically builds on top of two existing frameworks, Sender Policy Framework (SPF), and DomainKeys Identified Mail (DKIM).<\/p>\n<p>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 [&#8230;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[33],"tags":[],"_links":{"self":[{"href":"https:\/\/mohan.sg\/index.php?rest_route=\/wp\/v2\/posts\/6173"}],"collection":[{"href":"https:\/\/mohan.sg\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mohan.sg\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mohan.sg\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mohan.sg\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6173"}],"version-history":[{"count":1,"href":"https:\/\/mohan.sg\/index.php?rest_route=\/wp\/v2\/posts\/6173\/revisions"}],"predecessor-version":[{"id":6174,"href":"https:\/\/mohan.sg\/index.php?rest_route=\/wp\/v2\/posts\/6173\/revisions\/6174"}],"wp:attachment":[{"href":"https:\/\/mohan.sg\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6173"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mohan.sg\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6173"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mohan.sg\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6173"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}