Microsoft 365 Email & Collaboration

Enforcing Email Authentication for Exchange Online: SPF, DKIM, and DMARC From Monitoring to Reject

DMARC enforcement is the single highest-leverage control for stopping people from spoofing your domain, and it is also the easiest one to break production mail with if you flip it on blind. This guide walks the full progression for Exchange Online (EXO): a correct SPF record, DKIM signing on your accepted domains, and a deliberate DMARC ramp from p=none to p=reject, driven by what the aggregate reports actually tell you.

Everything here assumes the Exchange Online PowerShell V3 module (ExchangeOnlineManagement) for tenant-side config, and your DNS provider’s console (or its API/Terraform provider) for the public records. Keep the records in source control as zone files or HCL — these are security controls, and an undocumented edit to an SPF string is a future outage.

Install-Module ExchangeOnlineManagement -Scope CurrentUser
Connect-ExchangeOnline -UserPrincipalName admin@contoso.com

1. Why the three records work together

SPF, DKIM, and DMARC are not redundant. They check different things, and DMARC is the only one that ties them to the address your users actually see.

Alignment is the load-bearing concept and the one most people miss. SPF passing for bounce.mailer.com does nothing for a message whose From: is @contoso.com — the domains do not align. Either SPF must pass for an aligned domain or DKIM must produce a passing signature with d=contoso.com (or a subdomain, under relaxed alignment). One aligned pass is enough; you do not need both.

Record Checks against Header it protects Survives forwarding?
SPF Sending IP 5321.MailFrom (envelope) No — IP changes on relay
DKIM Signature + DNS key d= domain in signature Often yes, if body unchanged
DMARC SPF/DKIM alignment 5322.From (visible) Yes, if either underlying check stays aligned

This is why DKIM matters even though SPF is older and simpler: forwarding (mailing lists, “forward to my Gmail” rules) breaks SPF almost every time because the IP changes, but a clean DKIM signature can still carry the message past DMARC. Publish all three.

2. Inventory legitimate senders before you publish anything

The fastest way to cause an incident is to enforce DMARC before you know who sends as your domain. Build the inventory first. You are looking for every system that puts your domain in the From: header:

A practical first pass is to publish DMARC at p=none (Step 5) and let the aggregate reports enumerate senders you forgot — they always will. But do the manual inventory anyway; the reports tell you that an IP sends as you, not which business owner to call when you are about to start rejecting it.

For each sender, record: the From: domain/subdomain it uses, whether it supports DKIM signing with your domain, and what it needs in SPF (an include: or specific IPs).

3. Author a correct SPF record (and respect the 10-lookup limit)

SPF is a single TXT record at the domain root. The version tag and a final all mechanism are mandatory.

contoso.com.  IN  TXT  "v=spf1 include:spf.protection.outlook.com -all"

include:spf.protection.outlook.com authorizes Exchange Online’s outbound IPs. The qualifier on all is the policy:

Add other senders by their published mechanism, not by guessing IPs:

contoso.com.  IN  TXT  "v=spf1 include:spf.protection.outlook.com include:_spf.salesforce.com include:sendgrid.net ip4:203.0.113.10 -all"

The 10-lookup limit is a hard ceiling, not a guideline. RFC 7208 caps the number of DNS-querying mechanisms (include, a, mx, ptr, exists) that may be resolved while evaluating a record at 10. Exceed it and evaluation returns permerror, which most receivers treat as SPF fail — silently breaking mail for every sender, not just the one that tipped you over. Each nested include counts, and includes nest (Microsoft’s own expands internally).

Count your lookups and keep headroom:

# Quick manual check of the top-level record
dig +short TXT contoso.com | grep spf1

When you approach the ceiling, the fixes in order of preference:

  1. Remove senders you no longer use. The cheapest lookup is the one you delete.
  2. Replace an include with explicit ip4:/ip6: mechanisms if the vendor publishes a small, stable IP set. Literal IPs cost zero lookups.
  3. SPF flattening — resolve includes to IPs and inline them. This works but is brittle: when the vendor changes IPs, your flattened record is stale and starts failing. Only automate flattening with a service or pipeline that re-resolves on a schedule; never flatten by hand and forget it.

Avoid ptr entirely — it is deprecated and slow. Never publish more than one v=spf1 TXT record per domain; multiple SPF records are themselves a permerror.

4. Enable and rotate DKIM signing in Exchange Online

By default EXO signs outbound mail with a key for your *.onmicrosoft.com initial domain. That signature is valid but does not align with a custom From: domain, so it does nothing for your DMARC on contoso.com. You must enable DKIM per custom (accepted) domain.

DKIM in EXO uses two selectors (selector1, selector2) so keys can rotate without an outage — one is active while the other is staged. First publish two CNAMEs that point at Microsoft’s managed key endpoints. The right-hand targets follow a fixed pattern: dots in your domain become dashes, then ._domainkey.<tenant>.onmicrosoft.com.

