58. DKIM, SPF, SRS and DMARC
Author: The Exim Project. <https://www.exim.org/>
Description: Exim is a message transfer agent (MTA) developed at the University of Cambridge for use on Unix systems connected to the Internet.
Keywords: exim,smtp,mta,email

# Exim Internet Mailer

<-previous next->

## Chapter 58 - DKIM, SPF, SRS and DMARC

### 1. DKIM (DomainKeys Identified Mail)

DKIM is a mechanism by which messages sent by some entity can be provably linked to a domain which that entity controls. It permits reputation to be tracked on a per-domain basis, rather than merely upon source IP address. DKIM is documented in RFC 6376.

As DKIM relies on the message being unchanged in transit, messages handled by a mailing-list (which traditionally adds to the message) will not match any original DKIM signature.

DKIM support is compiled into Exim by default if TLS support is present. It can be disabled by setting DISABLE_DKIM=yes in Local/Makefile.

Exim’s DKIM implementation allows for

1. Signing outgoing messages: This function is implemented in the SMTP transport. It can co-exist with all other Exim features (including transport filters) except cutthrough delivery. However, signing options may not depend on headers modified by routers, the transport or a transport filter.

2. Verifying signatures in incoming messages: This is implemented by an additional ACL (acl_smtp_dkim), which can be called several times per message, with different signature contexts.

In typical Exim style, the verification implementation does not include any default "policy". Instead it enables you to build your own policy using Exim’s standard controls.

Please note that verification of DKIM signatures in incoming mail is turned on by default for logging (in the <= line) purposes.

Additional log detail can be enabled using the dkim_verbose log_selector. When set, for each signature in incoming email, exim will log a line displaying the most important signature details, and the signature status. Here is an example (with line-breaks added for clarity):

    2009-09-09 10:22:28 1MlIRf-0003LU-U3 DKIM:
        d=facebookmail.com s=q1-2009b
        c=relaxed/relaxed a=rsa-sha1
        i=@facebookmail.com t=1252484542 [verification succeeded]

You might want to turn off DKIM verification processing entirely for internal or relay mail sources. To do that, set the dkim_disable_verify ACL control modifier. This should typically be done in the RCPT ACL, at points where you accept mail from relay sources (internal hosts or authenticated senders).

#### 1.1. Signing outgoing messages

For signing to be usable you must have published a DKIM record in DNS. Note that RFC 8301 (which does not cover EC keys) says:

    rsa-sha1 MUST NOT be used for signing or verifying.

    Signers MUST use RSA keys of at least 1024 bits for all keys.
    Signers SHOULD use RSA keys of at least 2048 bits.

Note also that the key content (the ’p=’ field) in the DNS record is different between RSA and EC keys; for the former it is the base64 of the ASN.1 for the RSA public key (equivalent to the private-key .pem with the header/trailer stripped) but for EC keys it is the base64 of the pure key; no ASN.1 wrapping.

Signing is enabled by setting private options on the SMTP transport. These options take (expandable) strings as arguments.

dkim_domain | Use: smtp | Type: string list† | Default: unset

The domain(s) you want to sign with. After expansion, this can be a list. Each element in turn, lowercased, is put into the $dkim_domain expansion variable while expanding the remaining signing options. If it is empty after expansion, DKIM signing is not done, and no error will result even if dkim_strict is set.

dkim_selector | Use: smtp | Type: string list† | Default: unset

This sets the key selector string. After expansion, which can use $dkim_domain, this can be a list. Each element in turn is put in the expansion variable $dkim_selector which may be used in the dkim_private_key option along with $dkim_domain. If the option is empty after expansion, DKIM signing is not done for this domain, and no error will result even if dkim_strict is set.

To do, for example, dual-signing with RSA and EC keys this could be be used:

    dkim_selector = ec_sel : rsa_sel
    dkim_private_key = KEYS_DIR/$dkim_selector

dkim_private_key | Use: smtp | Type: string† | Default: unset

This sets the private key to use. You can use the $dkim_domain and $dkim_selector expansion variables to determine the private key to use. The result can either

- be a valid RSA private key in ASCII armor (.pem file), including line breaks

