SPF, DKIM and DMARC for Microsoft 365: step by step to p=reject

This guide takes a Microsoft 365 domain from “anyone can spoof us” to DMARC p=reject without losing legitimate mail. It uses contoso.nl as the example domain. For the background, read the blog Email security explained. For DNSSEC and DANE, the last step, there is a separate guide: DNSSEC and DANE for Exchange Online.

Plan for three to six weeks. The DNS changes take an afternoon. Most of the time goes into reading DMARC reports and fixing the services you didn’t know were sending mail as you.

Before you start

  • Access to your public DNS (registrar, Cloudflare or your DNS host).
  • Exchange Online PowerShell (Install-Module ExchangeOnlineManagement) and the Security Administrator or Exchange Administrator role.
  • An address for DMARC reports. Expect dozens of XML reports a day. In our experience DMARC Advisor is the best tool to receive and read them: it turns the reports into overviews per sending source, alerts you to new or failing sources and guides you to p=reject. Use the rua address it gives you, or a mailbox such as [email protected].
  • A baseline: check your domain below and keep the result.
Specify a DKIM selector (optional)

We only read public DNS data, via Cloudflare DNS. No mail is sent. 365Fanatic stores no domain names or other details of your check; we only count how often the check is used.

Step 1: List everything that sends mail as you

This is the step that decides whether p=reject goes smoothly. Typical senders besides Microsoft 365:

  • Newsletter and marketing tools
  • CRM, ticketing and case management systems
  • Invoicing, payroll and HR software
  • Multifunction printers and scanners (SMTP relay)
  • The website: contact forms, WordPress notifications, web shop
  • Monitoring and backup tools that mail alerts

Ask each supplier two things: the SPF include: they need, and how to set up DKIM signing with your domain. Where possible, give bulk senders their own subdomain (for example mail.contoso.nl or news.contoso.nl) so their reputation can’t hurt the mail your staff send.

Step 2: SPF

For a domain that only sends through Microsoft 365, this is Microsoft’s recommended record:

Type Name Value
TXT contoso.nl (or @) v=spf1 include:spf.protection.outlook.com -all

Add other senders to the same record, for example v=spf1 include:spf.protection.outlook.com include:_spf.newsletter-supplier.example -all. Use exactly what the supplier documents; some don’t need an include at all because they send with their own return address and only need DKIM. Rules that catch most people out:

  • One SPF record per domain. Two v=spf1 records make SPF invalid.
  • Ten DNS lookups at most. Every include:, a, mx, exists and redirect counts, including the ones nested inside an include. ip4: and ip6: don’t count. Over ten, SPF returns a permanent error for all mail.
  • Use -all. Microsoft recommends hard fail for Microsoft 365 domains, because DMARC decides what happens to failing mail anyway.
  • Subdomains need their own record. The SPF record on contoso.nl doesn’t cover news.contoso.nl.

Check it:

PowerShell
Resolve-DnsName -Name contoso.nl -Type TXT | Where-Object { $_.Strings -like 'v=spf1*' }

Step 3: DKIM in Microsoft 365

Every tenant signs mail with its *.onmicrosoft.com domain by default. That doesn’t help DMARC for contoso.nl: the signature has to be for your own domain. Create the signing configuration with 2048-bit keys:

PowerShell
Connect-ExchangeOnline
New-DkimSigningConfig -DomainName contoso.nl -KeySize 2048 -Enabled $false
Get-DkimSigningConfig -Identity contoso.nl | Format-List Name,Enabled,Status,Selector1CNAME,Selector2CNAME

Publish the two CNAME records exactly as Selector1CNAME and Selector2CNAME show them. Don’t copy them from an old guide: since May 2025, new domains get targets ending in .dkim.mail.microsoft (for example selector1-contoso-nl._domainkey.contoso.n-v1.dkim.mail.microsoft), with a partition letter only the cmdlet can tell you. Older domains keep the *.onmicrosoft.com form.

Type Name Points to
CNAME selector1._domainkey value of Selector1CNAME
CNAME selector2._domainkey value of Selector2CNAME

