DMARC Postfix Setup


DMARC Postfix Setup

Introduction

Here we’ll work on DMARC Postfix Setup. I’ll avoid long story here. Domain-based Message Authentication, Reporting, and Conformance (DMARC) is a standard that helps protect email senders and recipients from spoofing, spam and phishing. It relies on SPF and DKIM to do so. For more details check “DMARC email validation system“. Without further ado, we’ll go straight to DMARC Postfix Setup.

Outgoing mail – DMARC DNS Record

Create a subdomain (_dmarc) and add TXT record. Adjust values to your linking. Example:

v=DMARC1;p=quarantine;sp=quarantine;adkim=r;aspf=r;fo=1;rf=afrf;rua=mailto:dmarc@cyberpunk.rs;ruf=mailto:dmarc@cyberpunk.rs

Count in DNS propagation time.

Incoming mail – DMARC with postfix

Install opendmarc:

# apt-get install opendmarc

Open conf and change lines:

# nano /etc/opendmarc.conf

AuthservID mail.cyberpunk.rs
PidFile /var/run/opendmarc.pid #Debian default
RejectFailures false
Syslog true
TrustedAuthservIDs mail.cyberpunk.rs, cyberpunk.rs
UMask 0002
UserID opendmarc:opendmarc
IgnoreHosts /etc/opendmarc/ignore.hosts
HistoryFile /var/run/opendmarc/opendmarc.dat
#for testing:
SoftwareHeader true

Some explanations (check more details on trusteddomains.org):

  • AuthservID:  hostname of your mail server (or unique string)
  • PidFile: Path to the PID file
  • RejectFailures:  if true, E-Mails that fail DMARC verification will be rejected by your mail server. To only tag them, set this to false
  • Syslog: True/False. Tells opendmarc, whether it should log to syslog or not
  • TrustedAuthservIDs: these AuthservIDs are assumed to be valid inputs for DMARC assessment. This can prevent the DMARC tests from running several times if you have multiple mail servers in your organization
  • UMask: the PID file and the socket file are created with this umask
  • UserID: User and group running the opendmarc service separated by “:”
  • IgnoreHosts: Ignored Hosts list file path
  • HistoryFile: The path under which the History file should be created. This file is necessary if you want to be able to create aggregate reports to send out to other organizations
  • SoftwareHeader: adds a “Dmarc-Filter” header in every processed mail (with the opendmarc version). Good to have during testing, disable when finished setting up.

For some reason Forensic Reporting option, doesn’t work. Setting ForensicReports to true, causes fatal error. Although there is a mentioned online on information leakage related to ForensicReports and mailing lists. We’ll ignore it for now.

Create ignore hosts file defined in configuration:

# mkdir /etc/opendmarc
# nano /etc/opendmarc/ignore.hosts

File should contain a list of networks and hosts that you trust. Their mail will not go through openDMARC checker:

localhost
192.168.1.0/24

When you create /etc/opendmarc directory, make sure ownership is set to “opendmarc” user. I had some errors like: “opendmarc-reports: can’t create report file for domain” and the problem was just that. Opendmarc-reports creates temporary files in the current directory, and due to wrong ownership/permissions it was unable to generate them.

To continue, add another line to default conf: /etc/default/opendmarc:

SOCKET="inet:12345@localhost"

Start OpenDMARC:

/etc/init.d/opendmarc start
or
service opendmarc start

Adjust Postfix, add to /etc/postfix/main.cf:

smtpd_milters=inet:localhost:12345
non_smtpd_milters=inet:localhost:12345

Reload postfix:

# /etc/init.d/postfix reload

DMARC verification on incoming mail should now be active. Easiest way to check is to send email to one of email accounts on your server. Look at the “Authentication-Results” field(s) and dmarc=pass.

DMARC Reporting

The server performs DMARC checks but it’s not  DMARC compliant yet. We’re missing reporting capabilities. To implement that missing segment, you’ll need access to MySQL DB. This might come in handy: MySQL Server Install

DMARC DB schema can be found on:

/usr/share/doc/opendmarc/schema.mysql

Default is ok, but if you want to avoid creating user database manually, uncomment:

-- CREATE USER 'opendmarc'@'localhost' IDENTIFIED BY 'changeme';
-- GRANT ALL ON opendmarc.* to 'opendmarc'@'localhost';

Those two lines will create user and assign appropriate rights. Adjust if needed (at least chose your password instead of “changeme”).

Import database:

# mysql -u root -p < /path/to/schema.mysql/schema.sql

Note: Import (or report_script execution, mentioned later on) might break due to “invalid default value for repurl” or “Invalid default value for ‘lastsent’. To circumvent it temporarily, you could place SET sql_mode = ''; at the top of the schema file. I’ve seen some schema alterations offered online:

ALTER TABLE messages MODIFY COLUMN policy_domain int(10) UNSIGNED DEFAULT NULL;
ALTER TABLE requests MODIFY COLUMN adkim tinyint(4) DEFAULT NULL;
ALTER TABLE requests MODIFY COLUMN aspf tinyint(4) DEFAULT NULL;
ALTER TABLE requests MODIFY COLUMN pct tinyint(4) DEFAULT NULL;
ALTER TABLE requests MODIFY COLUMN policy tinyint(4) DEFAULT NULL;
ALTER TABLE requests MODIFY COLUMN repuri VARCHAR(255) DEFAULT NULL;
ALTER TABLE requests MODIFY COLUMN spolicy tinyint(4) DEFAULT NULL;