- with GnuTLS 3.6.0 or OpenSSL 1.1.1 or later, be a valid Ed25519 private key (same format as above)

- start with a slash, in which case it is treated as a file that contains the private key

- be "0", "false" or the empty string, in which case the message will not be signed. This case will not result in an error, even if dkim_strict is set.

To generate keys under OpenSSL:

    openssl genrsa -out dkim_rsa.private 2048
    openssl rsa -in dkim_rsa.private -out /dev/stdout -pubout -outform PEM

The result file from the first command should be retained, permissions set so that Exim can read it, and this option set to use it. Take the base-64 lines from the output of the second command, concatenated, for the DNS TXT record. See section 3.6 of RFC 6376 for the record specification.

Under GnuTLS:

    certtool --generate-privkey --rsa --bits=2048 --password='' -8 --outfile=dkim_rsa.private
    certtool --load-privkey=dkim_rsa.private --pubkey-info

Note that RFC 8301 says:

    Signers MUST use RSA keys of at least 1024 bits for all keys.
    Signers SHOULD use RSA keys of at least 2048 bits.

EC keys for DKIM are defined by RFC 8463. They are considerably smaller than RSA keys for equivalent protection. As they are a recent development, users should consider dual-signing (by setting a list of selectors, and an expansion for this option) for some transition period. The "_CRYPTO_SIGN_ED25519" macro will be defined if support is present for EC keys.

OpenSSL 1.1.1 and GnuTLS 3.6.0 can create Ed25519 private keys:

    openssl genpkey -algorithm ed25519 -out dkim_ed25519.private
    certtool --generate-privkey --key-type=ed25519 --outfile=dkim_ed25519.private

To produce the required public key value for a DNS record:

    openssl pkey -outform DER -pubout -in dkim_ed25519.private | tail -c +13 | base64
    certtool --load-privkey=dkim_ed25519.private --pubkey-info --outder | tail -c +13 | base64

Exim also supports an alternate format of Ed25519 keys in DNS which was a candidate during development of the standard, but not adopted. A future release will probably drop that support.

dkim_hash | Use: smtp | Type: string† | Default: sha256

Can be set to any one of the supported hash methods, which are:

- `sha1` – should not be used, is old and insecure

- `sha256` – the default

- `sha512` – possibly more secure but less well supported

Note that RFC 8301 says:

    rsa-sha1 MUST NOT be used for signing or verifying.

dkim_identity | Use: smtp | Type: string† | Default: unset

If set after expansion, the value is used to set an "i=" tag in the signing header. The DKIM standards restrict the permissible syntax of this optional tag to a mail address, with possibly-empty local part, an @, and a domain identical to or subdomain of the "d=" tag value. Note that Exim does not check the value.

dkim_canon | Use: smtp | Type: string† | Default: unset

This option sets the canonicalization method used when signing a message. The DKIM RFC currently supports two methods: "simple" and "relaxed". The option defaults to "relaxed" when unset. Note: the current implementation only supports signing with the same canonicalization method for both headers and body.

dkim_strict | Use: smtp | Type: string† | Default: unset

This option defines how Exim behaves when signing a message that should be signed fails for some reason. When the expansion evaluates to either “1” or “true”, Exim will defer. Otherwise Exim will send the message unsigned. You can use the $dkim_domain and $dkim_selector expansion variables here.

dkim_sign_headers | Use: smtp | Type: string† | Default: see below

If set, this option must expand to a colon-separated list of header names. Headers with these names, or the absence of such a header, will be included in the message signature. When unspecified, the header names listed in RFC 4871 will be used, whether or not each header is present in the message. The default list is available for the expansion in the macro “_DKIM_SIGN_HEADERS” and an oversigning variant is in “_DKIM_OVERSIGN_HEADERS”.

If a name is repeated, multiple headers by that name (or the absence thereof) will be signed. The textually later headers in the headers part of the message are signed first, if there are multiples.

A name can be prefixed with either an “=” or a “+” character. If an “=” prefix is used, all headers that are present with this name will be signed. If a “+” prefix if used, all headers that are present with this name will be signed, and one signature added for a missing header with the name will be appended.