selector1._domainkey.contoso.com.  IN  CNAME  selector1-contoso-com._domainkey.contoso.onmicrosoft.com.
selector2._domainkey.contoso.com.  IN  CNAME  selector2-contoso-com._domainkey.contoso.onmicrosoft.com.

Get the exact targets for your tenant from the Defender portal (Email & collaboration > Policies & rules > Threat policies > DKIM) or by reading the signing config in PowerShell — do not assume the tenant string. With both CNAMEs resolving, enable signing:

# If a config does not yet exist for the domain, create one (2048-bit)
New-DkimSigningConfig -DomainName contoso.com -KeySize 2048 -Enabled $true

# If it already exists (e.g. created with a 1024-bit key), just enable it
Set-DkimSigningConfig -Identity contoso.com -Enabled $true

# Confirm and read the selector CNAME targets EXO expects
Get-DkimSigningConfig -Identity contoso.com |
  Format-List Domain,Enabled,Selector1CNAME,Selector2CNAME,KeySize,Status

Use 2048-bit keys. New configs created today can be 2048-bit directly. Older domains may still be on 1024-bit; upgrade them by rotating (below) with -KeySize 2048.

Rotation

DKIM keys should rotate periodically (quarterly is a reasonable cadence) and immediately after any suspected compromise. EXO handles rotation by flipping between the two selectors:

Rotate-DkimSigningConfig -Identity contoso.com -KeySize 2048

Two timing realities to plan around:

5. Publish DMARC at p=none and ingest RUA reports

With SPF authorized and DKIM aligned, publish DMARC in monitoring mode. p=none changes nothing about how mail is delivered — it only asks receivers to report. That is exactly what you want before enforcing.

_dmarc.contoso.com.  IN  TXT  "v=DMARC1; p=none; rua=mailto:dmarc-rua@contoso.com; ruf=mailto:dmarc-ruf@contoso.com; fo=1; adkim=r; aspf=r; pct=100"

Tag by tag:

Tag Value here Meaning
p none Policy for the org domain: monitor only
rua mailbox Where aggregate (RUA) XML reports go — the ones you actually use
ruf mailbox Where forensic/failure (RUF) reports go — sparse; many receivers never send them for privacy reasons
fo 1 Generate failure reports if any mechanism fails alignment (most useful setting)
adkim/aspf r Relaxed alignment — subdomains of the From: domain count as aligned (vs s strict, exact match)
pct 100 Percentage of mail the policy applies to (meaningful only at quarantine/reject)

Do not point rua at a raw mailbox and read XML by hand. Aggregate reports are gzipped XML, one per receiver per day, and at any real volume you will get dozens daily. Send them to a DMARC processing service (commercial or self-hosted such as OpenDMARC/parsedmarc) that aggregates by source IP, SPF/DKIM result, and message count. The point of p=none is the data; make it readable.

If the RUA mailbox is on a different domain than the one being reported, that domain must opt in with an authorization record, or receivers will refuse to send reports there:

contoso.com._report._dmarc.reports-vendor.com.  IN  TXT  "v=DMARC1"

Let p=none run at least two to four weeks — long enough to capture monthly batch jobs, invoicing runs, and quarterly campaigns that an inventory misses.

6. Read the reports and fix unauthenticated legitimate sources

This is the real work, and it is where you spend most of your calendar time. Every distinct source IP in the aggregate data falls into one of three buckets:

  1. Legitimate and aligned — passes SPF or DKIM with alignment. Leave it.
  2. Legitimate but failing alignment — a real sender of yours (a SaaS platform, a relay) that is not yet authorized or aligned. Fix it before enforcing, or enforcement will block it.
  3. Illegitimate — spoofers and spammers. This is precisely what you are about to stop. Confirm it is not actually one of yours, then ignore it.

A decision-tree style reading of bucket 2:

Symptom in report Likely cause Fix
SPF passes for a different domain, DKIM none Sender uses its own envelope domain, no DKIM for you Add the sender’s include: to SPF, and/or configure DKIM with d=contoso.com at the vendor
DKIM d= is the vendor’s domain, not yours Vendor signs with its own domain Set up custom-domain DKIM (CNAME + vendor config) so d=contoso.com
Mail from your own IPs failing both On-prem app/relay sending directly Route through EXO (which signs) or add the IP to SPF and arrange DKIM
Only forwarded mail fails Mailing lists / auto-forwards break SPF Acceptable if DKIM still aligns; ARC mitigates (Step 8)

The exit criterion for this phase is explicit: for every source you can identify as legitimate, it passes DMARC via an aligned SPF or DKIM result. Only spoofed/unknown traffic should still be failing. Do not move on until that is true — a single unfixed payroll or invoicing sender becomes a P1 the moment you reach p=reject.

