Skip to content
Linux Running · 5 min read

DMARC Reports Explained: What the rua XML Actually Tells You

What DMARC aggregate reports are, how to read the XML, the external rua authorization record most people miss, and when to move from p=none to reject.

Every mail domain I set up gets a DMARC record. And for years most of them had p=none and no rua=, which means DMARC was doing exactly nothing. No policy, no reports, just a TXT record that makes checkers happy.

The reports are the whole point. They're the only way to see who is sending mail as your domain, and whether those mails pass SPF and DKIM at Gmail, Outlook and the rest. Without them, going to p=reject is a guess.

TL;DR: Add rua=mailto:... to your DMARC record. You'll get one gzipped XML file per receiver per day. Each <record> is one sending IP with a message count and the SPF/DKIM verdicts. If the report address is on a different domain, that domain needs a _report._dmarc TXT record, or a lot of receivers won't send anything. Watch for a couple of weeks, fix every legit sender that fails, then tighten the policy.

The record

_dmarc.example.com.  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@example.com; adkim=r; aspf=r"
  • p=none just watch, change nothing about delivery
  • rua= where the aggregate reports go
  • adkim/aspf relaxed alignment, so mail.example.com counts as example.com

Skip ruf= (forensic reports). Most big receivers don't send them anymore, and the ones that do send copies of real messages, which you probably don't want sitting in some mailbox.

Gotcha: rua on a different domain needs permission

This is the one that gets people, me included. A client's domain sent its reports to an address on a different domain I control, and I never set the authorization record on that domain.

The DMARC spec says a receiver should check that the report domain actually wants those reports. If your record for example.com says rua=mailto:reports@example.net, receivers look up:

example.com._report._dmarc.example.net.  TXT  "v=DMARC1"

No record there and you get nothing, or only from the receivers that don't check. And you don't get any error about it either, the reports just never arrive. If you collect reports for many domains in one place, a wildcard does it:

*._report._dmarc.example.net.  TXT  "v=DMARC1"

Same domain (rua=mailto:dmarc@example.com for example.com) doesn't need any of this.

What arrives

Usually once a day per receiver: an email with a .xml.gz or .zip attachment, filename like google.com!example.com!1729036800!1729123199.xml.gz (receiver, your domain, start and end of the window as Unix timestamps). Small domains get one or two a day. Busy ones get a pile.

Unpack one:

gunzip -c 'google.com!example.com!1729036800!1729123199.xml.gz' | less

Reading the XML

Cut down to the parts that matter:

<feedback>
  <report_metadata>
    <org_name>google.com</org_name>
    <date_range><begin>1729036800</begin><end>1729123199</end></date_range>
  </report_metadata>
  <policy_published>
    <domain>example.com</domain>
    <p>none</p>
  </policy_published>
  <record>
    <row>
      <source_ip>203.0.113.10</source_ip>
      <count>42</count>
      <policy_evaluated>
        <disposition>none</disposition>
        <dkim>pass</dkim>
        <spf>pass</spf>
      </policy_evaluated>
    </row>
    <identifiers><header_from>example.com</header_from></identifiers>
    <auth_results>
      <dkim><domain>example.com</domain><result>pass</result></dkim>
      <spf><domain>example.com</domain><result>pass</result></spf>
    </auth_results>
  </record>
</feedback>

One <record> = one sending IP + one combination of results. Read it like this:

  • source_ip + count: who sent, how many mails
  • policy_evaluated dkim/spf: the DMARC verdict, meaning passed and aligned with the From domain. This is the one that counts.
  • auth_results: the raw SPF/DKIM results and which domain they were for. When policy_evaluated says fail but auth_results says pass, it's an alignment problem: SPF passed for the newsletter tool's bounce domain, not for yours.
  • disposition: what the receiver did. With p=none always none.

DMARC passes if either aligned SPF or aligned DKIM passes. Forwarded mail usually breaks SPF but keeps DKIM, so DKIM is the one you really want working.

Quick summary from the shell

For a handful of reports, no tooling needed:

for f in *.xml.gz; do gunzip -c "$f"; done \
| grep -oE '<source_ip>[^<]+|<count>[^<]+|<dkim>[a-z]+</dkim>|<spf>[a-z]+</spf>' \
| sed -E 's/<[^>]+>//g' | paste - - - - | sort | uniq -c

Gives you IP, count, DKIM, SPF per line. Good enough to spot the one IP that fails everything.

The usual suspects when something fails

From my own domains, failures are almost never attacks. It's:

  • A tool sending as you: invoicing, CRM, newsletter, website contact form. Fix: add it to SPF or, better, set up DKIM for it with your domain.
  • The copier/scanner sending through some random relay. Fix: send via your own mail server with auth.
  • Forwarding: SPF fails, DKIM survives. Usually fine. If DKIM fails too, the forwarder modified the message (mailing lists love adding footers).
  • Your own server without DKIM on one of several domains. Check the SNI and per-domain setup while you're at it.

IPs you don't recognise at all, with tiny counts and failing everything: that's spoofing. That's exactly what p=reject will stop.

Moving the policy up

My order:

  1. p=none with rua, two to four weeks
  2. every legit sender passes aligned DKIM or SPF
  3. p=quarantine; pct=25, then 100
  4. p=reject

Don't skip step 1. Going straight to reject because a checker gave you a red badge is how invoices stop arriving and nobody notices for a week.

Gotcha: parse the XML safely

If you write your own parser for these (I did): the reports come from the internet, the address is public, anyone can send you an "XML report". Turn off DTD and entity handling in your parser. In Python, defusedxml instead of the plain xml.etree. I also just reject anything containing <!DOCTYPE or <!ENTITY, no real report has those. Cap the size and the number of rows too, someone can send you a fake report with millions of rows.

Or let something read them for you

Reading XML every morning gets old fast. There are self-hosted options (parsedmarc into Elasticsearch/Grafana works if you already run that stack, I covered monitoring stack choices separately) and plenty of hosted ones.

I ended up building a small one for my own domains because I wanted a weekly mail that says, in plain words, what changed. It's at sudowl.com/en/dmarc-monitor and free while it's in the test phase: you point rua at a generated address, it keeps the report rows for 90 days and mails you a summary once a week. My own domain is the first one running on it.

Whatever you use, the rule is the same: no rua, no idea. Add the tag today, even if you don't look at the reports for a week.


Related posts