SPF, DKIM, and DMARC in Microsoft 365 — the walk to p=reject
Most tenants published a DMARC record years ago and have sat at p=none
ever since. That's the norm, not negligence: p=none is the only policy you
can publish without first knowing every service that sends mail as your
domain, and that inventory is real work nobody scheduled. The DNS records
take ten minutes. This page is about the rest of the job.
The part checklists get wrong
"DMARC configured" and "DMARC enforced" are different controls, and the
frameworks score them separately. CISA SCuBA splits them explicitly:
MS.EXO.4.1 wants a syntactically valid record on every accepted custom
domain; MS.EXO.4.2 (with CIS M365 v6 2.1.10) requires p=reject on each of
them. A record reading p=none passes the first and fails the second, so a
checklist that ticks "DMARC: yes" at p=none is reporting the publish step
as the enforcement step. Same trap on SPF: CISA MS.EXO.2.2 is met only when
every domain's SPF record ends in -all — a ~all softfail still permits a
soft or neutral result at the receiver.
And "every accepted custom domain" means exactly that, including the parked
ones that send nothing. A domain that sends no mail is the easiest p=reject
you'll ever publish, and the one attackers check first.
The sender inventory is the actual work
Before any policy tightens, list everything that sends as your domain: Exchange Online, then the long tail — CRM, marketing platform, ticketing, payroll, monitoring alerts, the scan-to-email copier. For each one you need aligned SPF or aligned DKIM, and for most third-party platforms aligned DKIM (their console gives you CNAMEs for a custom signing domain) is the right answer, because stuffing their ranges into SPF walks you into the 10-DNS- lookup limit that silently breaks the whole record.
The record you can't guess at is the one that shows up in reports. Publish
p=none with aggregate reporting first (rua=mailto:..., ideally into a
report-parsing service rather than a mailbox nobody opens), and let two to
four weeks of reports find the senders the inventory missed.
The walk itself
- SPF: one record per domain,
v=spf1 include:spf.protection.outlook.com -allplus your verified third parties. The softfail argument (that~allis harmless once DMARC enforces) has some merit at receivers that evaluate DMARC, but not every receiver does, and the framework text asks for-allregardless. Hard fail costs nothing once the inventory is honest. - DKIM: add the two CNAME records per domain (M365 admin center → Settings → Domains → your domain → DNS records), then enable signing in the Defender portal → Email & collaboration → Policies & rules → Threat policies → Email authentication settings → DKIM. Enable it for the custom domains, not just the onmicrosoft.com default.
- DMARC:
_dmarc.yourdomain.comTXT,v=DMARC1; p=none; rua=...→ read reports → fix senders →p=quarantine(usepct=to ramp if the volume is scary) →p=reject. Addsp=rejectso subdomains don't inherit a hole. The tenants that get stuck are the ones with no named owner for the report-reading step; put a recurring 30-minute review on a calendar or the walk stops at step one.
What a renewal reviewer actually accepts
Not "DMARC is configured." A dated export of the three DNS records for every
accepted domain (p=reject visible in the DMARC record, -all visible in
SPF) plus a recent aggregate-report summary showing alignment pass rates.
The per-domain completeness is what reviewers have learned to probe: one
enforced primary domain and three forgotten p=none vanity domains reads as
an unfinished control, because it is.
How Calibrant checks this
As separate checks, matching the framework split: SPF hard-fail, DKIM
enabled, and DMARC at p=reject, each evaluated across every accepted
custom domain rather than just the primary. A published-but-p=none record
fails the enforcement check by design, so the readout can't report the
publish step as done. Findings land in a dated assessment report built for
the renewal exchange above.
Early access is by invite — code SEO-EMAIL-AUTH at calibrant.ai.