Email authentication guide: SPF, DKIM, and DMARC
// email_authentication_guide.md — SPF, DKIM, and DMARC for domain owners
// what_is_spf
SPF (Sender Policy Framework) is a DNS TXT record that lists the servers and services authorised to send email on behalf of your domain. When a message arrives, the receiving server checks whether the sending server's IP address appears in your SPF record. If it does not, the SPF check fails.
The record is published at your root domain. A typical SPF record for a Microsoft 365 tenant looks like this:
DNS TXT record — example.comv=spf1 include:spf.protection.outlook.com -all
| v=spf1 | Required. Identifies this as an SPF record. |
| include: | Authorises another domain's SPF record. Each sending service (Exchange Online, Google Workspace, Mailchimp, etc.) provides its own value. |
| -all | Hard fail. Reject any sender not listed. Use ~all (soft fail) during initial setup, then move to -all. |
include: mechanisms rather than publishing separate records.
// what_is_dkim
DKIM (DomainKeys Identified Mail) adds a cryptographic signature to the headers of outgoing messages. The receiving server retrieves your public key from DNS and uses it to verify that the message has not been modified in transit.
DKIM must be configured on every service that sends mail from your domain. Exchange Online, Mailchimp, HubSpot, and similar platforms each generate their own key pair. You publish the public key as a DNS TXT record at a selector-specific subdomain, and the platform signs outgoing mail with the corresponding private key.
DNS TXT record — selector._domainkey.example.comv=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ...
Each sending service provides its own selector name and public key value. Follow the DKIM setup guide for each platform you use. If a service sends mail but has no DKIM signature configured, those messages will fail DKIM checks regardless of your DMARC policy.
// what_is_dmarc
DMARC (Domain-based Message Authentication, Reporting and Conformance) ties SPF and DKIM
together with a published policy. It tells receiving servers what to do when one or both of those checks fail,
and adds an alignment requirement: the authenticated domain must match the From: address the
recipient sees.
The record is published as a DNS TXT record at _dmarc.yourdomain.com. For a message to pass
DMARC, at least one of SPF or DKIM must pass and align with the From: domain.
DNS TXT record — _dmarc.example.comv=DMARC1; p=reject; sp=reject; rua=mailto:dmarc-reports@example.com
Without a DMARC record at enforcement level, anyone can send email that appears to come from your domain. This technique is the foundation of phishing campaigns, business email compromise, and brand spoofing attacks.
// the_three_policies
The p= tag sets your policy. The sp= tag sets the policy for subdomains separately.
If sp= is omitted, subdomains inherit p=.
rua= is configured. A starting point, not a permanent posture.
// how_to_set_it_up
A DMARC record is a single DNS TXT record. Here are two common configurations:
Minimum viable record (monitoring only)v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com
Fully hardened recordv=DMARC1; p=reject; sp=reject; rua=mailto:dmarc-reports@example.com; adkim=s; aspf=s
| v=DMARC1 | Required. Identifies the record as DMARC. |
| p= | Policy for the domain. none, quarantine, or reject. |
| sp= | Policy for subdomains. Defaults to p= if omitted. Set explicitly to avoid ambiguity. |
| pct= | Percentage of failing messages the policy applies to. Default is 100. Lower values are useful during a staged rollout to quarantine or reject. |
| rua= | Aggregate report destination. Receiving servers send daily XML summaries of pass/fail counts to this address. Essential for visibility. |
| ruf= | Forensic report destination. Individual message-level failure reports. Not widely supported and often disabled by major providers. |
| adkim= | DKIM alignment mode. r (relaxed, default) allows subdomain matches. s (strict) requires an exact domain match. |
| aspf= | SPF alignment mode. Same relaxed/strict options as adkim=. |
// recommended_rollout
Moving straight to p=reject without preparation risks blocking legitimate mail. A staged approach
gives you visibility before applying enforcement.
-
Publish SPF and configure DKIM on all sending services. Inventory every service that sends mail from your domain: your mail server, CRM, marketing platform, ticketing system, and so on. Each one needs to be covered by SPF and have DKIM configured before you move beyond
p=none. -
Start at
p=nonewith a reporting address. Setrua=to an address you monitor. Aggregate reports will show which services are passing and which are failing, without affecting mail delivery. -
Review aggregate reports for two to four weeks. Look for legitimate senders that are failing SPF or DKIM. For each one, add the appropriate SPF
include:and configure DKIM on that platform. Repeat until reports show only unauthenticated sources failing. -
Move to
p=reject. Once every legitimate sending service is covered, update your DMARC record top=reject; sp=reject. Spoofed messages are now blocked outright.
// common_mistakes
No sp= |
Subdomains inherit p= but the behaviour is implicit. Setting sp=reject explicitly makes your intent clear and protects subdomains even if the inheritance rules change. |
No rua= |
Without an aggregate reporting address you have no visibility into what is failing. You are flying blind. |
Stopping at p=none |
p=none provides zero protection. It is a monitoring phase. Domains that stay here indefinitely appear on the Wall of Shame. |
| Missing DKIM on a sender | If a sending service is not configured for DKIM it will fail DKIM checks on every message. Identify all senders before moving to enforcement. |
Jumping straight to p=reject |
Without reviewing reports first you risk rejecting legitimate mail from services you forgot to configure. Follow the staged rollout above. |
| Forgetting parked domains | Domains that send no mail are common spoofing targets precisely because they have no DMARC record. Publish v=DMARC1; p=reject; sp=reject plus v=spf1 -all on parked domains. |
Ready to check your domain?
▶ run check_my_domain.sh