dkim_timestamps | Use: smtp | Type: integer† | Default: unset

This option controls the inclusion of timestamp information in the signature. If not set, no such information will be included. Otherwise, must be an unsigned number giving an offset in seconds from the current time for the expiry tag (e.g. 1209600 for two weeks); both creation (t=) and expiry (x=) tags will be included unless the offset is 0 (no expiry).

RFC 6376 lists these tags as RECOMMENDED.

#### 1.2. Verifying DKIM signatures in incoming mail

Verification of DKIM signatures in SMTP incoming email is done for all messages for which an ACL control dkim_disable_verify has not been set.

Individual classes of DKIM signature algorithm can be ignored by changing the main options dkim_verify_hashes or dkim_verify_keytypes. The dkim_verify_minimal option can be set to cease verification processing for a message once the first passing signature is found.

Performing verification sets up information used by the authresults expansion item.

For most purposes the default option settings suffice and the remainder of this section can be ignored.

The results of verification are made available to the acl_smtp_dkim ACL, which (for complex needs) can examine and modify them. A missing ACL definition defaults to accept. By default, the ACL is called once for each syntactically(!) correct signature in the incoming message. If any ACL call does not accept, the message is not accepted. If a cutthrough delivery was in progress for the message, that is summarily dropped (having wasted the transmission effort).

To evaluate the verification result in the ACL a large number of expansion variables containing the signature status and its details are set up during the runtime of the ACL.

Calling the ACL only for existing signatures is not sufficient to build more advanced policies. For that reason, the main option dkim_verify_signers, and an expansion variable $dkim_signers exist.

The main option dkim_verify_signers can be set to a colon-separated list of DKIM domains or identities for which the ACL acl_smtp_dkim is called. It is expanded when the message has been received. At this point, the expansion variable $dkim_signers already contains a colon-separated list of signer domains and identities for the message. When dkim_verify_signers is not specified in the main configuration, it defaults as:

    dkim_verify_signers = $dkim_signers

This leads to the default behaviour of calling acl_smtp_dkim for each DKIM signature in the message. Current DKIM verifiers may want to explicitly call the ACL for known domains or identities. This would be achieved as follows:

    dkim_verify_signers = paypal.com:ebay.com:$dkim_signers

This would result in acl_smtp_dkim always being called for "paypal.com" and "ebay.com", plus all domains and identities that have signatures in the message. You can also be more creative in constructing your policy. For example:

    dkim_verify_signers = $sender_address_domain:$dkim_signers

If a domain or identity is listed several times in the (expanded) value of dkim_verify_signers, the ACL is only called once for that domain or identity.

Note that if the option is set using untrustworthy data (such as the From: header) care should be taken to force lowercase for domains and for the domain part if identities. The default setting can be regarded as trustworthy in this respect.

If multiple signatures match a domain (or identity), the ACL is called once for each matching signature.

Inside the DKIM ACL, the following expansion variables are available (from most to least important):

$dkim_cur_signer

  The signer that is being evaluated in this ACL run. This can be a domain or an identity. This is one of the list items from the expanded main option dkim_verify_signers (see above).

$dkim_verify_status

  So long as a DKIM ACL is defined (it need do no more than accept, which is the default), after all the DKIM ACL runs have completed, the value becomes a colon-separated list of the values after each run. The value is maintained for the MIME, PRDR and DATA ACLs.

  Within the DKIM ACL, a string describing the general status of the signature. One of

  - none: There is no signature in the message for the current domain or identity (as reflected by $dkim_cur_signer).

  - invalid: The signature could not be verified due to a processing error. More detail is available in $dkim_verify_reason.

  - fail: Verification of the signature failed. More detail is available in $dkim_verify_reason.

  - pass: The signature passed verification. It is valid.

  This variable can be overwritten using an ACL ’set’ modifier. This might, for instance, be done to enforce a policy restriction on hash-method or key-size:

        warn 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}}}}
             logwrite        = NOTE: forcing DKIM verify fail (was pass)
             set dkim_verify_status = fail
             set dkim_verify_reason = hash too weak or key too short