Wait until DNS has picked up both records, then enable signing:

PowerShell
Set-DkimSigningConfig -Identity contoso.nl -Enabled $true
Get-DkimSigningConfig -Identity contoso.nl | Format-List Enabled,Status

Status should read Valid. CnameMissing means DNS isn’t there yet. The Defender portal shows the same under Email & collaboration → Policies & rules → Threat policies → Email authentication settings → DKIM.

Already had DKIM with 1024-bit keys? Rotate to 2048. The new key takes about four days to become active, and the second rotation brings the other selector to 2048 as well:

Code
Rotate-DkimSigningConfig -Identity contoso.nl -KeySize 2048

Then set up DKIM with your domain in every other service from step 1. Each supplier has its own records; usually one or two CNAMEs or a TXT record with their own selector name.

Step 4: DMARC at p=none

Publish a monitoring policy first:

Type Name Value
TXT _dmarc.contoso.nl v=DMARC1; p=none; np=reject; rua=mailto:[email protected]
  • p=none: measure only, change nothing for recipients.
  • rua=: where the daily aggregate reports go. If that address is on another domain (a report service), that domain must publish an authorisation record; the service usually does this for you.
  • np=reject: new in RFC 9989 (May 2026). Rejects mail from subdomains that don’t exist, such as invoices.contoso.nl. No legitimate mail comes from those, so it’s safe from day one, even while p is still none. Receivers that don’t know np yet simply ignore it.

Now read the reports for two to four weeks (in DMARC Advisor or your own report tool). For every source that sends as contoso.nl, ask: is this us? If yes, make it pass SPF or DKIM aligned with contoso.nl (DKIM is the more robust choice). If no, it’s exactly what DMARC will block later.

Step 5: Quarantine, then reject

When all legitimate sources pass, tighten in steps. Since RFC 9989 (May 2026) you test a step with t=y: receivers then apply the policy one step lighter, so quarantine works as none and reject works as quarantine. The old pct tag has been removed from the standard and new receivers ignore it, so don’t use it any more.

Code
v=DMARC1; p=quarantine; np=reject; t=y; rua=mailto:[email protected]
v=DMARC1; p=quarantine; np=reject; rua=mailto:[email protected]
v=DMARC1; p=reject; t=y; rua=mailto:[email protected]
v=DMARC1; p=reject; rua=mailto:[email protected]

Give each step a week or two and keep reading the reports. Once p=reject is in place, it also covers your subdomains that don’t have their own DMARC record, and np falls back to it for subdomains that don’t exist, so np=reject can go. Add sp= only if your subdomains need a different policy than the main domain. To build or check these records, use our record generator.

Step 6: Domains that don’t send mail

Every domain you own but don’t mail from gets these three records. It takes five minutes per domain and removes a favourite tool of fraudsters.

Type Name Value Meaning
TXT @ v=spf1 -all Nobody may send mail
TXT _dmarc v=DMARC1; p=reject; sp=reject Reject anything that claims to be us
MX @ 0 . This domain receives no mail (null MX, RFC 7505)

Step 7: Check inbound DMARC handling

Exchange Online honours the DMARC policies of other domains. In the Defender portal, open your anti-phishing policy and check the actions under Honor DMARC record policy when the message is detected as spoof: p=quarantine should go to quarantine, p=reject should be rejected. If mail reaches Microsoft 365 through a third-party filter or gateway first, add it as a trusted ARC sealer or use Enhanced Filtering for Connectors, otherwise every message looks like it failed authentication.

Step 8: DNSSEC and DANE

SPF, DKIM and DMARC protect your recipients. DNSSEC and DANE protect the mail coming to you. For Exchange Online, the order is: sign your zone with DNSSEC and add the DS record at the registrar, move MX to the signed *.mx.microsoft host, then enable inbound DANE. The full procedure is in DNSSEC and DANE for Exchange Online. Optionally add MTA-STS and TLS-RPT as well: Gmail uses MTA-STS rather than DANE.

