The Undelegated Subdomain Trap: Why p=reject Alone Is Not Enough
Two Weeks of Spoofing Despite p=reject
A practitioner report surfaced on Reddit recently that should make anyone uneasy with the blanket advice of “just publish p=reject, you’re safe.” An organization had done everything right: rolled out DMARC, ran in monitoring mode for months, moved to p=reject, felt good about it. No sp= tag in the record — RFC 7489 clearly says the subdomain policy inherits from p= when sp= is absent.
Then this happened. An attacker found a subdomain that did not exist. It was not in DNS anywhere. Nobody had ever delegated it. It appeared in no inventory. And yet the attacker started sending finance-themed mail as billing.<clientdomain>. Some receivers blocked it correctly. Others, faced with a missing DMARC record at the subdomain label, did not interpret that as “walk up to the org domain” — they interpreted it as “no policy here, deliver.” The mail landed.
The spoofing went on for about two weeks before a customer forwarded a sample. The fix was obvious in hindsight. Before that, it wasn’t.
What the Spec Says — and What Receivers Actually Do
RFC 7489 is unambiguous on paper. If _dmarc.billing.example.com returns no answer, the receiver must walk up the DNS hierarchy and apply the policy from _dmarc.example.com. This is the so-called “tree walk,” and it ensures that a missing subdomain policy falls back to the org policy. If every receiver did this, sp= would be a redundant tag.
They don’t, reliably. Major providers like Google, Microsoft, and Yahoo implement tree walking largely correctly. Smaller mail services, spam-filter appliances, and some on-premise setups are a different story: they perform an exact lookup on _dmarc.<full-subdomain>, see NXDOMAIN, and conclude “no policy at this label.” Some implementations apply heuristics depending on whether SPF or DKIM happen to be present.
This inconsistency is exactly why the upcoming DMARCbis specification introduces a stricter tree-walk algorithm — and adds the new np= tag for non-existent subdomains as a separate directive. The IETF working group behind it knows the current state is ambiguous.
The “Wildcard DMARC” Trick — and Why It Doesn’t Work
Threads like that one reliably surface a supposed silver bullet: “Just publish a TXT record at *._dmarc.<yourdomain> with the same content as your org record — then every receiver finds an answer, no matter what label the attacker invents.”
It sounds elegant. It doesn’t work — for a plain DNS reason. A wildcard matches at exactly the label level where the * sits. *._dmarc.example.com synthesizes answers for names of the form <something>._dmarc.example.com. But the lookup a receiver actually performs for mail from billing.example.com is _dmarc.billing.example.com — here _dmarc is the leftmost label, not the rightmost. Those two names don’t line up: the wildcard *._dmarc.example.com simply does not cover _dmarc.billing.example.com. The receiver still gets NXDOMAIN.
Worse, the probe people use to “verify” such a wildcard — dig TXT _dmarc.<random-string>.<yourdomain> — queries that same uncovered name. It returns NXDOMAIN whether or not the wildcard is in the zone file. The record looks like it does something, and does nothing.
What about the “big” wildcard, *.<yourdomain>? That would technically answer a query for _dmarc.billing.example.com — but only as long as billing.example.com itself doesn’t exist (DNS wildcards don’t apply if a real node sits in between; so your existing subdomains, of all things, would be exempt). And it then answers every TXT query under the domain with your DMARC record: ACME challenges, subdomain-specific SPF records, verification tokens. That “wildcard returns non-DMARC data / DMARC record in the wrong place” configuration is, per dmarc.org, by far the most common cause of broken DMARC records worldwide. Not a viable path.
In short: there is no DNS record you can publish at your org domain that forces a receiver which never walks up to apply your policy to an invented label. If the receiver neither resolves _dmarc.billing.example.com correctly nor performs the tree walk, nothing can sit at a name that’s never queried.
What Actually Helps
1. sp=, or the p=reject you already have. An org record with p=reject covers subdomains for any receiver that implements RFC 7489 correctly — the tree walk lands at _dmarc.<yourdomain> and applies p=. An explicit sp=reject becomes more than cosmetic only when your org policy isn’t reject (e.g. p=quarantine; sp=reject). If p=reject is already there, sp=reject is a no-op for compliant receivers — but it does no harm. Against receivers that don’t perform the subdomain lookup at all, neither helps.
2. DMARCbis np=reject. Structurally the cleanest path: a dedicated tag for non-existent subdomains, plus the reworked, stricter tree-walk algorithm that removes today’s receiver divergence. The catch: DMARCbis is queued at the RFC Editor with publication expected this year, but receiver-side adoption is still thin — so far essentially only United Internet (GMX, mail.com, WEB.DE) has been observed emitting DMARCbis-format reports at all. Until Gmail, Microsoft 365, Yahoo and the rest ship the new algorithm, np= is future-proofing rather than enforcement. Setting it costs nothing.
3. Clean up your existing subdomains. For subdomains that do exist, there’s plenty you can do: give every actively sending subdomain its own correct _dmarc record (and don’t leave it at p=none), and put a lockdown record in front of every non-sending one — _dmarc.<sub> with v=DMARC1; p=reject;, plus v=spf1 -all and an empty DKIM entry. That protects your known labels — it does nothing against a label you never owned and never will inventory. Hygiene, not a cure.
What to Check Today
Check 1 — Org record: Is _dmarc.<yourdomain> at p=reject? If your org policy is weaker, add at least sp=reject. That closes the subdomain gap for every receiver that implements the spec correctly.
Check 2 — np=: Add np=reject. Mostly documentation today, but free, and the standard path forward once DMARCbis receivers are widespread.
Check 3 — Existing subdomains: Walk through the subdomains that actually exist. Does one send mail? Give it its own clean _dmarc record. Does it send none? p=reject + -all + empty DKIM. Invented, undelegated labels can’t be covered this way — that’s the residual gap, and it hinges on the receiving server’s behavior, not on your zone file.
Check 4 — Read your reports: Your aggregate reports show you which (sub)domain names actually appear in your name and how the big receivers treat them. An invented billing.<yourdomain> showing up in the reports is exactly the early warning that was missing for two weeks in the Reddit case — and the reason someone like us is reading along here.
An Honest Closing Note
The standard advice — “Move to p=reject and you’re done” — isn’t wrong, just incomplete: it relies on every receiver reading the spec the same way. They don’t, and they won’t until DMARCbis is broadly deployed.
What you can do about it is modest and unspectacular: keep p=reject, set sp=/np=, keep your existing subdomains tidy, watch the reports. What you should not do is rely on a wildcard record that, per the spec, can’t address the problem at all — however often forum threads sell it as the one trick. The honest answer here is, unfortunately, less clever than the convenient one.