$dkim_verify_reason

  A string giving a little bit more detail when $dkim_verify_status is either "fail" or "invalid". One of

  - pubkey_unavailable (when $dkim_verify_status="invalid"): The public key for the domain could not be retrieved. This may be a temporary problem.

  - pubkey_syntax (when $dkim_verify_status="invalid"): The public key record for the domain is syntactically invalid.

  - bodyhash_mismatch (when $dkim_verify_status="fail"): The calculated body hash does not match the one specified in the signature header. This means that the message body was modified in transit.

  - signature_incorrect (when $dkim_verify_status="fail"): The signature could not be verified. This may mean that headers were modified, re-written or otherwise changed in a way which is incompatible with DKIM verification. It may of course also mean that the signature is forged.

  This variable can be overwritten, with any value, using an ACL ’set’ modifier.

$dkim_domain

  The signing domain. IMPORTANT: This variable is only populated if there is an actual signature in the message for the current domain or identity (as reflected by $dkim_cur_signer).

$dkim_identity

  The signing identity, if present. IMPORTANT: This variable is only populated if there is an actual signature in the message for the current domain or identity (as reflected by $dkim_cur_signer).

$dkim_selector

  The key record selector string.

$dkim_algo

  The algorithm used. One of ’rsa-sha1’ or ’rsa-sha256’. If running under GnuTLS 3.6.0 or OpenSSL 1.1.1 or later, may also be ’ed25519-sha256’. The "_CRYPTO_SIGN_ED25519" macro will be defined if support is present for EC keys.

  Note that RFC 8301 says:

      rsa-sha1 MUST NOT be used for signing or verifying.

      DKIM signatures identified as having been signed with historic
      algorithms (currently, rsa-sha1) have permanently failed evaluation

  To enforce this you must either have a DKIM ACL which checks this variable and overwrites the $dkim_verify_status variable as discussed above, or have set the main option dkim_verify_hashes to exclude processing of such signatures.

$dkim_canon_body

  The body canonicalization method. One of ’relaxed’ or ’simple’.

$dkim_canon_headers

  The header canonicalization method. One of ’relaxed’ or ’simple’.

$dkim_copiedheaders

  A transcript of headers and their values which are included in the signature (copied from the ’z=’ tag of the signature). Note that RFC 6376 requires that verification fail if the From: header is not included in the signature. Exim does not enforce this; sites wishing strict enforcement should code the check explicitly.

$dkim_bodylength

  The number of signed body bytes. If zero ("0"), the body is unsigned. If no limit was set by the signer, "9999999999999" is returned. This makes sure that this variable always expands to an integer value. Note: The presence of the signature tag specifying a signing body length is one possible route to spoofing of valid DKIM signatures. A paranoid implementation might wish to regard signature where this variable shows less than the "no limit" return as being invalid.

$dkim_created

  UNIX timestamp reflecting the date and time when the signature was created. When this was not specified by the signer, "0" is returned.

$dkim_expires

  UNIX timestamp reflecting the date and time when the signer wants the signature to be treated as "expired". When this was not specified by the signer, "9999999999999" is returned. This makes it possible to do useful integer size comparisons against this value. Note that Exim does not check this value.

$dkim_headernames

  A colon-separated list of names of headers included in the signature.

$dkim_key_testing

  "1" if the key record has the "testing" flag set, "0" if not.

$dkim_key_nosubdomains

  "1" if the key record forbids subdomaining, "0" otherwise.

$dkim_key_srvtype

  Service type (tag s=) from the key record. Defaults to "*" if not specified in the key record.

$dkim_key_granularity

  Key granularity (tag g=) from the key record. Defaults to "*" if not specified in the key record.

$dkim_key_notes

  Notes from the key record (tag n=).

$dkim_key_length

  Number of bits in the key. Valid only once the key is loaded, which is at the time the header signature is verified, which is after the body hash is.

  Note that RFC 8301 says:

      Verifiers MUST NOT consider signatures using RSA keys of
      less than 1024 bits as valid signatures.

  This is enforced by the default setting for the dkim_verify_min_keysizes option.

In addition, two ACL conditions are provided:

dkim_signers

  ACL condition that checks a colon-separated list of domains or identities for a match against the domain or identity that the ACL is currently verifying (reflected by $dkim_cur_signer). This condition is only usable in a DKIM ACL. This is typically used to restrict an ACL verb to a group of domains or identities. For example:

      # Warn when Mail purportedly from GMail has no gmail signature
      warn sender_domains = gmail.com
           dkim_signers = gmail.com
           dkim_status = none
           log_message = GMail sender without gmail.com DKIM signature

  Note that the above does not check for a total lack of DKIM signing; for that check for empty $h_DKIM-Signature: in the data ACL.

dkim_status

  ACL condition that checks a colon-separated list of possible DKIM verification results against the actual result of verification, given by $dkim_verify_status if that is non-empty or "none" if empty. This condition may be used in DKIM, MIME, PRDR and DATA ACLs.

  A basic verification might be:

      deny !dkim_status = pass:none:invalid

  A more complex use could be to restrict an ACL verb to a list of verification outcomes, for example:

      deny sender_domains = paypal.com:paypal.de
           dkim_signers = paypal.com:paypal.de
           dkim_status = none:invalid:fail
           message = Mail from Paypal with invalid/missing signature

  The possible status keywords are: ’none’,’invalid’,’fail’ and ’pass’. Please see the documentation of the $dkim_verify_status expansion variable above for more information of what they mean.

  The condition is true if the status (or any of the list of status values) is any one of the supplied list.

### 2. SPF (Sender Policy Framework)

SPF is a mechanism whereby a domain may assert which IP addresses may transmit messages with its domain in the envelope from, documented by RFC 7208. For more information on SPF see http://www.open-spf.org, a static copy of the http://openspf.org.

Messages sent by a system not authorised will fail checking of such assertions. This includes retransmissions done by traditional forwarders.

SPF verification support is built into Exim if SUPPORT_SPF=yes is set in Local/Makefile. The support uses the libspf2 library https://www.libspf2.org/.

The support can be built as a dynamic-load module if desired; see the comments in that Makefile.

There is no Exim involvement in the transmission of messages; publishing certain DNS records is all that is required.

For verification, an ACL condition and an expansion lookup are provided. Performing verification sets up information used by the authresults expansion item.

The ACL condition "spf" can be used at or after the MAIL ACL. It takes as an argument a list of strings giving the outcome of the SPF check, and will succeed for any matching outcome. Valid strings are:

pass

  The SPF check passed, the sending host is positively verified by SPF.

fail

  The SPF check failed, the sending host is NOT allowed to send mail for the domain in the envelope-from address.

softfail

  The SPF check failed, but the queried domain can’t absolutely confirm that this is a forgery.

none

  The queried domain does not publish SPF records.

neutral

  The SPF check returned a "neutral" state. This means the queried domain has published a SPF record, but wants to allow outside servers to send mail under its domain as well. This should be treated like "none".

permerror

  This indicates a syntax error in the SPF record of the queried domain. You may deny messages when this occurs.

temperror

  This indicates a temporary error during all processing, including Exim’s SPF processing. You may defer messages when this occurs.

invalid

  There was an error during processing of the SPF lookup

You can prefix each string with an exclamation mark to invert its meaning, for example "!fail" will match all results but "fail". The string list is evaluated left-to-right, in a short-circuit fashion.

Example:

    deny spf = fail
         message = $sender_host_address is not allowed to send mail from \
                   ${if def:sender_address_domain \
                        {$sender_address_domain}{$sender_helo_name}}.  \
                   Please see http://www.open-spf.org/Why;\
                   identity=${if def:sender_address_domain \
                                 {$sender_address}{$sender_helo_name}};\
                   ip=$sender_host_address

Note: The above mentioned URL may not be as helpful as expected. You are encouraged to replace the link with a link to a site with more explanations.

When the spf condition has run, it sets up several expansion variables:

$spf_header_comment

  This contains a human-readable string describing the outcome of the SPF check. You can add it to a custom header or use it for logging purposes.

$spf_received

  This contains a complete Received-SPF: header (name and content) that can be added to the message. Please note that according to the SPF draft, this header must be added at the top of the header list, i.e. with

      add_header = :at_start:$spf_received

  See section 44.15 for further details.

  Note: in case of "Best-guess" (see below), the convention is to put this string in a header called X-SPF-Guess: instead.

