Before sitting down to write this, I did something simple: I queried the DMARC records of my own domains. Five domains, one dig loop, thirty seconds. The result forced me to change the plan for this article.
_dmarc.mustafaerbay.com.tr v=DMARC1; p=reject; pct=100; rua=mailto:mustafa@mustafaerbay.com.tr;
ruf=mailto:mustafa@mustafaerbay.com.tr; fo=1; aspf=r; adkim=r
_dmarc.sysroot.app v=DMARC1; p=reject; pct=100; rua=mailto:mustafa@mustafaerbay.com.tr;
ruf=mailto:mustafa@mustafaerbay.com.tr; fo=1; aspf=r; adkim=r
_dmarc.mustafaerbay.com v=DMARC1; p=quarantine; rua=mailto:postmaster@mustafaerbay.com;
ruf=mailto:postmaster@mustafaerbay.com; fo=1; adkim=s; aspf=s
_dmarc.burncpu.com v=DMARC1; p=quarantine; rua=mailto:postmaster@burncpu.com;
ruf=mailto:postmaster@burncpu.com; fo=1; sp=quarantine; adkim=s; aspf=s
_dmarc.hrmarge.com v=DMARC1; p=none
Five domains, four levels of policy maturity — the first two records are copies of each other. Two of them carry pct=100 — a tag the new DMARC specification, published in May 2026, deleted. One (hrmarge.com) says p=none but has no reporting address; in theory it says "I'm monitoring," in practice it monitors nothing. Two demand strict alignment (adkim=s; aspf=s), and the person who asked for that strictness — me — did not know which sending path it might break until I sat down and thought about it for this article.
I'm presenting this picture not as a confession but as the thesis itself: hardening DMARC is not a DNS editing job, it is the job of honestly inventorying who sends mail on your behalf. p=reject is a declaration that the inventory is complete. You write it at the end, not at the beginning.
The ground shifted in May 2026
Something has to go on the table before anything else, because nearly every DMARC guide on the internet still points at the old text.
RFC 7489, DMARC's reference document for a decade, is no longer current. Three Standards Track documents replaced it, all dated May 2026: RFC 9989 is the core protocol (obsoleting RFC 7489 and RFC 9091), RFC 9990 covers aggregate reporting, and RFC 9991 covers failure reporting. The answer to "what is DMARC" hasn't changed. The answer to "how do you roll out a DMARC record gradually" has.
The sharpest break: the pct tag is gone. One of RFC 9989's appendix sections is titled, plainly, "Removal of the 'pct' Tag." That means the ladder everyone has recommended for years — pct=10, then pct=50, then pct=100 — no longer has any basis in the specification.
The second big change is how the organizational domain is discovered. Receivers used to consult the Public Suffix List, a hand-maintained file; RFC 9989 replaces that with a DNS tree walk: labels are stripped from the left one at a time and queried going up the tree, and the document caps that process at eight queries so it can't be abused against the DNS. The lookup order is spelled out too: author domain first, then organizational domain, then public suffix domain.
Third, the t tag fills the gap pct left behind — and this is the real news. Its default is n. Writing t=y asks receivers not to apply your policy as declared, and RFC 9989 doesn't leave that vague: the receiver is expected to apply the policy one level below the one you declared. In the document's own example, if the policy is quarantine and t=y, failing messages get none; if the policy is reject and t=y, they get quarantine.
So it's wrong to think pct left nothing behind. It left this: instead of fiddling with percentages, you publish the target policy and hold it one notch down. That's where staging lives now.
⚠️
If your record still has
pct=100, don't panic. RFC 9989 instructs validators that "unknown tags MUST be ignored," so apctthe specification no longer processes won't break your record — it simply means nothing. But if you wrotepct=25and believe you are protecting a quarter of your mail, you are not protecting what you think you are.
What the tree walk changed for multi-label domains
This change has a particular meaning for domains like mine, so it's worth dwelling on.
In the old world, the answer to "what is the organizational domain" was read from the Public Suffix List. Getting mustafaerbay.com.tr right depended on that list recognising com.tr as a public suffix — meaning the boundary of your domain was drawn by a text file maintained by a third party and living nowhere in your DNS. Nobody notices while the list is current. The day it isn't, your subdomain policy gets applied to the wrong domain.
RFC 9989 moves that dependency into DNS. The query starts at the author domain, labels are stripped going upward, and in the document's own terms the walk stops when a record containing psd=n or psd=y is found. psd=n means "this is the organizational domain"; a psd=y record marks a registry operator's boundary and pushes the organizational domain one label lower. If no psd tag exists anywhere — the common case today, since the default is u — the record found at the name with the fewest labels is selected.
The practical consequence: the boundary is no longer an assumption at the mercy of a list, it's a fact visible in DNS. If you run a multi-label domain, you can nail the boundary down yourself by putting psd=n in your own record. I'll be doing that on mustafaerbay.com.tr and hrmarge.com; both are organizational domains and declaring it costs nothing.
One caveat about the price of the walk: every step is a DNS query. The document caps it at eight, but keeping the TTL on your _dmarc record needlessly short just generates pointless traffic on the receiver side. An hour is perfectly reasonable here.
Vendor documentation hasn't caught up
Now the awkward part. Microsoft's DMARC setup document for Microsoft 365 — the update stamp on it reads July 2026 — still cites RFC 7489 and still recommends exactly this ladder for a gradual rollout: pct=10, pct=25, pct=50, pct=75, pct=100.
So the specification deleted the tag while one of the world's largest mail receivers still has readers planning around it. I can't tell you what real receiver behaviour is today; I haven't measured it, and I don't write what I haven't measured. But building a migration plan on a tag the standard removed, with no guarantee that every receiver interprets it the same way, is a clearly bad bet in my view. Stage the rollout by scope, not by percentage.
Scope instead of percentage: the real ladder
If pct is gone, where does staging come from? From four levers: domain selection, sp, np, and the t tag above.
By RFC 9989's definition, sp applies only to existing subdomains of the organizational domain, not to the domain itself. np applies only to non-existent subdomains. If neither is present, p covers subdomains too. That trio gives you something useful: while your root domain is still at p=none, you can shut down imaginary subdomains you never send from — invoices.company.com and similar phishing bait — with np=reject. Near-zero risk, real gain.
One notch of caution: np depends on the subdomain genuinely not existing in DNS — on an NXDOMAIN response. If you run a wildcard DNS record (*.company.com), no subdomain ever returns NXDOMAIN and np never fires. That is almost always why this advice quietly does nothing in production.
Of my five records, only one has sp and none has np. That's the first thing I'll fix after publishing this.
One more detail about sp: RFC 9989 says sp is ignored for records published on subdomains of organizational domains and PSDs. Put a separate DMARC record on marketing.company.com with an sp tag in it and that tag goes nowhere — sp only means something in the organizational domain's own record.
Here's the order I'd use:
- Inventory. Who sends mail as your domain? ERP, CRM, billing, monitoring alerts, the newsletter tool, recruiting software, an IoT device, a forgotten cron job. You can't fill this list from memory; you fill it from reports.
-
p=noneplusrua. This isn't a policy, it's a telescope. Ap=nonewith no reporting address, like myhrmarge.comrecord, protects nobody and teaches you nothing. - Fix alignment. The longest phase by far. It gets its own section below.
-
Harden the edges. First
np=reject, thenp=quarantineon a low-volume subdomain. -
Rehearse the target policy with
t=y. Publishp=reject; t=yon the root and sit with it: what you declare isreject, what receivers are expected to apply isquarantine. If the reports stay clean you drop thet; if they don't, you back out without losing a single message. -
Drop the
t. Let the policy be applied as declared. If you got this far,p=rejectis no longer a guess.
No reports, no plan
The part of DMARC that does the real work is the feedback. Aggregate reports (rua) show you sending IPs, volumes and — most importantly — the cases where authentication passed but alignment did not. If you want reports delivered to a mailbox in a different organizational domain, that domain has to authorize it: RFC 9990 describes a verification record built with the _report._dmarc prefix, and requires the receiver to ignore the URI when the authorization is missing. Sending to your own subdomain needs no such step.
While writing this I checked my own records and found exactly the failure I was describing. sysroot.app sends its reports to a mailbox on mustafaerbay.com.tr — a different organizational domain. The required authorization record would be sysroot.app._report._dmarc.mustafaerbay.com.tr; I queried it, and it isn't there. So sysroot.app hasn't received a single aggregate report in months, and I didn't notice until I ran the query to fact-check my own article. Having p=reject in the record doesn't close that gap: hard policy, zero visibility.
As for failure reports (ruf), lower your expectations up front. RFC 9991 states outright that these reports can carry message headers and content fragments, and that large-scale providers therefore limit or entirely disable generating them. Microsoft says in its own documentation that Microsoft 365 doesn't send failure reports at all.
I'm not going to defend my own record here. Four of my records carry fo=1 together with a ruf address — RFC 9989 says fo is ignored unless ruf is also present, and mine has it, so the setting is live. I'm not passively carrying these reports, I'm actively asking for them: reports that can contain fragments of message headers, delivered to a mailbox I use every day. My inbox stays empty not by design but because most large receivers never send them. The right move is to drop ruf entirely or point it at a separate, restricted mailbox.
This has a concrete cost on my side too. The blog's newsletter goes out from notify-subscribers.mjs through my own mailcow install, using the SMTP_HOST and MAIL_FROM environment variables. The script's default MAIL_FROM is noreply@mustafaerbay.com — the record in that list with strict alignment and p=quarantine. When I had to change the sending side at the end of July, the first thing I worried about wasn't deliverability, it was alignment: under adkim=s; aspf=s the signing domain has to match From exactly, and even a subdomain won't save you. Becoming the victim of your own policy is the most common own goal in DMARC.
The real breaking point: alignment
Most people read DMARC as "just make SPF and DKIM pass." It isn't. DMARC doesn't ask whether SPF or DKIM passed; it asks whether the domain they passed for aligns with the domain in the From header. A message passes DMARC when at least one of those two alignments holds, and fails when neither does.
The classic failure looks like this: a SaaS provider sends on your behalf, sets the envelope sender to its own domain (bounce.provider.com), and signs DKIM with its own domain too. SPF passes, DKIM passes, DMARC fails. In an aggregate report you'll see this as pass under auth_results next to fail under policy_evaluated — every row where those two sit side by side is an alignment problem.
Alignment mode defaults to relaxed (r): if the organizational domains match, a subdomain difference is fine. Strict mode (s) requires an exact match. My mustafaerbay.com and burncpu.com records are strict, which effectively forbids any tool sending for those domains from using a subdomain. Strict isn't bad — but it should be a conscious choice. Mine wasn't a choice; it was inherited.
Then there's SPF's silent wall: RFC 7208 limits the terms requiring DNS lookups during SPF evaluation to 10, and recommends limiting void lookups to two. The cost of an include: isn't fixed — depending on what's inside it, it can be one lookup or eight. The day the limit blows, SPF returns permerror, DMARC's SPF leg collapses and DKIM is all you have left. Under p=reject, that's standing on one foot. I've written before about what a single DNS record can do to you in a different context; SPF limits are the email edition of that story.
Forwarding and mailing lists belong here too. When a message passes through a list, the envelope sender changes, so SPF alignment breaks; if the body wasn't modified, the DKIM signature survives. Answering "which lists does mail from my domain travel through" is the most tedious and most necessary step before moving to p=reject.
The accepted remedy on the receiving side is ARC: the intermediary signs the authentication result it saw on arrival and adds it to a chain, and a final receiver that trusts the intermediary can decline to apply the DMARC failure mechanically. Microsoft exposes this as a configurable "trusted ARC sealer." ARC gains you nothing directly as a sender — but if your legitimate mail travels through lists, how much p=reject actually hurts depends heavily on how seriously receivers take ARC.
What receivers ask for, and what you should ask of yourself
Some of the pressure already comes from outside.
Under Google's sender guidelines, senders of 5,000 or more messages a day must have both SPF and DKIM, must publish a DMARC record, and must have the domain aligned with either the SPF or the DKIM domain. Google's own wording is explicit: "Your DMARC enforcement policy can be set to none." They also ask that the spam rate reported in Postmaster Tools stays below 0.3%.
Microsoft uses the same number with a different definition: you count as a high-volume sender once you send 5,000 or more messages to Microsoft's consumer mail services using the same 5322.From domain. These thresholds and requirements don't live in the Microsoft 365 DMARC setup document but in Outlook.com's high-volume sender documentation — confusing the two is a common mistake. The error text published there leaves nothing to interpretation: 550 5.7.515 Access denied, sending domain <domain> does not meet the required authentication level. The requirements are again that SPF and DKIM pass, that a DMARC record is published (the example given is v=DMARC1; p=none), and that alignment holds.
The asymmetry matters: the big receivers ask you for the minimum, while what protects you is the maximum. p=none is enough to get through the door; the only thing that stops a phishing message sent in your domain's name is quarantine or reject. You harden for your own brand; the compliance list only sets the floor. My reflex on the brand side of email has always been the same: email security is less a compliance question than a reputation one.
Parked domains are the cheapest win
The cheapest win sits where nobody looks: domains that send no mail at all.
The ones you bought for brand protection, the typo variants you hold, leftovers from an old project — none of them send mail, so none of them has a legitimate flow to break. Microsoft's own documentation says directly that for these you should declare that no mail should ever come from them. This is the one place you can write p=reject without an inventory, without reading a single report, without annoying anyone.
To a phisher, an unused domain is worth more than a used one: nobody is reading reports for it, so the odds of getting caught are lower. One line, v=DMARC1; p=reject;, and that domain never helps fool anyone again.
Checklist
This is the list I'll be working through on my own records; it should fit yours as well:
- Run
dig +short TXT _dmarc.<domain>and look at what's actually there today. Make sure there is exactly one record: RFC 9989 says that if multiple DMARC records are returned for a single target, all of them are discarded. - If
pctis in the record, remove it. The specification no longer processes it, and leaving it there creates false confidence. - If there's no
rua, add one. If you send to a different organizational domain, set up the_report._dmarcauthorization too — thendigit to confirm it actually answers. - Don't make the report destination a personal mailbox; use a shared mailbox or a group.
- If you publish
ruf, know what you're asking for:foonly works whenrufis present, and the reports can carry message headers. If you don't need them, don't publishrufat all. - Add
np=reject— but check for a wildcard DNS record first; with one in place,npnever fires. - Rehearse the root with
t=y. One level below the declared policy is applied, and backing out costs nothing. - Count the DNS lookups in your SPF record. If you're near the limit of 10, move to separate sending subdomains instead of more
include:terms. - Choose your alignment mode deliberately. If you're going strict (
s), write down why. - Have bulk-sending tools speak from a dedicated subdomain rather than the root domain, so the root's reputation and policy stay independent.
- Leave the root for last. Don't relearn on the root what you could learn on a low-volume subdomain.
- Put the migration window in the calendar and actually read the reports during it. If you won't read them, stay at
p=none— at least that's honest.
Conclusion
The technical part of DMARC is three tags. The hard part is producing the list of every system that speaks in your domain's name. As long as that list is incomplete, p=reject isn't a security control — it's a deferred outage plan, and the outage arrives on the night it swallows your own invoice, your own newsletter or your own monitoring alert.
The May 2026 changes actually simplified this picture. With pct gone, so is the ability to tell yourself you're "protecting twenty percent of it." What's left is one clean question: who sends mail as this domain, and are they all aligned? If your answer is "I think so," your policy belongs at none. If your answer is a list of names, you've already earned the right to write reject.
I'm starting my own list today. Five domains, five missing np tags, two pointless pct tags, one p=none that reports to nobody, and one sysroot.app that publishes p=reject while never receiving a report. The best evidence for this article's thesis turned out to be the fault I found in my own records while writing it.
Official Sources
- RFC 9989 — Domain-Based Message Authentication, Reporting, and Conformance (DMARC)
- RFC 9990 — DMARC Aggregate Reporting
- RFC 9991 — DMARC Failure Reporting
- RFC 7208 — Sender Policy Framework (SPF), Version 1
- Microsoft — Set up DMARC to validate email in Microsoft 365
- Microsoft — Fix NDR error 550 5.7.515 in Outlook.com
Top comments (0)