You did the work. SPF configured, DKIM keys published, DMARC policy at p=reject. Your DMARC reports show clean passes. You’re done. (If you haven’t moved your root policy to p=reject yet, see moving from p=none to p=reject safely first.)

Except two subdomain DMARC gaps can still hand attackers a working phishing address. RFC 9989 §4.7 says that when sp= is absent, subdomains inherit p= by default, so your root policy does apply in the common case. But two exploitable gaps remain, and attackers enumerate them deliberately.

The two gaps your root DMARC policy leaves open

Gap 1: a subdomain-specific record overrides the root. DMARC lookup checks _dmarc.billing.example.com before falling back to _dmarc.example.com. If a subdomain-specific record exists (left by a decommissioned SaaS integration, misconfigured by a vendor, or set during a forgotten pilot), it wins, regardless of what your root says. A p=none record on any subdomain is a clean override of your root p=reject.

The attack path via Gap 1:

  1. Attacker finds _dmarc.mail.example.com TXT "v=DMARC1; p=none" left from an old service
  2. Sends phishing email with From: mail.example.com
  3. Receiving server finds the subdomain record: policy is p=none
  4. Root fallback never applies, since the subdomain record takes precedence
  5. Email lands in the inbox

Gap 2: implicit policy relies on every receiver implementing the RFC default correctly. RFC 9989 §4.7 also covers non-existent subdomains, addresses like billing.example.com that resolve nowhere in DNS at all. The defaulting chain is explicit and normative: if the np= tag is absent, the policy from sp= (if present) or p= (if sp= isn’t set either) MUST be applied to those non-existent subdomains too, the same policy that already covers your existing subdomains. So once you’ve added sp=reject for Gap 1, non-existent subdomains inherit it automatically; you do not need a separate np=reject just to close this off. np= exists for the narrower case where you want different treatment for subdomains that exist versus ones that don’t, and most organizations don’t need that distinction. Either way, the whole chain (implicit sp= from p=, implicit np= from sp=) depends on every receiver implementing the RFC default correctly. Most major ones do; edge cases and misconfigured receivers exist. Setting sp= (and np=, if you need the distinction) explicitly makes the intent auditable and independent of receiver interpretation.

The sp= tag and what it does

RFC 9989 §4.7 defines the sp= tag to specify the policy applied to subdomains of the organizational domain when no subdomain-specific DMARC record exists.

Your current DMARC record probably looks something like:

v=DMARC1; p=reject; rua=mailto:...

With sp= added:

v=DMARC1; p=reject; sp=reject; rua=mailto:...

That one addition asks receiving mail servers to reject mail from any subdomain that has no record of its own. Receivers make the final call, but this is the strongest request you can publish.

The three values mirror the main p= tag: none (no action, monitor only), quarantine (spam folder), reject (ask receivers to refuse the mail).

Why sp=reject is the right default for most organizations

If your subdomains don’t send email, and most of them don’t, sp=reject is the correct setting. You’re not rejecting legitimate mail. You’re rejecting all mail, which is exactly right for subdomains that have no business sending email in the first place.

There is also a useful rollout pattern: set p=quarantine on the root while you’re still auditing your root-domain senders, but set sp=reject immediately for subdomains you know don’t send email. This lets you tighten subdomain policy ahead of root enforcement. Explicit sp= is the simplest way to decouple them, short of a subdomain-specific record.

The domains that need consideration are the ones that legitimately send email from a subdomain:

  • mail.example.com: sometimes used as a return-path domain by sending services
  • em.example.com or sends.example.com: common SendGrid and Mailchimp tracking domains
  • helpdesk.example.com: Zendesk, Freshdesk, or similar

For these, you have two options. The cleaner approach is to publish a full DMARC record at _dmarc.mail.example.com with the appropriate policy for that specific subdomain. The subdomain-specific record takes precedence over the fallback, so it won’t inherit sp=reject from the root. The messier approach is to set sp=quarantine at the root, slightly softer than reject, to give yourself room for subdomains that aren’t cleanly configured.

The practical recommendation: set sp=reject at the root, then audit which subdomains actually send email and give them their own records.

How to find your sending subdomains

Before touching your DMARC record, you need to know which subdomains send email. (Setting up a new app? See root domain or subdomain for app email first.) There are three places to look.

Your sending service dashboards. SendGrid, Amazon SES, Postmark, and similar services show you the domains and subdomains they’re authorized to send from. Check the verified sender domain list in each service you use.