$spf_result

  This contains the outcome of the SPF check in string form, currently one of pass, fail, softfail, none, neutral, permerror, temperror, or “(invalid)”.

$spf_result_guessed

  This boolean is true only if a best-guess operation was used and required in order to obtain a result.

$spf_smtp_comment

  This contains a string that can be used in a SMTP response to the calling party. Useful for "fail". The string is generated by the SPF library from the template configured in the main config option spf_smtp_comment_template.

In addition to SPF, you can also perform checks for so-called "Best-guess". Strictly speaking, "Best-guess" is not standard SPF, but it is supported by the same framework that enables SPF capability. Refer to http://www.open-spf.org/FAQ/Best_guess_record for a description of what it means.

To access this feature, simply use the spf_guess condition in place of the spf one. For example:

    deny spf_guess = fail
         message = $sender_host_address doesn't look trustworthy to me

In case you decide to reject messages based on this check, you should note that although it uses the same framework, "Best-guess" is not SPF, and therefore you should not mention SPF at all in your reject message.

When the spf_guess condition has run, it sets up the same expansion variables as when spf condition is run, described above.

Additionally, since Best-guess is not standardized, you may redefine what "Best-guess" means to you by redefining the main configuration spf_guess option. For example, the following:

    spf_guess = v=spf1 a/16 mx/16 ptr ?all

would relax host matching rules to a broader network range.

A lookup expansion is also available. It takes an email address as the key and an IP address (v4 or v6) as the database:

      ${lookup {username@domain} spf {ip.ip.ip.ip}}

The lookup will return the same result strings as can appear in $spf_result (pass,fail,softfail,neutral,none,err_perm,err_temp).

#### 2.1. SRS (Sender Rewriting Scheme)

SRS can be used to modify sender addresses when forwarding so that SPF verification does not object to them. It can also be used to identify a received bounce message as likely (or not) having been trigged by a message from the local system, and for identifying dead addresses in mailing lists. It is one implementation of a VERP (Variable Envelope Return Path) method.

SRS operates by encoding the original envelope sender in a new sender local part and using a domain run by the forwarding site as the new domain for the sender. Any DSN message should be returned to this new sender at the forwarding site, which can extract the original sender from the coded local part and forward the DSN to the originator.

This is a way of avoiding the breakage that SPF does to forwarding. The constructed local-part will be longer than the original, leading to possible problems with very long addresses. The changing of the sender address also hinders the tracing of mail problems.

Exim can be built to include native SRS support. To do this SUPPORT_SRS=yes must be defined in Local/Makefile. If this has been done, the macros _HAVE_SRS and _HAVE_NATIVE_SRS will be defined. The support is limited to SRS0-encoding; SRS1 is not supported.

To encode an address use this expansion item:

${srs_encode {<secret>}{<return path>}{<original domain>}}

  The first argument should be a secret known and used by all systems handling the recipient domain for the original message. There is no need to periodically change this key; a timestamp is also encoded. The second argument should be given as the envelope sender address before this encoding operation. If this value is empty the the expansion result will be empty. The third argument should be the recipient domain of the message when it arrived at this system. All arguments are expanded before use.

  The result of the expansion is the replacement envelope-from (return path) to be used.

To decode an address use this expansion condition:

inbound_srs {<local part>}{<secret>}

  The first argument should be the recipient local part as it was received. The second argument is the site secret. Both arguments are expanded before use.

  If the messages is not for an SRS-encoded recipient the condition will return false. If it is, the condition will return true and the variable $srs_recipient will be set to the decoded (original) value.

  If the second argument is empty then the condition returns true if the first argument is in valid SRS formet, else false. The variable $srs_recipient is not set for this case.

