Tag Archives: dmarc

SPF, DKIM, DMARC, MTA-STS

Securing and reducing spam is an ongoing battle.

Prerequisits:
* DNSSEC [usually managed by your domain provider and if you run bind]
* PTR [usually setup by your ISP unless you run an authoritative DNS. Implies a static IP]
* Exim >4.7

Useful Links:
http://www.dnssec-or-not.com
https://dnssec-analyzer.verisignlabs.com
https://dnschecker.org/domain-health-checker.php
https://en.internet.nl/

SPF (Sender Policy Framework)

You can put the following into it's own config eg acl_check_spf or place it in the global acl (acl_check_data:).

    deny condition = ${if eq{$sender_helo_name}{} {1}}
         message = Nice bots say HELO first

    # reject messages from senders listed in these DNSBLs
    deny dnslists = zen.spamhaus.org

    # SPF validation
    deny spf = fail : softfail
            message = SPF validation failed: \
                    $sender_host_address is not allowed to send mail from \
                    ${if def:sender_address_domain \
                        {$sender_address_domain}{$sender_helo_name}}
            log_message = SPF validation failed\
                    ${if eq{$spf_result}{softfail} { (softfail)}{}}: \
                    $sender_host_address is not allowed to send mail from \
                    ${if def:sender_address_domain \
                        {$sender_address_domain}{$sender_helo_name}}
    deny spf = permerror
            message = SPF validation failed: \
                    syntax error in SPF record(s) for \
                    ${if def:sender_address_domain \
                        {$sender_address_domain}{$sender_helo_name}}
            log_message = SPF validation failed (permerror): \
                    syntax error in SPF record(s) for \
                    ${if def:sender_address_domain \
                        {$sender_address_domain}{$sender_helo_name}}
    defer spf = temperror
            message = temporary error during SPF validation; \
                    please try again later
            log_message = SPF validation failed temporary; deferred
    # Log SPF none/neutral result
    warn spf = none : neutral
            log_message = SPF validation none/neutral

    # Use the lack of reverse DNS to trigger greylisting. Some people
    # even reject for it but that would be a little excessive.

    warn condition = ${if eq{$sender_host_name}{} {1}}
         set acl_m_greylistreasons = Host $sender_host_address \
             lacks reverse DNS\n$acl_m_greylistreasons

    accept
            # Add an SPF-Received header to the message
            add_header = :at_start: $spf_received
            logwrite = SPF validation passed

You will also need a TXT record publishing with the registrar and/or internal DNS.

Host nameTypeTTLData
example.comTXT1 hour"v=spf1 ip4:xxx.xxx.xxx.xxx ip6::1 -all"

Looking at the record itself, we see that the version indicator, 'v=spf1', is followed by a typical SPF policy: first a list of systems that are authorised to send mail for the domain, then '-all', which means that all other systems are not authorised. The alternative to ending the record with '-all' is to end with '~all'. That is known as a 'soft fail', meaning that messages from non-validating systems should not be blocked, but forwarded with a tag.

DKIM (Domain Keys Identified Mail)

Before the ACL Configuration, place the following:

#  # DKIM macros
#  # get the sender domain from the outgoing mail
  SENDER_DOMAIN = ${if def:h_from:{${lc:${domain:${address:$h_from:}}}}{$qualify_domain}}
#  # the key file name will be based on the domain name in the From header
  DKIM_KEY_PATH = /etc/exim/keys
  DKIM_KEY_FILE = dkim_rsa.private

Put the following under the ACL Configuration.

# This access control list is used to process DKIM status.
acl_check_dkim:

  # Skip DKIM checks for all authenticated connections (probably MUAs)
  accept
          authenticated = *

  # Record the current timestamp, in order to delay crappy senders
  warn
          set acl_m0  = $tod_epoch

  # Warn no DKIM
  warn
          dkim_status = none
          set acl_c4  = X-DKIM-Warning: No signature found

  # RFC 8301 requires 'permanently failed evaluation' for DKIM signatures signed with 'historic algorithms (currently, rsa-sha1)'
  # @SEE: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-dkim_and_spf.html
  warn
          condition              = ${if !def:acl_c4 {true}{false} }
          condition              = ${if eq {$dkim_verify_status}{pass} }
          condition              = ${if eq {${length_3:$dkim_algo} }{rsa} }
          condition              = ${if or { {eq {$dkim_algo}{rsa-sha1} } \
                                    {< {$dkim_key_length}{1024} } } }
          set acl_c4             = X-DKIM-Warning: forced DKIM failure (weak hash or short key)
          set dkim_verify_status = fail
          set dkim_verify_reason = hash too weak or key too short

  # RFC6376 requires that verification fail if the From: header is not included in the signature
  # @SEE: https://www.exim.org/exim-html-current/doc/html/spec_html/ch-dkim_and_spf.html
  warn
          condition   = ${if !def:acl_c4 {true}{false} }
          condition   = ${if !inlisti{from}{$dkim_headernames}{true}{false} }
          set acl_c4  = X-DKIM-Warning: From: header not included in the \
                        signature, this defies the purpose of DKIM

  # Warn invalid or failed signatures
  warn
          condition   = ${if !def:acl_c4 {true}{false} }
          dkim_status = fail:invalid
          set acl_c4  = X-DKIM-Warning: verifying signature of $dkim_cur_signer \
                        failed for $sender_address because $dkim_verify_reason

  # Add a DKIM-Received: line to the message header (regardless of DKIM status)
  warn
          add_header  = Received-DKIM: $dkim_verify_status ${if \
                        def:dkim_cur_signer {($dkim_cur_signer with \
                        $dkim_algo for $dkim_headernames)} }

  # Set up for finalisation: add header and write to log
  warn
          condition   = ${if def:acl_c4 {true}{false} }
          add_header  = $acl_c4
          logwrite    = $acl_c4