Your DNS records. Look for CNAME entries pointing to third-party sending infrastructure. em.example.com CNAME something related to SendGrid or Salesforce Marketing Cloud is a sign that subdomain sends email. Look for SPF records on subdomains too. If mail.example.com has its own v=spf1 record, it’s sending email.

Your DMARC aggregate reports. If you’re receiving aggregate report XML, look for records where the header_from shows a subdomain. If billing.example.com is appearing in reports with pass results, that subdomain is in use. If it’s appearing with no results, it might be a spoofing target. See reading DMARC aggregate reports without drowning in XML for what the header_from field means.

DMARC Guardian groups reports by sender (the From: domain), so a subdomain that’s appeared in your aggregate reports shows up as its own row, separate from your root domain.

The wildcard subdomain problem

Even with sp=reject in place, there’s a related attack vector worth understanding: wildcard subdomains that your DNS legitimately resolves.

If your DNS has a wildcard A record (*.example.com pointing to some IP), every possible subdomain resolves. That doesn’t enable phishing directly (DMARC is about email authentication, not DNS resolution), but it does mean that any service you spin up under a subdomain will inherit sp=reject protection automatically, without you needing to remember to configure DMARC for it. Which is exactly the behavior you want.

What you’re protecting against is an attacker using a non-existent subdomain (billing.example.com with no DNS record) in the From: header. Receivers don’t need the subdomain to resolve. For a non-existent subdomain, RFC 9989 §4.7 applies np= if present, otherwise your sp=, otherwise p=. So sp=reject with no np= already covers it.

The fix is one DNS change

If you’ve confirmed which subdomains legitimately send email and given them their own DMARC records, the root fix is straightforward:

_dmarc.example.com  TXT  "v=DMARC1; p=reject; sp=reject; rua=mailto:your-rua-address"

If you’re not sure yet and want to audit first, add sp=quarantine as an intermediate step:

_dmarc.example.com  TXT  "v=DMARC1; p=reject; sp=quarantine; rua=mailto:your-rua-address"

This won’t block subdomain-spoofed mail outright, but it will push it to spam, which generates signals you can monitor while you identify whether any legitimate subdomain mail starts getting flagged.

While you’re auditing with sp=quarantine, you can still hard-reject the subdomains you already know don’t exist by adding np=reject alongside it. np= only applies to non-existent subdomains, so it never touches the existing-but-unconfirmed ones sp=quarantine is protecting you from breaking:

_dmarc.example.com  TXT  "v=DMARC1; p=reject; sp=quarantine; np=reject; rua=mailto:your-rua-address"

After publishing the change, wait 48 hours for DMARC aggregate reports to reflect the new policy. Review for any unexpected quarantine increases that might indicate a legitimate subdomain you missed. If you see nothing new in quarantine, move to sp=reject.

What this looks like in DMARC reports

The signal you’re looking for after adding sp=reject is subdomain mail appearing in aggregate reports with a reject disposition rather than none. If that mail was legitimate, a sending service you forgot about, it will appear with a volume spike in failing sources.

The riskier outcome is that nothing appears: the attacker-sent mail was being delivered without appearing in reports because a subdomain-specific p=none record (Gap 1) was silently overriding the root, or because a receiver wasn’t applying the RFC default (Gap 2). After adding sp=reject, the concern shifts from monitoring to confirmation: you want to verify that nothing got newly blocked that shouldn’t have been.

DMARC Guardian’s Reports tab shows the disposition on every record, and opening a source IP lists its records with an issues-only toggle on by default to skip the clean passes. After applying sp=reject, the none disposition for subdomain sources should drop to zero. Any remaining volume with reject disposition indicates mail that got blocked; review the source IPs to confirm it’s not a misconfigured legitimate sender.

sp= is one line. Deploy it today.

Most teams skip sp= because nothing prompts them to add it. The DMARC deployment guides focus on the root domain. The monitoring dashboards show root domain health. The subdomain gaps sit unaddressed in the background until someone points them out.

Adding sp=reject to your existing DMARC record takes five minutes. If your subdomains don’t send email, and you’ve checked, it’s zero risk. If they do send email, finding that out now, in your own audit, is significantly better than finding it out because a customer got a convincing fake invoice.


DMARC Guardian’s DNS page shows your published DMARC record for every domain you monitor, including whether sp= is set. A domain sitting at p=reject with no sp= is still worth a second look.

Check your DMARC, SPF and DKIM records, free, no signup →