Introduction
Email spoofing remains one of the most effective vectors for phishing attacks, business email compromise (BEC), and brand impersonation. Without authentication controls, anyone on the internet can send an email claiming to be ceo@yourcompany.com — and most mail clients will display it with no visible warning.
The three-layer email authentication stack — SPF, DKIM, and DMARC — closes this gap. Together they let receiving mail servers verify that:
- Mail from your domain comes from authorized servers (SPF)
- The message content hasn't been tampered with in transit (DKIM)
- Unauthenticated mail is quarantined or rejected, and you receive reports about it (DMARC)
This guide walks you through deploying all three, starting with a monitoring-only DMARC policy and progressing to full enforcement — the approach recommended by CISA, Microsoft, and Google.
Prerequisites
- DNS management console for your domain
- Admin access to your email platform (Exchange Online, Google Workspace, Postfix with OpenDKIM, etc.)
digornslookupavailable locally for validation
Understanding the Protocols
| Protocol | What It Does | DNS Record Type |
|---|---|---|
| SPF | Lists IP addresses and mail servers authorized to send email for your domain | TXT on @ / root |
| DKIM | Cryptographically signs outbound mail; receiving servers verify the signature against your public key | TXT on selector._domainkey |
| DMARC | Policy layer that tells receivers what to do when SPF/DKIM fail, and where to send reports | TXT on _dmarc |
DMARC requires at least one of SPF or DKIM to align (the From: domain must match the authenticated domain). This alignment check is what closes the "authenticated but still spoofed" loophole.
Step 1: Configure SPF
SPF is a TXT record on your domain's root (@) that lists all mail-sending services authorized for your domain.
Identify your sending sources
Before writing the record, list every service that sends email as your domain:
- Primary mail server (on-prem Exchange, Postfix, etc.)
- Microsoft 365 / Google Workspace
- Marketing platforms (Mailchimp, HubSpot, SendGrid, Resend, etc.)
- Ticketing systems, CRMs, monitoring alerts
- Transactional email providers
Build the SPF record
# Basic SPF structure
v=spf1 <mechanisms> <all>Common mechanisms:
# Authorize Microsoft 365
v=spf1 include:spf.protection.outlook.com -all
# Authorize Google Workspace
v=spf1 include:_spf.google.com -all
# Authorize both + a specific mail server IP
v=spf1 include:spf.protection.outlook.com include:_spf.google.com ip4:203.0.113.10 -all
# Include a third-party sender (e.g. SendGrid)
v=spf1 include:spf.protection.outlook.com include:sendgrid.net -allThe final qualifier controls what happens to non-matching mail:
| Qualifier | Meaning |
|---|---|
-all | Fail — reject mail not matching (recommended for production) |
~all | SoftFail — accept but mark (use during testing) |
?all | Neutral — no policy (avoid in production) |
+all | Pass everything — never use this |
Publish the SPF record
In your DNS console, create a TXT record:
Host: @ (or your bare domain, e.g. example.com)
Type: TXT
TTL: 3600
Value: v=spf1 include:spf.protection.outlook.com -all
SPF lookup limit: SPF allows a maximum of 10 DNS lookups per evaluation. Each
include:costs one lookup. Useip4:andip6:mechanisms for static IPs to save lookups. Tools like MXToolbox SPF Surveyor can count your lookups.
Verify SPF
# Check your SPF record
dig TXT example.com | grep spf
# Or use nslookup
nslookup -type=TXT example.com
# Expected output
example.com. 3600 IN TXT "v=spf1 include:spf.protection.outlook.com -all"Step 2: Configure DKIM
DKIM uses asymmetric cryptography. Your mail server signs outbound messages with a private key; the public key is published in DNS for receivers to verify.
Option A: Microsoft 365 (Exchange Online)
Microsoft 365 generates DKIM keys per domain. Enable DKIM signing in the Defender portal:
# Connect to Exchange Online PowerShell
Connect-ExchangeOnline -UserPrincipalName admin@example.com
# Check current DKIM status
Get-DkimSigningConfig -Identity example.com | Format-List
# Enable DKIM signing
Set-DkimSigningConfig -Identity example.com -Enabled $true
# If the domain hasn't had DKIM set up before, use:
New-DkimSigningConfig -DomainName example.com -Enabled $trueMicrosoft will display two CNAME records to publish in DNS:
selector1._domainkey.example.com → selector1-example-com._domainkey.example.onmicrosoft.com
selector2._domainkey.example.com → selector2-example-com._domainkey.example.onmicrosoft.com
Publish both CNAME records in your DNS console, then re-run Set-DkimSigningConfig to activate.
Option B: Google Workspace
- Go to Admin Console → Apps → Google Workspace → Gmail → Authenticate email
- Select your domain and click Generate new record
- Google provides a TXT record value for
google._domainkey.example.com - Publish the TXT record in your DNS console
- Return to Admin Console and click Start authentication
Option C: Postfix with OpenDKIM (Linux)
# Install OpenDKIM
sudo apt install opendkim opendkim-tools -y
# Generate a 2048-bit key pair
sudo mkdir -p /etc/opendkim/keys/example.com
sudo opendkim-genkey -b 2048 -d example.com -D /etc/opendkim/keys/example.com -s mail -v
# Set permissions
sudo chown opendkim:opendkim /etc/opendkim/keys/example.com/mail.private
sudo chmod 600 /etc/opendkim/keys/example.com/mail.private
# View the public key record to publish in DNS
cat /etc/opendkim/keys/example.com/mail.txtThe output will be a TXT record for mail._domainkey.example.com. Publish it in DNS.
Configure /etc/opendkim.conf:
Mode sv
SubDomains no
AutoRestart yes
AutoRestartRate 10/1M
Syslog yes
SyslogSuccess yes
LogWhy yes
Canonicalization relaxed/simple
Domain example.com
Selector mail
KeyFile /etc/opendkim/keys/example.com/mail.private
Socket inet:8891@localhost
PidFile /run/opendkim/opendkim.pid
OversignHeaders FromAdd milter configuration to /etc/postfix/main.cf:
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891sudo systemctl enable --now opendkim
sudo systemctl restart postfixVerify DKIM
# Check DKIM public key is published (replace 'mail' with your selector)
dig TXT mail._domainkey.example.com
# For Microsoft 365 selectors
dig TXT selector1._domainkey.example.com
dig TXT selector2._domainkey.example.com
# Send a test email to check-auth@verifier.port25.com and review the reply
# It will show DKIM pass/fail in the resultsStep 3: Deploy DMARC (Start with Monitoring)
DMARC ties SPF and DKIM together with a policy and a reporting mechanism. Always start with p=none (monitor mode) to collect data before enforcing — rushing to p=reject without analysis will break legitimate mail flows.
Phase 1 — Monitor only (p=none)
Publish this TXT record on _dmarc.example.com:
v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; ruf=mailto:dmarc-forensics@example.com; fo=1; adkim=r; aspf=r; pct=100| Tag | Value | Meaning |
|---|---|---|
p | none | No enforcement — monitor only |
rua | mailto:... | Aggregate report destination (daily XML digests) |
ruf | mailto:... | Forensic/failure report destination (per-message) |
fo | 1 | Send forensic reports when SPF or DKIM fails |
adkim | r | Relaxed DKIM alignment |
aspf | r | Relaxed SPF alignment |
pct | 100 | Apply policy to 100% of messages |
Tip: Point
ruato a dedicated mailbox or a DMARC reporting SaaS (dmarcian, Postmark, Google Postmaster Tools) — aggregate reports are compressed XML and difficult to parse manually.
Publish in DNS:
Host: _dmarc
Type: TXT
TTL: 3600
Value: v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; fo=1
Phase 2 — Quarantine (p=quarantine)
After 2–4 weeks of aggregate report analysis and confirming all legitimate senders are covered by SPF/DKIM, escalate:
v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.com; fo=1; pct=100Mail failing DMARC alignment goes to the recipient's spam/junk folder. Start with pct=10 to apply to only 10% of messages and ramp up as confidence grows.
Phase 3 — Reject (p=reject)
Full enforcement — unauthenticated mail is rejected at the gateway:
v=DMARC1; p=reject; rua=mailto:dmarc-reports@example.com; fo=1; pct=100Before moving to p=reject, verify:
- All expected sending services appear as DMARC pass in aggregate reports
- No legitimate internal systems are sending unauthenticated mail
- Subdomains are covered (or explicitly set
sp=rejectfor subdomain policy)
DMARC for subdomains
To apply a separate policy to subdomains:
v=DMARC1; p=reject; sp=quarantine; rua=mailto:dmarc-reports@example.com; fo=1Step 4: Monitor with DMARC Aggregate Reports
DMARC aggregate reports (RUA) are XML files sent daily by major receivers (Google, Microsoft, Yahoo, etc.). They show:
- Source IP addresses sending mail as your domain
- SPF and DKIM pass/fail status per source
- Volume of messages per source
Parse reports with parsedmarc (Python)
pip install parsedmarc elasticsearch
# Parse a local aggregate report XML
parsedmarc report.xml.gz
# Parse reports from a mailbox and output to JSON
parsedmarc --imap-host imap.example.com \
--imap-user dmarc-reports@example.com \
--imap-password 'yourpassword' \
-o /var/log/dmarc/Quick sanity check without tooling
# Decode and inspect a report manually
zcat report.xml.gz | python3 -c "
import sys
from xml.dom.minidom import parseString
print(parseString(sys.stdin.read()).toprettyxml(indent=' '))
" | lessKey fields to review in each report:
<record>
<row>
<source_ip>203.0.113.45</source_ip> <!-- Who sent the mail -->
<count>142</count> <!-- How many messages -->
<policy_evaluated>
<disposition>none</disposition> <!-- What happened -->
<dkim>pass</dkim> <!-- DKIM result -->
<spf>pass</spf> <!-- SPF result -->
</policy_evaluated>
</row>
</record>Any source IP showing dkim>fail and spf>fail is sending unauthenticated mail as your domain and warrants investigation.
Verification and Testing
Full authentication check
Send a test message to check-auth@verifier.port25.com — you'll receive an automated reply showing SPF, DKIM, and DMARC results.
MXToolbox bulk check
# Use the MXToolbox command-line equivalent via curl (for scripting)
# Or visit https://mxtoolbox.com/EmailHeaders.aspx and paste raw headers
# Check SPF
dig TXT example.com | grep "v=spf1"
# Check DKIM (replace selector and domain)
dig TXT mail._domainkey.example.com | grep "v=DKIM1"
# Check DMARC
dig TXT _dmarc.example.com | grep "v=DMARC1"Inspect email headers manually
In any received email, view the full headers and look for the Authentication-Results header:
Authentication-Results: mx.google.com;
dkim=pass header.i=@example.com header.s=mail header.b=AbCdEfGh;
spf=pass (google.com: domain of sender@example.com designates 203.0.113.10 as permitted sender) smtp.mailfrom=sender@example.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=example.com
All three should show pass for legitimate outbound mail.
BIMI readiness (bonus)
Once DMARC is at p=quarantine or p=reject, you're eligible for BIMI (Brand Indicators for Message Identification), which displays your logo in supported mail clients. BIMI requires a verified mark certificate (VMC) for full support, but the _bimi TXT record is free to publish for self-asserted logos.
Troubleshooting
SPF PermError: too many DNS lookups
SPF evaluations are capped at 10 DNS lookups. Flatten nested include: chains by replacing them with explicit ip4: and ip6: blocks. Use tools like dmarcian's SPF Surveyor or kitterman.com/spf/validate.html to count your lookups.
# Before (3 includes, each with sub-lookups)
v=spf1 include:sendgrid.net include:mailchimp.com include:spf.protection.outlook.com -all
# After (flatten where possible)
v=spf1 ip4:149.72.0.0/16 ip4:198.2.128.0/18 include:spf.protection.outlook.com -allDKIM signature verification failures
- Clock skew: DKIM timestamps must be within 5 minutes. Ensure NTP is synchronized on your mail server (
timedatectl status). - Line length canonicalization: Use
relaxed/simplecanonicalization (Canonicalization relaxed/simplein OpenDKIM). Some forwarders modify whitespace. - Key mismatch: Confirm the selector DNS record matches the private key used for signing. Regenerate if uncertain.
- Key size: 1024-bit keys are now considered weak. Use 2048-bit minimum.
DMARC alignment failures from mailing lists
Mailing list software often rewrites the From: header or adds footers that break DKIM signatures. Solutions:
- Move to relaxed alignment (
adkim=r) if not already set - Work with mailing list operators to implement ARC (Authenticated Received Chain)
- Add the mailing list IP to SPF so SPF alignment can carry DMARC even when DKIM fails
Legitimate mail going to spam after p=quarantine
Review your DMARC aggregate reports to find the failing source IP. Common causes:
- An internal application server not listed in SPF
- A third-party service using your domain without DKIM signing enabled
- A subdomain without its own SPF/DKIM records inheriting the parent policy
Fix: add the missing source to SPF, enable DKIM signing for the service, or set a permissive sp= subdomain policy temporarily.
DMARC reports not arriving
- Confirm your
rua=email address is correct and the mailbox exists - If the report destination is on a different domain, publish a DMARC TXT record authorizing the cross-domain reporting:
# On the domain receiving reports (reports.example.net)
# if the sending domain is example.com:
example.com._report._dmarc.reports.example.net. TXT "v=DMARC1;"
Summary
You've deployed a complete email authentication stack:
| Step | Record | Status |
|---|---|---|
| 1 | SPF TXT on @ | Authorizes legitimate senders |
| 2 | DKIM TXT/CNAME on selector._domainkey | Signs and verifies message integrity |
| 3 | DMARC TXT on _dmarc | Enforces policy and collects reports |
Recommended progression timeline:
- Week 1–2: Deploy SPF and DKIM, publish DMARC at
p=none - Week 3–6: Analyze aggregate reports, remediate all unknown senders
- Week 7–8: Escalate to
p=quarantine; pct=25, monitor for breakage - Week 9–10: Increase to
pct=100at quarantine - Week 11+: Move to
p=rejectfor full enforcement
Email authentication is not a one-time task — revisit your SPF record whenever you onboard a new SaaS tool that sends mail as your domain, and rotate DKIM keys annually. Set a recurring calendar reminder.
For compliance programs, DMARC at p=reject satisfies email authentication requirements in NIST SP 800-177, CIS Control 9, and is mandated by CISA BOD 18-01 for US federal agencies.