accept

Again a TXT record needs to be defined.

Host nameTypeTTLData
<selector>._domainkey.example.comTXT1 hour"v=DKIM1; k=rsa; p="encrypted rsa key"

To enable DKIM-validating mail servers to validate our digital signatures, the public key from the DKIM key pair generated earlier has to be published in the zone file of the signing domain. The first step is to generate the public key from the DKIM key file:

  [root@system keys]# openssl rsa -in dkim_rsa.private -out /dev/stdout -pubout -outform PEM
  writing RSA key
  -----BEGIN PUBLIC KEY-----
  MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxMUk9Ac+aZVcqPkgSPny
  UOkWGrIvXcMJvUHjObpWlMNix3D74hE4KZ+Z18ZvOCUlUQGftzv0MJND/S4kXMlJ
  xuoxNMCKGozD/O71Rblz7RDUHxrhud2rjtSmXdmDHpH713djNiIxxZgeEeNBzfX3
  UGdCJlRMVQJXUcEozqgI5BmUTsdYtrb2Trr99IZtgaLEI92yXVdholtIyt83gnhA
  YLnvAzOQRV4zE/eBB/pfpbFrkPh1uQQxVIBi0pARj3xk9B8yXiCXUX+gyyBrw3zi
  /rnXFDe0ORjtDo/3WsSrwaivJ6KjywauYgnwYAx1eNyBGnPquVR6d8OlI15YIXy+
  1wIDAQAB
  -----END PUBLIC KEY-----

The public key can be inserted directly into a DKIM record as follows:

  dkim202205615._domainkey.example.com.    3600 TXT (
    "v=DKIM1; p="
    "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxMUk9Ac+aZVcqPkgSPny"
    "UOkWGrIvXcMJvUHjObpWlMNix3D74hE4KZ+Z18ZvOCUlUQGftzv0MJND/S4kXMlJ"
    "xuoxNMCKGozD/O71Rblz7RDUHxrhud2rjtSmXdmDHpH713djNiIxxZgeEeNBzfX3"
    "UGdCJlRMVQJXUcEozqgI5BmUTsdYtrb2Trr99IZtgaLEI92yXVdholtIyt83gnhA"
    "YLnvAzOQRV4zE/eBB/pfpbFrkPh1uQQxVIBi0pARj3xk9B8yXiCXUX+gyyBrw3zi"
    "/rnXFDe0ORjtDo/3WsSrwaivJ6KjywauYgnwYAx1eNyBGnPquVR6d8OlI15YIXy+"
    "1wIDAQAB")

Note the 'dkim20220615': that is the 'selector', which specifies the key pair used for signing. As you'll see shortly, the selector is also included in the 'DKIM Signature' header, so that when the receiving mail server follows the validation procedure, it knows exactly which public key to request from the DNS.

DMARC (Domain-based Message Authentication, Reporting and Conformance)

Put the following before the ACL Configuration.

# DMARC
  dmarc_tld_file=/usr/share/publicsuffix/public_suffix_list.dat
  dmarc_history_file=/var/spool/exim/opendmarc/history.dat
  dmarc_forensic_sender=postmaster@example.com

Put the following under the ACL Configuration.

acl_check_data:
# DMARC
  warn    dmarc_status = quarantine
          !authenticated = *
          log_message = Message from $dmarc_used_domain failed sender's DMARC policy; quarantine
          #control = dmarc_enable_forensic
          set acl_m_quarantine = 1
          # this variable to use in a router/transport
  deny    dmarc_status = reject
          !authenticated = *
          message = Message from $dmarc_used_domain failed sender's DMARC policy; reject
          #control = dmarc_enable_forensic
  warn    add_header = :at_start: ${authresults {$primary_hostname}}

You'll also need to generate the key pair.
The DKIM key pair is generated as follows:

  mkdir /etc/exim/keys/
  cd /etc/exim/keys/
  openssl genrsa -out dkim_rsa.private 2048

The new file 'dkim_rsa.private' contains the private key, which has to be kept secret. It's therefore important to ensure that the key file access rights provide appropriate security:

  chmod 640 dkim_rsa.private
  chown root:exim dkim_rsa.private

Although generating a longer key (4096 bits, rather than 2048 bits) is an option, DKIM signatures remain valid for relatively short periods. They are, after all, used exclusively for delivering messages, which, even in the worst-case scenario, only takes a few days. Restricting the key length to 2048 bits allows DNS traffic to go via the efficient UDP protocol, whereas it would be necessary to switch to the more onerous TCP protocol if longer keys were used.

As usual, you will need to submit a DMARC record to DNS:

Host nameTypeTTLData
_dmarc.example.comTXT6 hours"v=DMARC1;p=reject;rua=mailto:example@example.com;ruf=mailto:example@example.com;fo=1;aspf=r;adkim=r;"

I also added the following to the Transports section of exim.conf.

quarantine_delivery:
  driver = appendfile
  directory = /home/$local_part_data/Maildir/.INBOX.quarantine
  maildir_format
  delivery_date_add
  envelope_to_add
  return_path_add

Make sure the directory exists on the mail server.

I also have a cron setup to download the dat file (referenced above):

# DMARC
03 5 * * 1 cd /usr/share/publicsuffix && wget -c https://publicsuffix.org/list/public_suffix_list.dat

MTA-STS (Mail Transfer Agent Strict Transport Security)

This is just adding 2 TXT entries into DNS.

Host nameTypeTTLData
_mta-sts.exmaple.comTXT1 hour"v=STSv1; id=0002"
_smtp._tls.example.comTXT1 hour"v=TLSRPTv1;rua=mailto:example@example.com"

More info can on this can be found here (yes it's the UK gov)