I briefly mentioned using DKIM to verify an email’s sender in a previous blog post that described the steps I took to determine whether a suspicious email was legitimate or a phishing attempt. In this post, we will take a deeper dive into how organizations can help stop email spoofing using a combination of three (3) essential tools: Sender Policy Framework (SPF), DomainKeys Identified Mail (DKIM), and Domain-based Message Authentication, Reporting and Conformance (DMARC).
This blog post assumes familiarity with the SMTP protocol and basic email headers. The reader should review this primer on how email spoofing works if these are unfamiliar concepts.
Stopping SMTP Spoofing with SPF
SPF, defined in RFC 7208, is designed to prevent spoofing of SMTP sender information by checking to see that a particular mail server is authorized to send email for the domain in the email address provided in the SMTP MAIL FROM: command.
Every organization operating a legitimate mail server must do two (2) things for SPF to be effective:
- Set up their mail server(s) that receive inbound email from the Internet to check SPF records
- Maintain accurate SPF records in their DNS so that other organizations can perform SPF checks
SPF operates based on special DNS records, similar to the MX record used to identify an organization’s mail server(s). When a mail server receives an inbound email from the Internet, it will perform a DNS lookup on the sender’s domain to check for an SPF record and follow the rules it contains.
Organizations that want to prevent spoofing of emails from their domains must create a DNS record to contain the SPF rules. This must be a TXT record for a domain. This could be a second-level domain, e.g., example.com, or a subdomain, e.g., subdomain.example.com. Although an organization may only send email from one (1) domain, they should create SPF records for alternate domains and subdomains as well to keep attackers from using them to send spoofed emails that bypass the SPF record of the main domain.
This is an example of a minimal valid SPF DNS record:
All SPF records must start with a version tag v= that identifies the SPF version. Currently only SPF version 1 is supported so this tag will always be set to spf1.
The rest of the record may consist of one (1) or more rules, known as mechanisms. Mechanisms are checked from left to right and will stop on the first mechanism that is matched by the message, except for an INCLUDE mechanism as explained below.
The mechanisms include:
|A||• Matches if the sending mail server’s IP address is included in the DNS A or AAAA record for the SPF record’s domain name|
• Optionally, another domain can be provided, and the sending mail server’s IP address will be checked against A or AAAA records for that domain instead.
|EXISTS||• Matches if the provided domain name resolves to any address, even if it does not match the sending mail server’s IP address|
• This is typically used in macros and is generally not useful as a standalone rule.
|INCLUDE||• Matches if the SPF policy of another referenced domain passes (explained below)|
• Rule evaluation will continue if any result other than a PASS is returned from the referenced SPF policy
|IP4||Matches if the sending mail server’s IP address is in a provided IPv4 address range|
|IP6||Matches if the sending mail server’s IP address is in a provided IPv6 address range|
|MX||• Matches If the sending mail server’s IP address is in the MX records for the SPF record’s domain|
• Optionally, another domain can be provided, and the sending mail server’s IP address will be checked against MX records for that domain instead.
|PTR||• Matches if the DNS PTR record for the sending mail server’s IP address is in the SPF record’s domain and the DNS A or AAAA record received from the PTR lookup resolves to the sending mail server’s IP address|
• Optionally, another domain can be provided, and the sending mail server’s PTR and A or AAAA records will be checked for a match with that domain instead.
• This mechanism is deprecated and should not be used.
Each rule may indicate one (1) of four (4) return values, known as qualifiers, to provide if the rule matches:
- + PASS, allow the message
- ? NEUTRAL, treat the email as if an SPF record did not exist
- ~ SOFTFAIL, used for debugging
- Typically treated like a NEUTRAL but may be logged, tagged, or treated differently at the discretion of the receiving mail server
- – FAIL, reject the message
A mechanism without a qualifier will be treated as a PASS.
This is a more realistic example of an SPF record that demonstrates various mechanisms and qualifiers:
v=spf1 MX +a:mail.example.com -ip4:192.168.1.5 +ip4:192.168.1.0/24 ~ipv6 fdef:127e:2035:b7ab::/64 INCLUDE:example.com -all
This example record would be parsed as follows and would stop on the first rule that matched a specific message:
- PASS messages from any IP address listed in the domain’s MX records
- PASS messages from any IP address that mail.example.com resolves to
- FAIL messages from the IPv4 address 192.168.1.5
- PASS messages from the 192.168.1.0/24 IPv4 range
- SOFTFAIL messages from the fdef:127e:2035:b7ab::/64 IPv6 range
- PASS messages that pass the SPF policy of example.com
- FAIL all other messages
An SPF check will return NEUTRAL if none of the mechanisms match. This includes SPF records with no mechanisms provided at all, as in the minimal example shown earlier. This means there is an implicit ?all at the end of all SPF records, including blank records (such as our minimal valid SPF record shown above).
Most rulesets will have an -all rule at the end, similar to the catch-all rule on a firewall, so messages that don’t match any preceding rules are treated as a FAIL.
Records may also contain modifiers.
|redirect||• Checks the messages against the policy for another listed domain if all other mechanisms listed in the ruleset (if any) failed to match |
• The redirect is often used to point SPF records for subdomains and alternate domains to another domain so SPF can be managed through a single main record.
• A redirect is only processed after all other mechanisms, regardless of whether it appears before or after other mechanisms in the SPF record.
|exp||• Returns a custom explanation for messages that FAIL SPF checks |
• Explanation messages can use macros to provide detailed information on how a policy was failed that is beyond the scope of this post (RFC 7208 contains additional details).
Stopping Header Spoofing With DKIM
DKIM, defined in RFC 6376, can be used to detect spoofed sender information in message headers and verify the integrity of other parts of the message header and body.
DKIM operates by generating a digital signature for portions of the message body and headers to be protected and storing this digital signature in the message header. DKIM relies on DNS records for verification, much like SPF. Every organization operating a legitimate mail server must do three (3) things for DKIM to be effective:
- Set up mail server(s) that receive inbound email to check other organizations’ DKIM records
- Maintain accurate DKIM records in DNS so that other organizations can perform DKIM checks
- Set up mail servers(s) that send outbound email to create valid DKIM header signatures on each message
The first step in configuring DKIM is to generate a 1,024 or 2,048-bit RSA public/private key pair. The OpenDKIM package, or various other tools capable of generating RSA keys, can be used to create the key pair.
A DNS record must be created to contain the DKIM public key. This must be a TXT record for a subdomain named as follows:
- [domain] is the domain for which the rule should be enforced; this could be a second level domain, e.g., example.com, or a subdomain, e.g., subdomain.example.com
- [selector] is a unique identifier, i.e., an arbitrary made-up name for the key so that multiple keys issued for the same domain or subdomain can be distinguished
This is an example of a minimal valid DKIM record:
Note that everything after p= in this example would be the organization’s own public key. The string shown here is an invalid fictional public key and must be replaced with valid public key data generated as described above.
Various tags can be added to the DKIM DNS record to control how DKIM operates. The defined tags for a DKIM DNS record include:
|v=||DKIM key record version (optional, defaults to DKIM1, which is the only current supported version, must be the first tag if present)|
|h=||Colon-separated list of acceptable signing algorithms (optional, allows all algorithms by default) |
rsa-sha1 SHA-1 hashes signed with RSA are allowed
rsa-sha256 – SHA-256 hashes signed with RSA are allowed (preferred)
|k=||Key type (optional, defaults to rsa, which is the only currently supported key type)|
|n=||Notes for human administrators (optional, default is empty)|
|p=||Base64 encoded public key (required, an empty key means the key has been revoked)|
|s=||Limits the record to a specific service type (optional, defaults to *) |
email electronic mail
* Matches all service types
|t=||Used to set flags that control DKIM operation modes (optional, default is no flags) |
s The domain in an i= tag and d= tag in an email must match
y DKIM is in testing mode and should not be enforced
The organization’s mail server(s) will then need to be set up to add DKIM headers to each outbound message. Each type of mail server will have a different configuration process, but the goal will be to insert a valid DKIM header, signed by the DKIM private key, into each outbound message.
Much like the DNS record, there are various mandatory and optional tags that can be attached to the DKIM header. These indicate how the signature was generated and help with verification.
This is an example of a minimal valid DKIM email header:
DKIM-Signature: v=1; a=rsa-sha256; d=example.com; s=key1; h=From:Sender:Reply-To:Subject:Date:Message-Id:To:Cc; bh= ZjNiZmY1YjMwYzgxYmI1NTk4ZWY2OTA1N2RhODgzZjU4M2JmNjYwNA==; b= MDZlYTNiZjFlOWYxZjdjNGJjOGFlMWFhMjRiMTBhNDdjOGM1Mjc5ZA==;
The strings after the bh= and b= parameters will be generated automatically by the mail server based on the message headers and contents and will be unique for each message sent.
The defined tags for a DKIM header include:
|v=||DKIM version, currently only 1 is supported (required, must be the first tag)|
|a=||Algorithms used to generate the signature (required) |
rsa-sha1 Hashes use SHA-1, signed with RSA
rsa-sha256 Hashes use SHA-256, signed with RSA (preferred)
|b=||DKIM signature of the message body and headers (required)|
|bh=||Hash of the message body (required)|
|c=||Algorithm used to canonicalize the message before hashing (optional, defaults to simple/simple) |
simple/simple Both header and body canonicalized with the simple algorithm
simple/relaxed Headers canonicalized with the simple algorithm, body canonicalized with the relaxed algorithm
relaxed/simple Headers canonicalized with the relaxed algorithm, body canonicalized with the simple algorithm
relaxed/relaxed Both header and body canonicalized with the relaxed algorithm
|d=||DKIM signer key’s domain name (required)|
|h=||Headers included in the signature (required)|
|i=||Identity of the signer in email address format (optional, defaults to a blank address at the domain listed in d=)|
|l=||Length of the body to hash in octets (optional, hashes the entire body by default)|
|q=||Query method used to retrieve the public key (optional, defaults to dns/txt which is the only currently supported option)|
|s=||DKIM signer key’s selector (required)|
|t=||Timestamp when the message was signed in Unix Epoch seconds (recommended, defaults to an unknown time)|
|x=||Timestamp when the message will expire in Unix Epoch seconds (recommended, defaults to no expiration)|
|z=||List of header fields when the message was signed (optional, defaults to null)|
A receiving mail server configured to use DKIM will:
- Verify that the algorithm specified by a= in the DKIM header is allowed by h= in the DKIM DNS record
- Verify that the expiration time in x= hasn’t passed (if applicable)
- Verify that the signer’s domain in i= matches the domain in d= (if applicable)
- Run the first l= octets (if applicable) of the message body through the canonicalization algorithm indicated by c= to deal with any whitespace characters that might be altered as the message moves between mail servers
- Hash the canonicalized message body using the algorithm indicated in a=
- Verify the canonicalized and hashed body matches the value in bh=
- Use the method indicated in q= (if applicable) to retrieve the public key for the domain in d= matching the selector in s=
- Verify the canonicalized, hashed, and digitally signed body and headers indicated in h= can be verified using the retrieved public key
If all these checks pass, the recipient can be assured that the message (or the first l= octets of the message, if applicable) and headers included in h= haven’t been tampered with and were authorized to be sent by whoever controls the DKIM private key.
Bringing It All Together With DMARC
Both SPF and DKIM provide pass and fail results but don’t provide any indication of what to do with messages that fail. DMARC, defined in RFC 7489, allows the owner of a domain to publish instructions on what should be done with messages based on the results.
SPF and DKIM also operate independently of each other in two (2) separate parts of an email (SPF on the SMTP envelope and DKIM on the message contents). Including the From: header in the DKIM hash and signature is also optional. This creates the opportunity to create a message with a spoofed From: header by leveraging mismatched SPF, DKIM, and From: header domains. DMARC also addresses this loophole by providing instructions for checking to see if the domain used in the message’s From: header matches the domains used in the SPF checks and DKIM checks.
Like SPF and DKIM, every organization operating a legitimate mail server must do two (2) things for DMARC to be effective:
- Set up their mail server(s) that receive inbound email to check and enforce other organizations’ DMARC records
- Maintain accurate DMARC records in their DNS so that other organizations can perform DMARC checks and enforcement
DMARC also leverages DNS records to publish information, like SPF and DKIM. A DNS record must be created to contain the DMARC rule for each domain. This must be a TXT record for a subdomain named as follows:
Where [domain] is the domain for which the rule should be enforced, this could be a second level domain, e.g., example.com, or a subdomain, e.g., subdomain.example.com.
This is an example of a minimal valid DMARC record:
The defined tags for a DMARC DNS record include:
|v=||DMARC version, currently only DMARC1 is supported (required, must be the first tag)|
|adkim=||Check DKIM domain against header from: domain (optional, defaults to r) |
s Strict checking, the fully qualified domain names must match
r Relaxed checking, only the domain parts must match
|aspf=||Check SPF domain against header from: domain (optional, defaults to r) |
s Strict checking, the fully qualified domain names must match
r Relaxed checking, only the domain parts must match
|fo=||When to report failures (optional, defaults to 0) |
0 Report failure if all the DMARC mechanisms fail
1 Report failure if any of the DMARC mechanisms fail
d Report failure if DKIM failed
s Report failure if SPF failed
|p=||Policy to apply to the domain (required) |
none Allow emails to pass but generate reports
quarantine Treat messages as suspicious (how to deal with these messages is left up to the receiver)
reject Reject messages
|pct=||Percent of messages to reject, the next least restrictive policy is applied to remaining messages (optional, defaults to 100)|
|rf=||Format for failure reports sent to the URI in ruf= (optional, defaults to afrf, which is the only currently supported format)|
|ri=||Requested interval in seconds for sending aggregate reports to the URI in rua= (optional, defaults to 86400, which is one (1) day)|
|rua=||List of URIs to receive daily aggregate reports showing how many messages passed or failed checks (optional)|
|ruf=||List of URIs to receive immediate forensic reports with redacted copies of emails that failed checks (optional)|
|sp=||Policy to apply to subdomains (optional, p= policy will be applied to all subdomains if omitted) |
none Allow emails to pass but generate reports
quarantine Treat messages as suspicious (the interpretation of this is left up to the receiver)
reject Reject messages
A receiving mail server should:
- Check DNS for a DMARC1 record that matches the sender’s domain or subdomain
- Perform SPF and DKIM checks
- Check the FROM: header against the DKIM and SPF results as specified by adkim= and aspf=
- Apply the policy in p= or sp= (if it is a subdomain) to pct= percent of the messages that fail DKIM, SPF, adkim=, and adspf= checks
- Immediately generate and send a forensic report to the addresses listed in ruf= in the format specified by rf= for any messages that meet the conditions in f0=
- Generate and send an aggregate report to the addresses listed in rua= every ri= seconds
The astute reader will have noticed that this post has explained how to set up SPF, DKIM, and DMARC records that are primarily used by other organizations to prevent the receipt of forged emails purporting to be from domains controlled by the reader’s organization. Thus far it has not explained how the reader can configure their organization’s own mail servers to prevent the receipt of forged emails claiming to be from other domains. This is because altruism is required for SPF, DKIM, and DMARC to be effective.
Every MX in the world could be configured to check and enforce DMARC records for incoming mail, but it would have no effect if organizations didn’t publish SPF, DKIM, and DMARC records for their own domains: all those mail servers would have no rules to enforce. Similarly, DMARC enforcement would be useless if organizations go through the motions of creating basic SPF, DKIM, and DMARC records but leave them so loose that even obviously forged emails would not be marked for rejection, e.g., by allowing all messages to PASS SPF checks, not signing From: headers with DKIM, and/or leaving DMARC set to NOTIFY.
For DMARC to be effective, we must put in the effort to help other organizations protect themselves from spoofed emails purporting to be from our organizations’ domains and hope that those other organizations will return the favor so that we can protect ourselves. Every organization that takes the time to create and maintain tight and effective policies for SPF, DKIM, and DMARC makes it a little harder on spammers and a little easier on everyone else.
Configuring a mail server to check other organizations’ SPF, DKIM, and DMARC records is a little different on each type of mail server but is typically much more straightforward than configuring DNS records and DKIM signatures on outbound email.
If you’re one (1) of the many organizations using Microsoft 365 for your email: surprise! DMARC enforcement is already turned on by default. Microsoft’s documentation explains how incoming email is handled. The documentation also explains how to set up SPF, DKIM, and DMARC for outbound email.
The popular Unix EXIM mail server includes SPF, DKIM, and DMARC support and instructions for configuring it are included in the EXIM manual.
Other Unix mail servers, e.g., Postfix and Sendmail, can take advantage of open source modules to provide SPF, DKIM, and DMARC support:
- The SPF Project maintains a list of SPF implementations.
- OpenDKIM provides a DKIM library that works with various mail servers.
- OpenDMARC provides a DMARC library that works with various mail servers.
These modules are typically available in popular Linux distribution package repositories and can be installed using the usual methods.