7. Progress to quarantine and reject

Now ramp the policy. The pct tag lets you apply enforcement to a fraction of failing mail, so you discover surprises against a slice instead of all of it. (Receivers apply pct to failing messages; the rest get the next-weaker action.)

Step up, watching reports between each change:

# Stage 1: quarantine a slice
_dmarc.contoso.com.  IN  TXT  "v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc-rua@contoso.com; fo=1; adkim=r; aspf=r"
# Stage 2: quarantine everything failing
_dmarc.contoso.com.  IN  TXT  "v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc-rua@contoso.com; fo=1; adkim=r; aspf=r"
# Stage 3: full enforcement, with an explicit subdomain policy
_dmarc.contoso.com.  IN  TXT  "v=DMARC1; p=reject; sp=reject; pct=100; rua=mailto:dmarc-rua@contoso.com; fo=1; adkim=r; aspf=r"

Two things deserve emphasis:

A realistic timeline from a standing start: a few days at p=none to confirm reporting works, two to four weeks fixing senders, one to two weeks at quarantine (25 then 100), then reject. Faster is possible once the inventory is genuinely clean; slower is fine. Rushing the sender-fixing phase is the only step that reliably causes outages.

8. Interaction with Defender, ARC, and forwarding

A few EXO-specific behaviors change how this plays out:

# Trust an intermediary's ARC seals (inbound) — use the sealer's signing domain (the ARC "d=")
Set-ArcConfig -Identity Default -ArcTrustedSealers "gateway-vendor.com"
Get-ArcConfig -Identity Default | Format-List ArcTrustedSealers

Enterprise scenario

A retail group with ~9,000 mailboxes had sat at p=none for over a year, blocked on one number: the SPF record already resolved to 11 DNS lookups. The string was include:spf.protection.outlook.com plus four SaaS includes (Salesforce, SendGrid, a marketing platform, a legacy ticketing system), and Microsoft’s own include had quietly grown its internal expansion. Adding a newly-onboarded e-signature vendor tipped it to permerror — which most receivers treat as SPF fail — and inbound partners started bouncing invoices. The platform team’s first instinct was hand-flattening, which we vetoed: SendGrid and the marketing platform rotate IP ranges, so a frozen flattened record would silently rot.

The fix was to stop cramming every sender through the org domain. We moved every bulk/transactional sender onto a dedicated subdomain, mail.contoso.com, with its own SPF record and its own DKIM. That pulled the SaaS includes off the org root entirely, dropping it back to two lookups, and let DMARC alignment ride on DKIM under relaxed adkim=r.

contoso.com.       IN TXT "v=spf1 include:spf.protection.outlook.com -all"
mail.contoso.com.  IN TXT "v=spf1 include:_spf.salesforce.com include:sendgrid.net include:_spf.mktplatform.com -all"

Only the senders that genuinely use the corporate From: (Exchange itself) stayed on the root. Subdomain DKIM kept each vendor aligned, and within three weeks we cleared bucket 2 and went to p=reject; sp=reject on the org domain without a single legitimate bounce. The lesson: the 10-lookup ceiling is an architecture signal, not a record to micro-optimize — split your sending identity before you flatten.

Verify

Confirm each layer independently before trusting the chain. Use external resolvers so you see what the internet sees, not a cached internal view.

# SPF: exactly one v=spf1 record, correct includes, -all
dig +short TXT contoso.com @1.1.1.1 | grep spf1

# DKIM: both selector CNAMEs resolve to Microsoft endpoints
dig +short CNAME selector1._domainkey.contoso.com @1.1.1.1
dig +short CNAME selector2._domainkey.contoso.com @1.1.1.1

# DMARC: policy record present and at the stage you expect
dig +short TXT _dmarc.contoso.com @1.1.1.1
# DKIM is enabled and healthy on the custom domain
Get-DkimSigningConfig -Identity contoso.com |
  Format-List Domain,Enabled,Status,KeySize,RotateOnDate

End-to-end, the authoritative test is to send a real message to a mailbox you control on another provider (Gmail, Yahoo, or an external EXO tenant) and read the received headers. You are looking for spf=pass, dkim=pass with d=contoso.com (not onmicrosoft.com), and dmarc=pass. Tools like the Microsoft Message Header Analyzer or mail-tester.com make this quick. A passing Authentication-Results header from a third party is the only proof that matters; everything in DNS is just configuration until a real receiver agrees.

Checklist

Pitfalls

Once you are at p=reject; sp=reject with clean reports, do not consider it finished. New SaaS tools get adopted, vendors change IPs, and a marketing team will eventually wire up a sender nobody told you about. Keep the RUA pipeline alive and review it monthly — DMARC is an operational control, not a config you set and walk away from.

Exchange OnlineDMARCDKIMSPFEmail Security

Comments

Keep Reading