Step 9: Test

  1. Check the domain again with our email security check. SPF, DKIM and DMARC should be green; DNSSEC and DANE follow after step 8.
  2. Send a mail to a Gmail or Outlook.com address and paste its headers into our email header analyzer. You want spf=pass, dkim=pass and dmarc=pass, all for contoso.nl.
  3. Check the records from PowerShell:
PowerShell
Resolve-DnsName -Name _dmarc.contoso.nl -Type TXT
Resolve-DnsName -Name selector1._domainkey.contoso.nl -Type CNAME
Resolve-DnsName -Name selector2._domainkey.contoso.nl -Type CNAME
Get-DkimSigningConfig -Identity contoso.nl | Format-List Enabled,Status,RotateOnDate

Troubleshooting

Symptom Likely cause Fix
SPF permerror More than ten lookups, or two SPF records Merge into one record; replace includes with ip4:/ip6: where the supplier allows it, or move a sender to a subdomain
DKIM status CnameMissing CNAMEs not published or not propagated, or copied with a typo Compare DNS with Selector1CNAME/Selector2CNAME; some DNS hosts add the domain name twice
dmarc=fail while SPF passes SPF passes for the supplier’s domain, not yours (no alignment) Set up DKIM with your domain at that supplier, or a custom return-path on your domain
Forwarded mail fails SPF breaks on forwarding Expected; DKIM carries it. Make sure every message is DKIM-signed with your domain
Mailing list messages fail DMARC The list changes the subject or footer and breaks DKIM Most lists now rewrite the From address for p=reject domains; check the list settings

In short

  1. List every sender.
  2. One SPF record, under ten lookups, ending in -all.
  3. DKIM with 2048-bit keys for Microsoft 365 and every other sender, using the CNAMEs the cmdlet gives you.
  4. DMARC at p=none with np=reject, read the reports, then quarantine, then p=reject; test each step with t=y.
  5. Three records on every domain that doesn’t mail.
  6. DNSSEC and DANE for inbound mail.

Frequent errors and how to fix them

These are the messages people type into search when the module will not start. The exact wording is here so this page can be found.

The term 'Connect-ExchangeOnline' is not recognized
Module not installed, or the wrong host. Install-Module ExchangeOnlineManagement, then open a new window.
A window handle must be configured / MsalClientException
MSAL/WAM handler bug: no window for sign-in. As administrator: winget install --id Microsoft.PowerShell --source winget, then pwsh. ISE still hosts 5.1; the Exchange module does run there. Staying in ISE: Connect-ExchangeOnline -DisableWAM. Headless scripts: certificate or managed identity. Full explanation.
Error Acquiring Token: System.NullReferenceException / 0x80070520
WAM sign-in (module 3.7.0+) failed. NullReferenceException: another module in the same window (usually Microsoft Graph) loaded a different MSAL version; new window, Connect-ExchangeOnline first, or -DisableWAM. 0x80070520 "A specified logon session does not exist": PowerShell runs as another account or in Task Scheduler; start as yourself, or use a certificate. Full explanation.
WinRM cannot process the request / CreateExoPSSession
Legacy V1/V2 remoting. Use module V3+ and Connect-ExchangeOnline (no separate WinRM session).
AADSTS50076 / MFA required
Interactive: complete MFA. For scripts: certificate (app-only), not a user password.
Unauthorized / Access Denied
The account or app registration has no Exchange role (for example Exchange Administrator). App-only: Exchange.ManageAsApp plus that role.
Could not create SSL/TLS secure channel
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 — or use PowerShell 7.
Fail to create a runspace / connecting to outlook.office365.com
Legacy remote PowerShell. Update to ExchangeOnlineManagement V3; REST is the default. Disconnect-ExchangeOnline -Confirm:$false between jobs.
The term 'Get-Mailbox' is not recognized
You are not connected, or the REST session expired. Connect-ExchangeOnline again; do not mix with a leftover MSOnline session.

Questions about this, or need help setting it up? We are happy to think along.

Message on LinkedIn →