Example usage:

      #macro
      SRS_SECRET = <pick something unique for your site for this. Use on all MXs.>

      #routers

      outbound:
        driver =    dnslookup
        # if outbound, and forwarding has been done, use an alternate transport
        domains =   ! +my_domains
        transport = ${if eq {$local_part@$domain} \
                            {$original_local_part@$original_domain} \
                         {remote_smtp} {remote_forwarded_smtp}}

      inbound_srs:
        driver =    redirect
        senders =   :
        domains =   +my_domains
        # detect inbound bounces which are SRS'd, and decode them
        condition = ${if inbound_srs {$local_part} {SRS_SECRET}}
        data =      $srs_recipient

      inbound_srs_failure:
        driver =    redirect
        senders =   :
        domains =   +my_domains
        # detect inbound bounces which look SRS'd but are invalid
        condition = ${if inbound_srs {$local_part} {}}
        allow_fail
        data =      :fail: Invalid SRS recipient address

      #... further routers here get inbound_srs-redirected recipients
      # and any that were not SRS'd

      # transport; should look like the non-forward outbound
      # one, plus the max_rcpt and return_path options
      remote_forwarded_smtp:
        driver =              smtp
        # single-recipient so that $original_domain is valid
        max_rcpt =            1
        # modify the envelope from, for mails that we forward
        return_path =         ${srs_encode {SRS_SECRET} {$return_path} {$original_domain}}

### 3. DMARC

DMARC combines feedback from SPF, DKIM, and header From: in order to attempt to provide better indicators of the authenticity of an email. This document does not explain the fundamentals; you should read and understand how it works by visiting the DMARC website.

If Exim is built with DMARC support, the libopendmarc library is used.

For building Exim yourself, obtain the library from sourceforge to obtain a copy, or find it in your favorite package repository. You will need to attend to the local/Makefile feature SUPPORT_DMARC and the associated LDFLAGS addition. This description assumes that headers will be in /usr/local/include, and that the libraries are in /usr/local/lib.

#### 3.1. Configuration

There are three main-configuration options:

The dmarc_tld_file option defines the location of a text file of valid top level domains the opendmarc library uses during domain parsing. Maintained by Mozilla, the most current version can be downloaded from a link at https://publicsuffix.org/list/public_suffix_list.dat. See also the util/renew-opendmarc-tlds.sh script. The default for the option is unset.

It is expanded before use. If not set (or empty after expansion), DMARC processing is disabled.

The dmarc_history_file option defines the location of a file to log results of dmarc verification on inbound emails. The contents are importable by the opendmarc tools which will manage the data, send out DMARC reports, and expire the data. Make sure the directory of this file is writable by the user exim runs as. The default for the option is unset.

It is expanded before use. If not set (or empty after expansion), no history is written.

The dmarc_forensic_sender option defines an alternate email address to use when sending a forensic report detailing alignment failures if a sender domain’s dmarc record specifies it and you have configured Exim to send them. If set, this is expanded and used for the From: header line; the address is extracted from it and used for the envelope from. If not set (the default), the From: header is expanded from the dsn_from option, and <> is used for the envelope from.

#### 3.2. Controls

By default, the DMARC processing will run for any remote, non-authenticated user. It makes sense to only verify DMARC status of messages coming from remote, untrusted sources. You can use standard conditions such as hosts, senders, etc, to decide that DMARC verification should *not* be performed for them and disable DMARC with an ACL control modifier:

      control = dmarc_disable_verify

A DMARC record can also specify a "forensic address", which gives exim an email address to submit reports about failed alignment. Exim does not do this by default because in certain conditions it results in unintended information leakage (what lists a user might be subscribed to, etc). You must configure exim to submit forensic reports to the owner of the domain. If the DMARC record contains a forensic address and you specify the control statement below, then exim will send these forensic emails. It is also advised that you configure a dmarc_forensic_sender because the default sender address construction might be inadequate.

      control = dmarc_enable_forensic

(AGAIN: You can choose not to send these forensic reports by simply not putting the dmarc_enable_forensic control line at any point in your exim config. If you don’t tell exim to send them, it will not send them.)

There are no options to either control. Both must appear before the DATA acl.

#### 3.3. ACL

DMARC checks can be run on incoming SMTP messages by using the “dmarc_status” ACL condition in the DATA ACL. You are required to call the “spf” condition first in the ACLs, then the “dmarc_status” condition. Putting this condition in the ACLs is required in order for a DMARC check to actually occur. All of the variables are set up before the DATA ACL, but there is no actual DMARC check that occurs until a “dmarc_status” condition is encountered in the ACLs.

