Why using DMARC?
To be able to report DKIM or SPF errors to domain owners, RFC7489 has been written. Since I wanted to report these errors and prevent from spam reaching me, I decided to install OpenDMARC on my CentOS 7 system.
Setup opendmarc
There is a good guide available to setup everything on CentOS 7. But since I already had most things setup, I only used the DMARC part. First I installed opendmarc. The guide mentioned issues with libspf2, but I didn’t have any issues with that.
[root@kari ~]# yum install opendmarc
After installing opendmarc, I edited the /etc/opendmarc.conf and used
the following configuration (without comments):
AuthservID HOSTNAME
BaseDirectory /var/run/opendmarc
FailureReports true
FailureReportsBcc <my email address>
FailureReportsOnNone true
FailureReportsSentBy <a noreply email address>
HistoryFile /var/spool/opendmarc/opendmarc.dat
IgnoreAuthenticatedClients true
IgnoreHosts /etc/opendmarc/ignore.hosts
PublicSuffixList /etc/opendmarc/public_suffix_list.dat
ReportCommand /usr/sbin/sendmail -t
Socket inet:8893@localhost
SoftwareHeader true
SPFIgnoreResults false
SPFSelfValidate true
Syslog true
UMask 007
UserID opendmarc:mail
In the /etc/opendmarc/ignore.hosts I put 127.0.0.0/8, the local
network range, the verious versions of the local hostname and the
FQDN of a system that forwards a lot of email to this system.
To be able to report to other domain owners I also needed to install MariaDB.
[root@kari ~]# yum install mariadb-server
By default MariaDB listens on all interfaces, but I don’t need that, so
I configure it to listen to 127.0.0.1 only by editing
/etc/my.cnf.d/local.cnf. I don’t use localhost,
because that only creates a UNIX socket, not a TCP socket.
[mysqld]
bind-address = 127.0.0.1
When that has been configured, I started the database and configured it to start automaticly.
systemctl start mariadb.service
systemctl enable mariadb.service
By default the CentOS 7 installation does not have a root password, so I set one.
[root@kari ~]# mysql -u root
MariaDB [(none)]> SET PASSWORD = PASSWORD(‘root password’);
Query OK, 0 rows affected (0.01 sec)
MariaDB [(none)]> \q
Bye
Because the CentOS 7 opendmarc package does not contain the mysql schema, I downloaded it from the github repository
wget https://raw.githubusercontent.com/trusteddomainproject/OpenDMARC/master/db/schema.mysql
Then I loaded this into MariaDB and created the opedmarc database user
with the correct permissions.
[root@kari ~]# mysql -u root -p mysql
MariaDB [mysql]> CREATE USER 'opendmarc'@'localhost' IDENTIFIED BY 'changeme*';
Query OK, 0 rows affected (0.01 sec)
MariaDB [mysql]> GRANT ALL PRIVILEGES ON opendmarc.* to 'opendmarc'@'localhost';
Query OK, 0 rows affected (0.00 sec)
MariaDB [mysql]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.01 sec)
MariaDB [mysql]> \q
Bye
Public suffic list
To make the opendmarc more effective, I want to use the public suffic list. First I try to download it manually.
[root@kari ~]# wget -q -N -P /etc/opendmarc https://publicsuffix.org/list/public_suffix_list.dat
When this succeeded, I created a file /etc/cron.weekly/opendmarc and
made it executable to run it weekly from crontab.
#!/bin/sh
# /etc/cron.weekly/opendmarc
/usr/bin/wget -q -N -P /etc/opendmarc https://publicsuffix.org/list/public_suffix_list.datReporting to domain owners
I’ve configured opendmarc to store the failed dmarc items in
/var/spool/opendmarc/opendmarc.dat
To finally send the reports a new script
/etc/cron.hourly/opendmarc-reporting has been created to do the job.
And this script has been made executable with chmod +x /etc/cron.hourly/opendmarc-reporting. And because the password to the
database is in the script, I didn’t make it world readable.
#!/bin/bash -x
# Imports data from OpenDMARC's opendmarc.dat file into a local MySQL DB
# and sends DMARC failure reports to domain owners.
# Based on a script from Hamzah Khan (http://blog.hamzahkhan.com/)
# Adjusted from
# https://www.luzem.com/2015/06/20/configure-centos-7-postfix-virtual-users/
# Local settings
REPORT_EMAIL="<reporting email address>"
REPORT_ORG="<reporting domain>"
REPORT_INTERVAL="86400"
DEBUG="true"
set -e
# Database and History File Info
DBHOST='localhost'
DBUSER='opendmarc'
DBPASS='changeme'
DBNAME='opendmarc'
OPENDMARC_PORT='3306'
HISTDIR='/var/spool/opendmarc'
HISTFILE='opendmarc'
TMPFILE="/tmp/opendmarc/${HISTFILE}.$$"
# Don't run when the file is empty
if [ ! -s ${HISTDIR}/${HISTFILE}.dat ]; then
exit 0;
fi
LOGFILE="$(mktemp -p /tmp/opendmarc)"
if [ ! -d $(dirname ${TMPFILE}) ]; then
mkdir -p $(dirname ${TMPFILE})
fi
# Move history file temp dir for processing
mv ${HISTDIR}/${HISTFILE}.dat ${TMPFILE}
# Import temp history file data and send reports
/usr/sbin/opendmarc-import --dbhost=${DBHOST} --dbuser=${DBUSER} --dbpasswd=${DBPASS} --dbname=${DBNAME} --verbose < ${TMPFILE} > ${LOGFILE} 2>&1
/usr/sbin/opendmarc-reports --dbhost=${DBHOST} --dbuser=${DBUSER} --dbpasswd=${DBPASS} --dbname=${DBNAME} --verbose --utc --interval=${REPORT_INTERVAL} --report-email ${REPORT_EMAIL} --report-org ${REPORT_ORG} >> ${LOGFILE} 2>&1
/usr/sbin/opendmarc-expire --dbhost=${DBHOST} --dbuser=${DBUSER} --dbpasswd=${DBPASS} --dbname=${DBNAME} --verbose >> ${LOGFILE} 2>&1
# Delete temp history file
if [ "x${DEBUG}" == "xtrue" ]; then
echo "Used ${TMPFILE} as source"
echo "Used ${LOGFILE} as log"
# Remove the log file when it is empty
if [ ! -s ${LOGFILE} ]; then
rm -rf ${LOGFILE}
fi
else
rm -rf ${TMPFILE}
rm -rf ${LOGFILE}
fi
exit 0After all this, we still have to configure postfix to use opendmarc, which is done by adding inet:127.0.0.1:8893 to smtpd_milters in the postfix main.cf .
This completes the setup to get everything running.
SELinux issues
When I received an email message I saw the following in the /var/log/messages (added newlines for better reading):
setroubleshoot: SELinux is preventing opendmarc from execute access on the file /usr/bin/bash. For complete SELinux messages run: sealert -l 20e14de5-5d48-4353-a779-60371ad0d964
python: SELinux is preventing opendmarc from execute access on the file /usr/bin/bash.
***** Plugin catchall (100. confidence) suggests **************************
If you believe that opendmarc should be allowed execute access on the bash file by default.
Then you should report this as a bug.
You can generate a local policy module to allow this access.
Do
allow this access for now by executing:
# ausearch -c 'opendmarc' --raw | audit2allow -M my-opendmarc
# semodule -i my-opendmarc.pp
Then I tried to find what was causing this by using strace
[root@kari ~]# strace -fp 26292 -v -o /var/tmp/opendmarc.strace.log -y -P /usr/bin
But this didn’t give any useful information, so I tried it without the
-P /usr/bin and saw the following:
27052 execve("/bin/sh", ["sh", "-c", "/usr/sbin/sendmail -t"], ["LANG=en_US.UTF-8", "PATH=/usr/local/sbin:/usr/local/"..., "HOME=/var/run/opendmarc", "LOGNAME=opendmarc", "USER=opendmarc", "SHELL=/sbin/nologin", "OPTIONS=-c /etc/opendmarc.conf -"...] <unfinished ...>
27052 <... execve resumed> ) = -1 EACCES (Permission denied)
This is triggered by the ReportCommand in the /etc/opendmarc.conf,
when I sent an email from an address which had a ruf= entry in the _dmarc
Resource Record.
I wondered why SELinux is complaining about /usr/bin/bash and opendmarc
is using /bin/sh. (This is probably why the -P /usr/bin didn’t find
anything.) It seems that /bin/sh on CentOS 7 is a symlink to
/bin/bash which is a hardlink of /usr/bin/bash
The source code uses a popen(3) systemcall to run the ReportCommand.
out = popen(conf->conf_reportcmd, "w");
if (out == NULL)
{
if (conf->conf_dolog)
{
syslog(LOG_ERR, "%s: popen(): %s",
dfc->mctx_jobid,
strerror(errno));
}
}
else
{
fwrite(dmarcf_dstring_get(dfc->mctx_afrf),
1, dmarcf_dstring_len(dfc->mctx_afrf),
out);
status = pclose(out);
if (status != 0 && conf->conf_dolog)
{
int val;
const char *how;
if (WIFEXITED(status))
{
how = "exited with status";
val = WEXITSTATUS(status);
}
else if (WIFSIGNALED(status))
{
how = "killed with signal";
val = WTERMSIG(status);
}
else
{
how = "returned status";
val = status;
}
syslog(LOG_ERR, "%s: pclose() %s %d",
dfc->mctx_jobid, how, val);
}
}popen(3) manual page says the following:
Failure to execute the shell is indistinguishable from the shell’s failure to execute command, or an immediate exit of the command. The only hint is an exit status of 127.
And in the maillog, I see a few pclose() exited with status 127, so
that points to an issue with the command.
Fixing the SELinux issue
As stated in the SELinux messages, I used those commands to fix the SELinux issue.
[root@kari ~]# ausearch -c 'opendmarc' --raw | audit2allow -M my-opendmarc
******************** IMPORTANT ***********************
To make this policy package active, execute:
semodule -i my-opendmarc.pp
[root@kari ~]# semodule -i my-opendmarc.pp
[root@kari ~]#
But this wasn’t the only SELinux change that had to be done. I had to do 26 SELinux changes, which I finally combined to the following (my-opendmarc-all.te):
module my-opendmarc-all 1.1;
require {
type dkim_milter_t;
type shell_exec_t;
type postfix_public_t;
type postfix_spool_t;
type postfix_master_t;
type postfix_etc_t;
type postfix_postdrop_exec_t;
type sendmail_exec_t;
class file { execute execute_no_trans create getattr open read rename setattr unlink write };
class sock_file { getattr write };
class unix_stream_socket connectto;
class dir { add_name remove_name search write };
class process setrlimit;
}
#============= dkim_milter_t ==============
allow dkim_milter_t shell_exec_t:file { execute_no_trans execute };
allow dkim_milter_t postfix_master_t:unix_stream_socket connectto;
allow dkim_milter_t postfix_public_t:dir search;
allow dkim_milter_t postfix_public_t:sock_file { getattr write };
allow dkim_milter_t postfix_spool_t:dir { add_name remove_name write search };
allow dkim_milter_t postfix_spool_t:file { create getattr open read rename setattr unlink write };
allow dkim_milter_t postfix_etc_t:dir search;
allow dkim_milter_t postfix_etc_t:file { getattr open read };
allow dkim_milter_t postfix_postdrop_exec_t:file { execute open read execute_no_trans };
allow dkim_milter_t self:process setrlimit;
allow dkim_milter_t sendmail_exec_t:file { execute getattr open read execute_no_trans };
And then compiled the file to a policy module and activated that.
[root@kari ~]# checkmodule -M -m -o my-opendmarc-all.mod my-opendmarc-all.te
[root@kari ~]# semodule_package -m my-opendmarc-all.mod -o my-opendmarc-all.pp
[root@kari ~]# semodule -i my-opendmarc-all.pp
This stopped all the warning in the log and now all the reporting works as
designed.
If you run into SELinux errors, which you will know when you temporarily
set SELinux from enforcing to permissive with setenforce 0 and everything
works. Then you might want to take a look at the auditd’s log to find what is
blocking your program.