Here is another “fixed” opendmarc schema. Use ALTER instead of CREATE if you have some data already present. When you run “dmarc_report” script mentioned below, errors might be reported. Correct schema manually if you have to. If you encounter additional “Incorrect datetime value: ‘0000-00-00 00:00:00’ for column” errors, correct the scripts (opendmarc-import/reports/expire).  MySQL probably runs in “strict” mode, and a setting NO_ZERO_DATE prevents such default timestamps.A raw fix, replace “0000-00-00 00:00:00” with “1971-01-01 00:00:01”. For instance, opendmarc-expire script have a hardcoded value:

$dbi_s = $dbi_h->prepare("DELETE FROM requests WHERE lastsent <= DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL ? DAY) AND NOT lastsent = '0000-00-00 00:00:00'");

Replace that with “1971-01-01 00:00:01”.

Once that step is done, we’ll create a script to input history file into DB and send the reports.

/etc/opendmarc/dmarc_report

I dug this script somewhere:

#!/bin/bash

DB_SERVER='database.example.com'
DB_USER='opendmarc'
DB_PASS='password'
DB_NAME='opendmarc'
WORK_DIR='/var/run/opendmarc'
REPORT_EMAIL='dmarc [at] example [dot] com'
REPORT_ORG=example.com'

mv ${WORK_DIR}/opendmarc.dat ${WORK_DIR}/opendmarc_import.dat -f
cat /dev/null > ${WORK_DIR}/opendmarc.dat

/usr/sbin/opendmarc-import --dbhost=${DB_SERVER} --dbuser=${DB_USER} --dbpasswd=${DB_PASS} --dbname=${DB_NAME} --verbose < ${WORK_DIR}/opendmarc_import.dat
/usr/sbin/opendmarc-reports --dbhost=${DB_SERVER} --dbuser=${DB_USER} --dbpasswd=${DB_PASS} --dbname=${DB_NAME} --verbose --interval=86400 --report-email $REPORT_EMAIL --report-org $REPORT_ORG
/usr/sbin/opendmarc-expire --dbhost=${DB_SERVER} --dbuser=${DB_USER} --dbpasswd=${DB_PASS} --dbname=${DB_NAME} --verbose

Adjust the details in this script, with the ones you used in previous step(s).

  • opendmarc-import reads per-message data recorded by an instance of opendmarc and inserts it into an SQL database, for later use by opendmarc-reports to generate aggregate reports. Records are read from standard input.
  • opendmarc-reports pulls data from an OpenDMARC database and generates periodic aggregate reports. The database is populated by a running opendmarc-import on a message history file generated by an opendmarc filter as messages arrive and are processed. This includes the collection of reporting URIs, which this script uses to make reports available to those that request them.
  • opendmarc-expire expires old records from the database that is part of the OpenDMARC aggregate reporting feature.

Make the script executable:

chmod +x /etc/opendmarc/dmarc_report

Run under the opendmarc user as a test:

su -c "/etc/opendmarc/dmarc_report" -s /bin/bash opendmarc
Note: Additional problems might occur, like “Use of uninitialized value in concatenation (.) or string at /usr/sbin/opendmarc-expire line 291” error. You must patch that. Replace:
if ($dbi_s->execute)
with:
if (!$dbi_s->execute)

Another problem: Use of uninitialized value $answer in scalar chomp at /usr/sbin/opendmarc-reports line 938/939
You must patch that as well:
$answer = ${${*$smtp}{'net_cmd_resp'}}[1];
if (!defined($answer)){
$answer = $smtp->message();
}
chomp($answer);

When finished, add that script to crontab:

nano /etc/crontab
or
crontab -e

0 1 * * * opendmarc /etc/opendmarc/dmarc_report

With this, execution is scheduled to run each day at 01:00.

To receive a copy of every outgoing DMARC report during testing, add one of you Mailboxes as bcc for every Message sent by the DMARC address. Add following line to your postfix main.cf:

sender_bcc_maps = hash:/etc/postfix/bcc_map

Create the file bcc_map with following content:

info@cyberpunk.rs dmarc@cyberpunk.rs

Apply mapping:

# postmap /etc/postfix/bcc_map

Restart Postfix:

# service postfix restart

Note: Additional problem appeared while sending emails. For each mail sent, I had 3 mails reported back to dmarc@cyberpunk.rs. One advice was to add receive_override_options in master.cf:

submission inet n - n - - smtpd
     -o receive_override_options=no_address_mappings

no_address_mappings: Disable canonical address mapping, virtual alias map expansion, address masquerading, and automatic BCC (blind carbon-copy) recipients. This is typically specified BEFORE an external content filter.

To test DMARC we need to send an email from our server to external server that support DMARC and send it from there to us. You can play with GMail, they support this. In mail header there should be a DMARC header.

Delete debug header in configuration after you get everything running:

nano /etc/opendmarc.conf
#SoftwareHeader true

Conclusion

This process was more difficult than I initially thought. A lot of inconsistencies, adjustments, schema issues (MySQL), including some things which were to be absorbed. It was a journey.. I probably didn’t cover everything, but it is a start. We’ll upgrade this as we go.

Things to work on:

  • Continue Testing, Playing around, see about the reports