The “dmarc_status” condition takes a list of strings on its right-hand side. These strings describe recommended action based on the DMARC check. To understand what the policy recommendations mean, refer to the DMARC website above. Valid strings are:

accept | The DMARC check passed and the library recommends accepting the email
reject | The DMARC check failed and the library recommends rejecting the email
quarantine | The DMARC check failed and the library recommends keeping it for further inspection
none | The DMARC check passed and the library recommends no specific action, neutral
norecord | No policy section in the DMARC record for this RFC5322.From field
nofrom | Unable to determine the domain of the sender
temperror | Library error or dns error
off | The DMARC check was disabled for this email

You can prefix each string with an exclamation mark to invert its meaning, for example "!accept" will match all results but "accept". The string list is evaluated left-to-right in a short-circuit fashion. When a string matches the outcome of the DMARC check, the condition succeeds. If none of the listed strings matches the outcome of the DMARC check, the condition fails.

Of course, you can also use any other lookup method that Exim supports, including LDAP, Postgres, MySQL, etc, as long as the result is a list of colon-separated strings.

Performing the check sets up information used by the authresults expansion item.

Several expansion variables are set before the DATA ACL is processed, and you can use them in this ACL. The following expansion variables are available:

$dmarc_status

  A one word status indicating what the DMARC library thinks of the email. It is a combination of the results of DMARC record lookup and the SPF/DKIM/DMARC processing results (if a DMARC record was found). The actual policy declared in the DMARC record is in a separate expansion variable.

$dmarc_status_text

  Slightly longer, human readable status.

$dmarc_used_domain

  The domain which DMARC used to look up the DMARC policy record.

$dmarc_domain_policy

  The policy declared in the DMARC record. Valid values are "none", "reject" and "quarantine". It is blank when there is any error, including no DMARC record.

$dmarc_alignment_spf

  The result of the SPF alignment portion of the test status; "yes" or "no".

$dmarc_alignment_dkim

  The result of the DKIM alignment portion of the test status; "yes" or "no".

#### 3.4. Logging

By default, Exim’s DMARC configuration is intended to be non-intrusive and conservative. To facilitate this, Exim will not create any type of logging files without explicit configuration by you, the admin. Nor will Exim send out any emails/reports about DMARC issues without explicit configuration by you, the admin (other than typical bounce messages that may come about due to ACL processing or failure delivery issues).

In order to log statistics suitable to be imported by the opendmarc tools, you need to:

- Configure the global option dmarc_history_file

- Configure cron jobs to call the appropriate opendmarc history import scripts and truncating the dmarc_history_file

In order to send forensic reports, you need to:

- Configure the global option dmarc_forensic_sender

- Configure, somewhere before the DATA ACL, the control option to enable sending DMARC forensic reports

#### 3.5. Example

Example usage:

    (RCPT ACL)
      warn    domains        = +local_domains
              hosts          = +local_hosts
              control        = dmarc_disable_verify

      warn    !domains       = +screwed_up_dmarc_records
              control        = dmarc_enable_forensic

      warn    condition      = (lookup if destined to mailing list)
              set acl_m_mailing_list = 1

    (DATA ACL)
      warn    dmarc_status   = accept : none : off
              !authenticated = *
              log_message    = DMARC DEBUG: $dmarc_status $dmarc_used_domain

      warn    dmarc_status   = !accept
              !authenticated = *
              log_message    = DMARC DEBUG: '$dmarc_status' for $dmarc_used_domain

      warn    dmarc_status   = quarantine
              !authenticated = *
              set $acl_m_quarantine = 1
              # Do something in a transport with this flag variable

      deny    condition      = ${if eq{$dmarc_domain_policy}{reject}}
              condition      = ${if eq{$acl_m_mailing_list}{1}}
              message        = Messages from $dmarc_used_domain break mailing lists

      deny    dmarc_status   = reject
              !authenticated = *
              message        = Message from $dmarc_used_domain failed sender's DMARC policy, REJECT

      warn    add_header     = :at_start:${authresults {$primary_hostname}}

<-previous Table of Contents next->