<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Sentinely Blog</title>
    <link>https://sentinely.eu/blog/</link>
    <description>Notes on DMARC, email authentication, deliverability and keeping domains safe from spoofing.</description>
    <language>en</language>
    <lastBuildDate>Tue, 25 Aug 2026 21:47:13 GMT</lastBuildDate>
    <atom:link href="https://sentinely.eu/blog/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>How many IP addresses does your SPF record actually authorize?</title>
      <link>https://sentinely.eu/blog/how-many-ips-does-your-spf-authorize/</link>
      <guid isPermaLink="true">https://sentinely.eu/blog/how-many-ips-does-your-spf-authorize/</guid>
      <pubDate>Tue, 25 Aug 2026 21:47:13 GMT</pubDate>
      <description>Counting the addresses an SPF record authorizes is harder than summing its ip4 ranges. The four ways the naive count lies, worked on real ESP records.</description>
      <dc:creator>Valerio Bonaldi</dc:creator>
      <category>Email authentication</category>
      <category>DNS records</category>
      <category>ESPs &amp; vendors</category>
      <category>spf</category>
      <content:encoded><![CDATA[<p>Ask what your SPF record authorizes and you get an adjective. Broad. Permissive. <em>A larger attack surface.</em> Ask for a number and the conversation usually stops.</p>
<p>The number is obtainable. It is simply harder to get right than it looks, and almost every way of getting it wrong makes it bigger — which is the direction that sells alarm.</p>
<p>Start with the most widely deployed include there is:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>google.com        v=spf1 include:_spf.google.com ~all</span></span>
<span class="line"><span></span></span>
<span class="line"><span>_spf.google.com   v=spf1 ip4:74.125.0.0/16 ip4:209.85.128.0/17</span></span>
<span class="line"><span>                  ip6:2001:4860:4864::/56 ip6:2404:6800:4864::/56</span></span>
<span class="line"><span>                  ip6:2607:f8b0:4864::/56 ip6:2800:3f0:4864::/56</span></span>
<span class="line"><span>                  ip6:2a00:1450:4864::/56 ip6:2c0f:fb50:4864::/56 ~all</span></span></code></pre>
<p><em>Resolved August 2026. Provider ranges change without notice — resolve your own rather than trusting any number printed in an article, including this one.</em></p>
<p>The IPv4 half is school arithmetic. A <code>/16</code> is 2¹⁶ = 65,536 addresses, a <code>/17</code> is 32,768. Together: <strong>98,304 addresses</strong> entitled to send mail carrying that domain.</p>
<p>Then there is the IPv6 half, and this is where a count that looked easy starts lying. Six blocks. We will come back to them.</p>
<p>That was a <em>simple</em> record — one include, two IPv4 literals, no dynamic mechanisms. Here are the four ways the count goes wrong on everything else.</p>
<h2 id="rule-1-ip4-and-ip6-are-not-the-whole-record">Rule 1: <code>ip4:</code> and <code>ip6:</code> are not the whole record</h2>
<p>Some mechanisms do not carry addresses. They carry <em>instructions for finding addresses</em>, executed at the moment a message arrives.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>_spf.salesforce.com   v=spf1 exists:%{i}._spf.mta.salesforce.com -all</span></span></code></pre>
<p>Count the <code>ip4:</code> mechanisms in that record: zero. A tool that counts literals will report that this provider authorizes nothing at all — a number that is not merely imprecise but backwards.</p>
<p>What the record actually says is this. <code>exists:</code> builds a domain name out of the connecting IP address (<code>%{i}</code> is the macro for it), looks up an A record at that name, and authorizes the sender if one exists (RFC 7208 §5.7). Authorization is a per-address question, answered by a DNS zone you have no way to list. I tried it with a documentation address — <code>192.0.2.1._spf.mta.salesforce.com</code> returns NXDOMAIN, so that one is not authorized — but establishing the full set would mean asking the question 4.3 billion times.</p>
<p><code>a</code>, <code>mx</code> and <code>ptr</code> have the same shape: resolved at evaluation time, against records that can change between one message and the next. (<code>ptr</code> is a special case — RFC 7208 §5.5 is titled <em>“ptr” (do not use)</em> and says the mechanism should not be published. If you find one in your own record, that is a finding in itself.)</p>
<p>So an honest count does two things at once: it totals the literals, and it <strong>declares</strong> the mechanisms it could not enumerate. What comes out is a floor, never a total. A number that quietly omits the third kind is not a measurement.</p>
<h2 id="rule-2-ipv4-and-ipv6-do-not-add-up">Rule 2: IPv4 and IPv6 do not add up</h2>
<p>Back to those six IPv6 blocks. Take a bigger example — Microsoft 365’s sending infrastructure, which publishes both families:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>spf.protection.outlook.com</span></span>
<span class="line"><span>  v=spf1 ip4:40.92.0.0/15 ip4:40.107.0.0/16 ip4:52.100.0.0/15</span></span>
<span class="line"><span>         ip4:52.102.0.0/16 ip4:52.103.0.0/17 ip4:104.47.0.0/17</span></span>
<span class="line"><span>         ip6:2a01:111:f400::/48 ip6:2a01:111:f403::/49</span></span>
<span class="line"><span>         ip6:2a01:111:f403:8000::/51 ip6:2a01:111:f403:c000::/51</span></span>
<span class="line"><span>         ip6:2a01:111:f403:f000::/52 -all</span></span></code></pre>
<p>The IPv4 side totals <strong>458,752</strong> addresses. Now the IPv6 side: a single <code>/48</code> holds 2⁸⁰ addresses, and the five blocks together come to roughly 2.19 × 10²⁴.</p>
<p>Add the two halves and you get 2.19 × 10²⁴ — the same figure. The 458,752 IPv4 addresses do not survive the rounding; they are eighteen orders of magnitude too small to appear. A combined total is not a big number, it is the IPv6 number wearing a disguise, and the part an operator can actually reason about has been erased to produce it.</p>
<p>So: count IPv4 <strong>addresses</strong>, count IPv6 <strong>blocks</strong>, and never publish one figure for both. Anyone who tells you your SPF authorizes seventy-nine quintillion addresses has added the two families together and is quoting you the size of IPv6.</p>
<h2 id="rule-3-overlapping-ranges-have-to-be-merged-first">Rule 3: overlapping ranges have to be merged first</h2>
<p>Three distinct traps live here, and all three inflate.</p>
<p><strong>Ranges that are written separately but are one range.</strong> Look again at the Microsoft record above. It publishes six IPv4 literals, but <code>52.100.0.0/15</code>, <code>52.102.0.0/16</code> and <code>52.103.0.0/17</code> sit back to back: together they are the single unbroken run <code>52.100.0.0</code>–<code>52.103.127.255</code>, 229,376 addresses. There are six literals and <strong>four</strong> contiguous blocks. Report “six ranges” and you have counted the punctuation rather than the surface.</p>
<p><strong>The same provider counted twice.</strong> <code>sendgrid.net</code> already ends with <code>include:ab.sendgrid.net</code>. A domain that publishes both includes — an easy thing to end up with when two setup guides are followed a year apart — inherits that second record along both paths. Sum the literals naively and you get 238,592 addresses. Merge the intervals first and the true figure is <strong>237,056</strong>: the difference, 1,536, is exactly the sub-record counted a second time.</p>
<p><strong>Host bits that are not where you think.</strong> RFC 7208 §5.6 says only the high-order bits of a prefix are compared, so any bits below the prefix length in the published literal are simply irrelevant. Microsoft’s record for <code>_spf-b.microsoft.com</code> contains <code>ip4:207.46.22.98/29</code>. That does not authorize eight addresses starting at <code>.98</code>. It authorizes <code>207.46.22.96</code> through <code>207.46.22.103</code> — including <code>.96</code> and <code>.97</code>, and excluding <code>.104</code>. Take the literal at face value and every interval is misaligned at both ends, which quietly wrecks the merging in the previous paragraph.</p>
<p>Two different figures are at stake here and they are worth keeping apart. Ranges that merely sit next to each other inflate the count of <em>blocks</em> while leaving the address total correct — that is the Microsoft case. A provider inherited twice inflates the <em>addresses</em> themselves — that is the SendGrid one. Misread host bits corrupt both.</p>
<p>What the three have in common is direction. Every one of them makes the figure <em>larger</em>, so a count that skips the merge step is not neutral-but-rough; it is systematically biased towards alarm.</p>
<h2 id="rule-4-zero-and-i-dont-know-are-different-numbers">Rule 4: zero and “I don’t know” are different numbers</h2>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>example.com   v=spf1 -all</span></span></code></pre>
<p>That record authorizes exactly zero addresses, and that is a real, useful, defensible fact: it is what a domain that sends no mail should publish.</p>
<p>Now consider an <code>include:</code> that failed to resolve, or a branch abandoned because the record was already at the ten-lookup limit (RFC 7208 §4.6.4). The addresses behind it are not zero. They are <em>unknown</em>, and the honest output is a blank, not a digit.</p>
<p>The distinction sounds pedantic until you notice which way it fails. Rendered as <code>0</code>, an unknown reads as <strong>good news</strong> — the most permissive record in your estate, displayed as the tightest. If a tool cannot tell you when it failed to look, its zeros are worthless everywhere else too.</p>
<h2 id="the-better-question-how-many-of-them-have-ever-sent-anything">The better question: how many of them have ever sent anything?</h2>
<p>The authorized surface is a permissions list, and on its own it says nothing about what is actually in use. The interesting figure is the overlap with reality, and DMARC aggregate reports supply the other half: every report lists the source addresses that sent mail carrying your domain (RFC 9990 §3.1).</p>
<p>Cross the two sets and you get two useful numbers:</p>
<ul>
<li><strong>authorized ∩ observed</strong> — the addresses that genuinely send your mail;</li>
<li><strong>authorized − observed</strong> — the dead weight: addresses you vouch for that have not sent a message in months.</li>
</ul>
<p>Dead weight is almost always archaeology. The ESP you migrated away from and never removed. A mail server decommissioned two office moves ago. A monitoring appliance that stopped mailing when someone turned off its alerts. None of these announce themselves; the <code>include:</code> keeps resolving, the record keeps validating, and the addresses keep being authorized.</p>
<p>Two disciplines make that number honest, and both matter more than the arithmetic:</p>
<p><strong>No reports is not zero senders.</strong> If nothing arrived in the observation window, you have learned nothing about usage. Telling someone who configured reporting last week that none of their 98,304 authorized addresses sends mail would be absurd, and a tool that does it is subtracting from data it does not have. The correct output is silence about usage, not a large and frightening number.</p>
<p><strong>Count distinct sources, not messages.</strong> The question is how many authorized addresses are alive, so a server that sent a million messages counts once. Weighting by volume produces a big figure with no relationship to the surface it is supposed to describe.</p>
<h2 id="a-wide-spf-record-is-not-by-itself-a-mistake">A wide SPF record is not, by itself, a mistake</h2>
<p>This needs saying plainly, because the genre this article belongs to usually ends by implying the opposite.</p>
<p>Google’s 98,304 addresses are not carelessness. They are the size of a mail platform that delivers for millions of organisations, and any provider operating at that scale will authorize a comparable range. Nothing has gone wrong.</p>
<p>Nor is the figure a score. There is no threshold above which a record becomes bad, no benchmark to sit under, and it does not belong behind a red light. It is a measurement — the same category as the number of hosts on your network, which is worth knowing and meaningless as a grade.</p>
<p>It is also worth being precise about what the surface exposes. An authorized address is not an open door: it is a machine whose mail your domain will vouch for. The risk it represents is conditional — it matters if one of those machines is compromised, or if a range is released back to a cloud provider and re-leased to someone else. That is a real concern, and it is exactly why <em>provenance</em> beats <em>size</em>. A record authorizing 98,304 addresses belonging to one provider you actively use is in better shape than one authorizing 3,000 across six vendors, four of which you stopped paying two years ago.</p>
<h2 id="what-to-actually-do-with-the-number">What to actually do with the number</h2>
<p><strong>Remove providers you no longer use.</strong> This is the one intervention that is always correct, costs nothing, and shrinks both the surface and the lookup count. It requires no judgement call — only the list, and someone willing to say which entries are still real. Start with the dead weight.</p>
<p><strong>Give heavy senders their own subdomain.</strong> Marketing and transactional mail sent from <code>mail.example.com</code>, with its own SPF record, keeps the corporate domain’s record small and separately auditable. Your ESP’s ranges then sit behind a name whose only job is bulk mail, and a compromise of that surface does not carry the weight of your primary domain.</p>
<p><strong>Do not reach for flattening.</strong> Replacing <code>include:_spf.google.com</code> with the literal ranges it resolves to today does reduce your lookup count, and it is the standard advice for records that have hit the limit of ten. But it converts a live reference into a snapshot, and the provider carries on changing the ranges behind your back. When they add a range, your mail from it starts failing. When they <em>release</em> one — back to the cloud pool, on to another tenant — your record goes on authorizing addresses that now belong to a stranger. That second case is precisely the risk the phrase “attack surface” was invented to describe, and flattening manufactures it. It deserves an article of its own, but the short version is that it trades a limit you can measure for a liability you cannot see.</p>
<hr>
<p>Counting the surface takes an afternoon and a resolver. Knowing how much of it is still alive takes DMARC reports over a long enough window to catch your quarterly senders, and that is the half no record inspection can give you. <a href="https://sentinely.eu/pricing">Sentinely</a> does both — it measures the authorized surface, declares the parts it could not enumerate rather than rounding them to zero, and crosses the result against the sources your reports actually show, which is where the dead weight becomes visible. If the security framing is what brought you here, <a href="https://sentinely.eu/solutions/it-security/">that side of it lives here</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Your DMARC record is correct and no reports are arriving. Six causes, in order.</title>
      <link>https://sentinely.eu/blog/dmarc-record-correct-but-no-reports/</link>
      <guid isPermaLink="true">https://sentinely.eu/blog/dmarc-record-correct-but-no-reports/</guid>
      <pubDate>Mon, 24 Aug 2026 22:13:48 GMT</pubDate>
      <description>Your DMARC record is valid and no reports arrive. The six causes in checking order, including the one nobody mentions: your rua names someone else.</description>
      <dc:creator>Valerio Bonaldi</dc:creator>
      <category>Troubleshooting</category>
      <category>Aggregate reports</category>
      <category>DMARC</category>
      <category>DNS records</category>
      <content:encoded><![CDATA[<p>The record is right. You checked it three times, an online validator agrees, and the mailbox you pointed <code>rua=</code> at has been empty for a week.</p>
<p>Most published lists of reasons for this are unordered, which is the one thing such a list must not be. The checks have a natural sequence — each one makes the next meaningful — and doing them out of order means proving the same thing twice while the actual cause sits further down. Here is the sequence. Two questions come before it, because for a good share of readers the answer is that nothing is broken.</p>
<h2 id="two-questions-that-may-end-this-early">Two questions that may end this early</h2>
<h3 id="has-it-been-48-hours">Has it been 48 hours?</h3>
<p>Aggregate reports are not emitted as mail flows. Each report covers a <em>reporting period</em>, and RFC 9990 (§3.1.1.4) says the period will typically be a single UTC day beginning at 00:00 UTC. Generation and delivery happen after the period closes.</p>
<p>So if you published at 16:00 on Tuesday, the first period that includes your record ends at 00:00 UTC on Wednesday, and the report is built and sent some time after that. Twenty-four to forty-eight hours is the normal wait, and large receivers vary within it.</p>
<p>Two things stretch that window:</p>
<ul>
<li><strong>DNS caching.</strong> If the name <code>_dmarc.example.com</code> previously did not exist, resolvers cached its absence, not just its contents. Negative caching (RFC 2308) uses a lifetime derived from the zone’s SOA record, and a receiver that looked an hour before you published will not look again until it expires. Check your SOA minimum before assuming something is wrong.</li>
<li><strong>You cannot ask for faster.</strong> The <code>ri=</code> tag used to request a shorter reporting interval. RFC 9989 removed it (Appendix C.5.2), and the IANA registry now lists <code>ri</code> as historic. Publishing <code>ri=3600</code> changes nothing — it is an unknown tag, and unknown tags are ignored.</li>
</ul>
<h3 id="does-the-domain-actually-send-mail">Does the domain actually send mail?</h3>
<p>A receiver learns that you want reports only when it looks up your policy, and it looks up your policy only when it has received a message with your domain in <code>From:</code> (RFC 9990 §3.4).</p>
<p>No mail, no lookup, no report. If the domain is parked, or serves only a website, or was registered last month and has not sent anything yet, silence is the correct outcome and no amount of DNS work will change it. A domain that sends a handful of messages a day is a softer version of the same thing: you will get reports only from the receivers that both saw your mail and run a reporting programme, so a thin week is not evidence of a broken record.</p>
<p>If you are past both of these, work down the list.</p>
<h2 id="the-six-causes-in-the-order-to-check-them">The six causes, in the order to check them</h2>
<ol>
<li><strong>There is no DMARC record</strong> where receivers look.</li>
<li><strong>The record is there, but receivers discard it</strong> — most often a missing semicolon.</li>
<li><strong>The record is valid and has no <code>rua=</code> tag</strong>, or has one that is not a usable URI.</li>
<li><strong>The <code>rua=</code> is valid and is not yours</strong> — the reports exist, and go elsewhere.</li>
<li><strong>It is too early</strong> — covered above.</li>
<li><strong>They are arriving</strong> — and the problem is downstream.</li>
</ol>
<p>One command settles the first four:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>dig +short TXT _dmarc.example.com</span></span></code></pre>
<h3 id="1-no-record-where-receivers-look">1. No record where receivers look</h3>
<p>Policy discovery starts at the name formed by prepending <code>_dmarc</code> to the domain in the <code>From:</code> header (RFC 9989 §4.10.1). Not the apex, not <code>www</code>, not a CNAME to your provider’s marketing page.</p>
<p>The failure that produces the most confusion is a DNS panel that appends the zone for you. You typed <code>_dmarc.example.com</code> into the <em>name</em> field, the panel appended <code>.example.com</code>, and the record now lives at <code>_dmarc.example.com.example.com</code>, where nobody will ever look. The <code>dig</code> above returns nothing and the panel shows a record that looks perfect. Trust <code>dig</code>, not the panel.</p>
<p>There is a subtler version. If the mail is sent from a subdomain — <code>From: noreply@mail.example.com</code> — discovery looks at <code>_dmarc.mail.example.com</code> first, and only if there is no valid record there does it walk up to the organizational domain (RFC 9989 §4.10). Both outcomes are normal. What matters for your empty dashboard is that in the second case, the record that applies is the parent’s, and the reports for that mail go to the <strong>parent’s</strong> <code>rua</code>. Come back to this if you are watching a subdomain.</p>
<h3 id="2-the-record-is-there-and-receivers-discard-it">2. The record is there and receivers discard it</h3>
<p>Three ways a published record is thrown away before anyone reads your <code>rua</code>:</p>
<p><strong>More than one record.</strong> If a query returns multiple DMARC policy records for the same name, all of them are discarded (RFC 9989 §4.10). This is the standard aftermath of a vendor migration: the old record was never deleted and the new one was added beside it. <code>dig +short</code> shows both. Two correct records are worse than one.</p>
<p><strong><code>v=</code> is not first, or is not exactly <code>DMARC1</code>.</strong> The version tag must come first, and its value is case-sensitive. If it is absent, not first, or spelled differently, the entire record must be ignored (RFC 9989 §4.7). <code>v=dmarc1</code> is not a DMARC record.</p>
<p><strong>A missing semicolon.</strong> This one deserves its own section, because it is invisible and because of <em>how</em> it fails.</p>
<h2 id="one-character-the-missing-semicolon">One character: the missing semicolon</h2>
<p>These two records differ by a single <code>;</code>:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>v=DMARC1; p=none rua=mailto:dmarc@example.com</span></span></code></pre>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>v=DMARC1; p=none; rua=mailto:dmarc@example.com</span></span></code></pre>
<p>At a glance in a DNS panel they are indistinguishable. The first one gets you no reports at all, and here is the mechanism, because it is not what most people assume.</p>
<p>The grammar in RFC 9989 §4.8 defines a tag value as any printable character <em>except</em> the semicolon. A space qualifies. So does <code>=</code>. A receiver parsing the first record does not stop at the space: it reads the value of <code>p</code> as <code>none rua=mailto:dmarc@example.com</code>.</p>
<p>Two things are now true at once, and it is the second that hurts:</p>
<ul>
<li><code>p</code> holds a value that is not <code>none</code>, <code>quarantine</code> or <code>reject</code>, so it is not valid;</li>
<li><strong>there is no <code>rua</code> tag in the record at all</strong> — it was swallowed into the value of <code>p</code>.</li>
</ul>
<p>RFC 9989 §4.10.1 treats an invalid <code>p</code> gently, but the leniency has a condition. If a <code>rua</code> tag is present with at least one syntactically valid URI, the receiver behaves as though the record said <code>p=none</code> and carries on — which keeps reporting alive. Otherwise it “applies no DMARC processing to this message.”</p>
<p>The missing semicolon deletes exactly the tag that would have rescued it. That is why this typo is silent rather than noisy: it does not weaken your policy, it removes your reporting.</p>
<p>The same shape of error, less common, is a record whose tags are in the wrong order — <code>p=none; v=DMARC1; rua=…</code> — which fails on the version rule above.</p>
<p>And the reassuring counterpart, so you do not spend an afternoon on the wrong thing: a stray fragment that is not a <code>tag=value</code> pair is <em>not</em> fatal. RFC 9989 §4.8 says syntax errors in the rest of the record are discarded in favour of defaults or ignored outright. Untidy, worth cleaning, not the reason your dashboard is empty.</p>
<h2 id="3-no-usable-rua">3. No usable <code>rua=</code></h2>
<p>The tag is optional in the standard, and its absence is unambiguous: with no <code>rua</code>, receivers must not generate aggregate reports for the domain (RFC 9989 §4.7). A record can be perfectly valid, enforce <code>p=reject</code>, and tell you nothing.</p>
<p>Three ways the tag is present but unusable:</p>
<ul>
<li><strong>An address instead of a URI.</strong> <code>rua=dmarc@example.com</code> is not a DMARC reporting URI — a URI needs a scheme. It must be <code>rua=mailto:dmarc@example.com</code>. The omission is easy to make and hard to see, because an address without a scheme looks completely reasonable to a human reader.</li>
<li><strong><code>https:</code> only.</strong> Receivers are required to support <code>mailto:</code>; support for anything else is optional, and URIs in schemes a receiver does not support must be ignored (RFC 9989 §4.7). An HTTPS-only <code>rua</code> is a bet on each receiver.</li>
<li><strong>The old size limit.</strong> The <code>!10m</code> suffix is now obsolete syntax that reporters are told to ignore (RFC 9989 §4.8). Harmless if it is already there; pointless to add.</li>
</ul>
<h2 id="4-the-rua-is-valid-and-it-is-not-yours">4. The <code>rua=</code> is valid, and it is not yours</h2>
<p>This is the cause that survives every check above, and the one almost nobody looks for.</p>
<p>A syntax validator will confirm that <code>rua=mailto:dmarc@reports.vendor.example</code> is a well-formed reporting URI, because it is. What no validator can know is whether that address is <em>yours</em>. Only you know that. So the record passes every test, the tool you are paying for stays empty, and nothing anywhere reports an error — because from the protocol’s point of view nothing is wrong. The reports are being generated, on schedule, every day. They are going somewhere else.</p>
<p>Where the other address usually comes from:</p>
<ul>
<li>a previous DMARC vendor, after the subscription lapsed — cancelling a contract does not edit your DNS;</li>
<li>the agency or MSP that configured the domain years ago, pointing at their own aggregation account;</li>
<li>an internal mailbox on a mail system that has since been decommissioned.</li>
</ul>
<p>The check is to read the list yourself rather than trust a green tick. Run the <code>dig</code> above and read the <code>rua=</code> value for what it is: a comma-separated list of destinations. Ask, for each entry, <em>who receives mail at that address today?</em> If you cannot answer for one of them, you have found it.</p>
<p>Two things worth knowing before you edit:</p>
<p><strong>Being one of several is normal, so the fix is usually to add, not to replace.</strong> Every valid destination in the list gets its own copy; there is no primary. If you are moving between tools, run both for a week and compare, then remove the old one.</p>
<p><strong>If you are watching a subdomain, check the parent too.</strong> Per the walk described in cause 1, mail from a subdomain with no <code>_dmarc</code> record of its own is covered by the organizational domain’s record — so its reports arrive at the parent’s <code>rua</code> while the subdomain’s dashboard stays empty. Nothing is misconfigured; you are reading the wrong shelf.</p>
<h2 id="two-causes-that-are-not-in-the-record-at-all">Two causes that are not in the record at all</h2>
<h3 id="the-destination-is-on-another-domain-and-has-not-agreed-to-it">The destination is on another domain and has not agreed to it</h3>
<p>If the <code>rua</code> address sits under a different organizational domain from the one publishing the policy, receivers must confirm that the destination consented before sending anything (RFC 9990 §4). The reason is obvious once stated: without it, anyone could publish a record aiming a flood of reports at a victim.</p>
<p>The check is a DNS lookup the receiver performs. It takes the host from your <code>rua</code> URI, prepends <code>_report._dmarc</code>, and prepends the domain the policy came from. So a policy at <code>example.com</code> asking for <code>rua=mailto:dmarc@reports.example.net</code> produces a TXT query for</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>example.com._report._dmarc.reports.example.net</span></span></code></pre>
<p>and the answer must parse as a DMARC record with <code>v=DMARC1</code> first. If that record is missing, the URI must be ignored by the receiver generating the report (RFC 9990 §4). Silence again, with nothing broken anywhere you would think to look.</p>
<p>Three practical consequences:</p>
<ul>
<li><strong>The record goes in the destination’s zone, not yours.</strong> If your reports go to a third-party service, it is their record to publish, and most publish a wildcard — <code>*._report._dmarc.&lt;their domain&gt;</code> — which covers every customer at once. If your reports go to a mailbox on a second domain <em>you</em> own, nobody else is going to publish it for you.</li>
<li><strong>Same organizational domain, no verification needed.</strong> A policy at <code>_dmarc.mail.example.com</code> sending to <code>dmarc@example.com</code> does not trigger this at all.</li>
<li><strong>The destination has the last word.</strong> The confirming record may itself carry a <code>rua</code>, which overrides the one in your policy, restricted to the same host (RFC 9990 §4). Rare, but it is another way reports legitimately end up somewhere you did not choose.</li>
</ul>
<h3 id="the-mailbox-is-eating-them">The mailbox is eating them</h3>
<p>A report is an ordinary email with an attachment: an XML file that should be gzip-compressed, carried as <code>application/gzip</code> or <code>text/xml</code>, with a filename built as <code>receiver!policy-domain!begin-timestamp!end-timestamp.xml.gz</code> (RFC 9990 §3.5.2).</p>
<p>The subject line is fixed by the standard, which gives you something to search for:</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="text"><code><span class="line"><span>Subject: Report Domain: example.com</span></span>
<span class="line"><span>    Submitter: mx.example.net</span></span>
<span class="line"><span>    Report-ID: 1755820800-example.com@example.net</span></span></code></pre>
<p>Search the entire mailbox — spam, quarantine and archive included — for <code>Report Domain:</code>. If messages turn up, everything above is fine and your problem is downstream. If nothing turns up anywhere, the earlier causes are still in play.</p>
<p>What eats them, in rough order of frequency: attachment policies that strip or block compressed files; a rule that files them somewhere nobody looks; a shared mailbox whose quarantine nobody reviews; and an <code>rua</code> address that does not actually accept mail, which produces bounces you never see because they go to the receiver, not to you.</p>
<p>One detail that saves an argument with your mail team: report traffic is itself DMARC-authenticated — RFC 9990 §3.5.2 requires these streams to produce an aligned <code>pass</code>. When reports land in quarantine, that is a local filtering decision, not an authentication failure.</p>
<h2 id="they-arrive-and-the-dashboard-is-still-empty">They arrive, and the dashboard is still empty</h2>
<p>If you found the messages, the remaining causes are all downstream of delivery.</p>
<p><strong>You are looking at the wrong domain.</strong> A separate report is generated for each policy domain encountered in the period (RFC 9990 §3.1). Open one of the XML files and read <code>policy_published/domain</code>: that is the shelf the data was filed under. If it says <code>example.com</code> and you are looking at <code>mail.example.com</code>, see cause 4.</p>
<p><strong>Duplicates were removed, correctly.</strong> When a generator re-sends a report it must reuse the original filename, and the <code>Report-ID</code> exists so consumers can recognise and ignore duplicates (RFC 9990 §3.5.2). A report that arrived twice appearing once is the system working.</p>
<p><strong>Ingest is on a schedule.</strong> Reports are fetched and parsed in batches, not the instant they land. If the first one arrived twenty minutes ago, give it an hour before opening a ticket.</p>
<hr>
<p>While you wait for a first report, you do not have to wait to learn anything. A report summarises a day of mail; a single message’s headers tell you what one receiver concluded about one message, right now. If you can send yourself a test message, paste its headers into our <a href="https://sentinely.eu/tools/email-header-analyzer/">email header analyzer</a> — it reads the receiver’s own declarations, needs no account, and keeps nothing.</p>
<p>And once the reports do arrive, the thing worth watching is not only that your record still parses, but that its <code>rua</code> still names you. That check is what <a href="https://sentinely.eu/pricing">Sentinely</a> does continuously, which is the difference between finding out today and finding out at the next audit.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Your DMARC report says &quot;fail&quot;. Most of the time, nothing is wrong.</title>
      <link>https://sentinely.eu/blog/dmarc-report-fail-but-delivered/</link>
      <guid isPermaLink="true">https://sentinely.eu/blog/dmarc-report-fail-but-delivered/</guid>
      <pubDate>Mon, 24 Aug 2026 21:50:30 GMT</pubDate>
      <description>In a DMARC aggregate report, a failed alignment and a blocked message are two different things. Here is the rule that separates them, the three ordinary explanations behind most &quot;fail&quot; rows, and the signals that mean someone really is forging your domain.</description>
      <dc:creator>Valerio Bonaldi</dc:creator>
      <category>DMARC reports</category>
      <category>Aggregate reports</category>
      <category>Alignment</category>
      <category>DMARC</category>
      <content:encoded><![CDATA[<p>You published <code>v=DMARC1; p=none; rua=mailto:...</code> a few weeks ago, the XML started arriving, and now you are looking at your first summary. A large share of your own mail is marked <code>fail</code>. The obvious question is whether someone is forging your domain.</p>
<p>Almost certainly not. What you are looking at is the word <code>fail</code> used for two different things, and the tools that show you a single “failure rate” usually merge them. One of the two means a message was blocked. The other means a message was delivered normally, by a receiver that was perfectly happy with it. Telling them apart takes about a minute per row, and the rest of this article is that minute.</p>
<h2 id="two-different-things-called-fail">Two different things called “fail”</h2>
<p>A DMARC aggregate report (the format is specified in RFC 9990) is not a list of messages. It is a list of <em>groups</em>: each <code>&lt;record&gt;</code> covers a batch of messages that arrived from the same source IP with the same authentication outcome, and <code>&lt;count&gt;</code> says how many messages that batch represents. Any percentage you see anywhere is weighted by that number.</p>
<p>Inside each record there are three layers, and they answer three different questions.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="xml"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">record</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">row</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">source_ip</span><span style="color:#E1E4E8">>203.0.113.47&#x3C;/</span><span style="color:#85E89D">source_ip</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">count</span><span style="color:#E1E4E8">>128&#x3C;/</span><span style="color:#85E89D">count</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">policy_evaluated</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">disposition</span><span style="color:#E1E4E8">>none&#x3C;/</span><span style="color:#85E89D">disposition</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">dkim</span><span style="color:#E1E4E8">>pass&#x3C;/</span><span style="color:#85E89D">dkim</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">spf</span><span style="color:#E1E4E8">>fail&#x3C;/</span><span style="color:#85E89D">spf</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">policy_evaluated</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">row</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">identifiers</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">header_from</span><span style="color:#E1E4E8">>example.com&#x3C;/</span><span style="color:#85E89D">header_from</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">identifiers</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">auth_results</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">dkim</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">domain</span><span style="color:#E1E4E8">>example.com&#x3C;/</span><span style="color:#85E89D">domain</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">selector</span><span style="color:#E1E4E8">>s1&#x3C;/</span><span style="color:#85E89D">selector</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">result</span><span style="color:#E1E4E8">>pass&#x3C;/</span><span style="color:#85E89D">result</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">dkim</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">spf</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">domain</span><span style="color:#E1E4E8">>fwd.example.net&#x3C;/</span><span style="color:#85E89D">domain</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">      &#x3C;</span><span style="color:#85E89D">result</span><span style="color:#E1E4E8">>pass&#x3C;/</span><span style="color:#85E89D">result</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;/</span><span style="color:#85E89D">spf</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">auth_results</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#85E89D">record</span><span style="color:#E1E4E8">></span></span></code></pre>
<p><strong><code>&lt;auth_results&gt;</code> — did the protocols verify at all?</strong> These are the raw outcomes, uninterpreted. Here SPF returned <code>pass</code>, but look at the domain it passed <em>for</em>: <code>fwd.example.net</code>, not <code>example.com</code>. DKIM verified a signature carrying <code>d=example.com</code>.</p>
<p><strong><code>&lt;policy_evaluated&gt;/&lt;spf&gt;</code> and <code>/&lt;dkim&gt;</code> — did they verify <em>and</em> line up with the <code>From:</code> domain?</strong> This is the DMARC view, and it has only two values: <code>pass</code> or <code>fail</code>. SPF verified for someone else’s domain, so from DMARC’s point of view it is <code>fail</code>. DKIM verified for your domain, so it is <code>pass</code>.</p>
<p><strong><code>&lt;policy_evaluated&gt;/&lt;disposition&gt;</code> — what did the receiver actually do?</strong> Three values: <code>none</code> (delivered), <code>quarantine</code>, <code>reject</code>.</p>
<p>The record above contains the word <code>fail</code>, and nothing whatsoever went wrong. DMARC passes when <em>at least one</em> aligned method passes — the aligned DKIM signature was enough — so the message was authenticated, delivered, and correct. If your dashboard charts an “SPF pass rate” and you watch that number, this record will worry you every single day for no reason.</p>
<h2 id="the-rule">The rule</h2>
<p>Here is the whole thing, and it is worth keeping somewhere you can paste it.</p>
<blockquote>
<p>A group of messages in an aggregate report is a <strong>real</strong> failure if and only if:</p>
<p><strong>(A)</strong> the receiver blocked it — <code>disposition</code> is <code>quarantine</code> or <code>reject</code>; <strong>or</strong></p>
<p><strong>(B)</strong> it was delivered (<code>disposition</code> is <code>none</code>), the policy in force for that report was <code>p=none</code>, <strong>and</strong> both raw results in <code>&lt;auth_results&gt;</code> are exactly <code>fail</code>.</p>
<p>Everything else belongs in your volume, not in your failures.</p>
</blockquote>
<p>Case (A) needs no interpretation. The receiver made a decision and told you about it. It does not matter what the raw results were and it does not matter what your policy says: if <code>disposition</code> is <code>quarantine</code> or <code>reject</code>, mail of yours did not reach an inbox, and that deserves attention whether the cause is an attacker or a misconfigured vendor.</p>
<p>Case (B) is the one nobody writes down. The message was delivered, both protocols failed outright, and the only reason it landed in an inbox is that you have not asked anyone to stop it yet. That is latent spoofing: it looks harmless today and becomes a blocked message the day you move to enforcement.</p>
<p>Two details in case (B) are easy to get wrong.</p>
<p><strong>The policy that matters is the one in that report, not the one in your DNS today.</strong> Each report carries the policy the receiver saw, in <code>&lt;policy_published&gt;/&lt;p&gt;</code>, and it describes a window that has already closed — usually the previous day. If you changed your record on Tuesday, Monday’s reports still describe Monday’s policy. Read <code>p</code> from the report you are holding.</p>
<p><strong>Only the exact value <code>fail</code> counts.</strong> RFC 9990 allows several other values in <code>&lt;auth_results&gt;</code> — <code>none</code>, <code>neutral</code>, <code>policy</code>, <code>temperror</code>, <code>permerror</code>, and for SPF also <code>softfail</code> — and none of them is proof of anything. A <code>temperror</code> means the receiver could not complete the check. A <code>softfail</code> means your own record asked for indecision. Treating those as failures is how a monitoring tool invents problems that do not exist.</p>
<h2 id="why-case-b-requires-pnone-exactly">Why case (B) requires <code>p=none</code> exactly</h2>
<p>This is the part that gets skipped, and skipping it inverts the meaning of the row.</p>
<p>Suppose your domain is already at <code>p=quarantine</code>, and a report shows a batch that failed both methods and was still delivered, <code>disposition=none</code>. Under a naive reading that is worse — the spoofing got through anyway. It is the opposite. You asked the receiver to quarantine that mail, and the receiver chose not to. RFC 9989 is explicit that final handling is always the receiver’s local decision; the protocol expresses a preference, not a command. A delivered message on an enforcing domain therefore means an override was applied: a local allowlist, a trusted-forwarder rule, or — on records that still carry the legacy <code>pct</code> tag, which RFC 9989 removed — a sampling decision.</p>
<p>Enforcement exists on that domain. The receiver looked at your policy and made an exception to it. That is a different situation from “no enforcement exists yet”, and it is not latent spoofing.</p>
<p>The same reasoning covers the third case: if you do not know which policy was published, case (B) does not apply at all. You cannot conclude “delivered only because enforcement was missing” without knowing whether enforcement was there.</p>
<h2 id="sometimes-the-receiver-tells-you-why">Sometimes the receiver tells you why</h2>
<p>When a receiver applies an override, it may say so. <code>&lt;policy_evaluated&gt;</code> can carry one or more <code>&lt;reason&gt;</code> elements, and RFC 9990 defines five types: <code>local_policy</code>, <code>mailing_list</code>, <code>other</code>, <code>policy_test_mode</code>, and <code>trusted_forwarder</code>. Some reporters add a free-text <code>&lt;comment&gt;</code> as well.</p>
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="xml"><code><span class="line"><span style="color:#E1E4E8">&#x3C;</span><span style="color:#85E89D">policy_evaluated</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">disposition</span><span style="color:#E1E4E8">>none&#x3C;/</span><span style="color:#85E89D">disposition</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">dkim</span><span style="color:#E1E4E8">>fail&#x3C;/</span><span style="color:#85E89D">dkim</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">spf</span><span style="color:#E1E4E8">>fail&#x3C;/</span><span style="color:#85E89D">spf</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;</span><span style="color:#85E89D">reason</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">    &#x3C;</span><span style="color:#85E89D">type</span><span style="color:#E1E4E8">>mailing_list&#x3C;/</span><span style="color:#85E89D">type</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">  &#x3C;/</span><span style="color:#85E89D">reason</span><span style="color:#E1E4E8">></span></span>
<span class="line"><span style="color:#E1E4E8">&#x3C;/</span><span style="color:#85E89D">policy_evaluated</span><span style="color:#E1E4E8">></span></span></code></pre>
<p>That is a receiver telling you, in the report, that it recognised a mailing list and decided not to hold your policy against it. It is the most useful field in the format, and it is optional — most records will not have it, but when it is there it saves you the investigation. Reporters also move more slowly than the specification: until 2026 the reference was RFC 7489, whose list of reasons also included <code>forwarded</code> and <code>sampled_out</code>, and you will still see both in the wild.</p>
<h2 id="the-three-ordinary-explanations">The three ordinary explanations</h2>
<p>Most of the alarming volume comes from three situations.</p>
<p><strong>Automatic forwarding.</strong> A user forwards their mail elsewhere, or an old alias still points offsite. The forwarding server sends from its own IP, which is not in your SPF record, so SPF no longer aligns with your domain. If the forwarder passes the message through untouched, the DKIM signature survives, DMARC passes on DKIM, and the message is delivered. In the report: <code>&lt;spf&gt;fail&lt;/spf&gt;</code>, <code>&lt;dkim&gt;pass&lt;/dkim&gt;</code>, disposition <code>none</code>. <strong>What to do: nothing.</strong> This is precisely what having two authentication methods is for.</p>
<p><strong>Mailing lists.</strong> A list adds a subject prefix or a footer, which invalidates the DKIM signature over the modified body, and rewrites the envelope sender to its own domain, which moves SPF away from yours. Both methods fail alignment, and the message is delivered anyway because the receiver recognises the pattern. In the report: both <code>fail</code>, disposition <code>none</code>, sometimes with <code>&lt;reason&gt;&lt;type&gt;mailing_list&lt;/type&gt;</code>. <strong>What to do: nothing.</strong> The mechanism designed to carry authentication across this kind of rewrite is ARC (RFC 8617), and implementing it is the list’s job, not yours.</p>
<p><strong>A vendor signing with its own domain.</strong> Your invoicing platform, CRM or newsletter tool sends on your behalf and signs with <code>d=vendor.example</code>, not <code>d=example.com</code>. Look at <code>&lt;auth_results&gt;</code>: the DKIM <code>result</code> is <code>pass</code>, but <code>&lt;domain&gt;</code> is the vendor’s. Raw success, no alignment, so <code>&lt;policy_evaluated&gt;/&lt;dkim&gt;</code> is <code>fail</code>. <strong>This one needs action</strong> — not urgent action, but real action. Ask the vendor for DKIM signing under your own domain (almost always a CNAME pointing at a selector they give you) and, if they send with an envelope sender in your domain, make sure their <code>include:</code> is in your SPF record. Until then that traffic has no aligned identifier at all, and it is exactly the traffic that will break on the day you enforce.</p>
<h2 id="the-records-that-are-neither">The records that are neither</h2>
<p>There is a fourth shape, and it fits neither bucket cleanly: SPF <code>fail</code>, and no <code>&lt;dkim&gt;</code> element at all, because nothing signed the message.</p>
<p>The rule above does not count these as failures, deliberately. A missing DKIM element is not a DKIM failure — it is the absence of evidence on that side, and absence of evidence is not proof. But these records are not “fine” either, and they deserve their own queue, because on a domain at <code>p=none</code> there are exactly two explanations and they look identical in the report:</p>
<ol>
<li>someone is forging your domain, or</li>
<li>a legitimate sender of yours has no authentication at all — a form plugin on a small VPS, a monitoring script, a departmental application nobody remembers deploying.</li>
</ol>
<p>The report cannot tell you which. The source IP usually can. Work through this list <em>before</em> you raise your policy, because these are the messages that will start disappearing the moment you do.</p>
<h2 id="when-it-really-is-spoofing">When it really is spoofing</h2>
<p>The signals, in the order they are worth checking:</p>
<ul>
<li><strong>The source IP has no relationship to you.</strong> Not your provider, not your office, not a vendor you can name. This is the strongest signal there is, and the only one that stands on its own.</li>
<li><strong>Nothing passes, raw.</strong> No aligned DKIM signature, and no SPF pass for any domain you recognise.</li>
<li><strong>The sending infrastructure does not look like a mail system.</strong> No usable reverse DNS, hosting ranges rather than mail ranges, a source that appears once and never returns.</li>
<li><strong><code>&lt;header_from&gt;</code> is your domain while everything else belongs to someone else.</strong> That is the definition of the attack DMARC was built to describe.</li>
</ul>
<p>There are deliberately no thresholds here. “More than X% from a single IP” is the kind of number that gets copied from article to article and tells you nothing about your own domain: a legitimate email service provider concentrates enormous volume on very few addresses. Judge sources, not percentages.</p>
<h2 id="what-to-do-with-the-number">What to do with the number</h2>
<p>Do not raise your policy to make a number go down. It is the wrong number, and raising the policy will not do what you expect it to.</p>
<p>Work in this order instead. Identify every source in your reports until none is unexplained — that is the real work, and on a domain of any age it takes weeks, not an afternoon. Fix alignment for the legitimate ones, starting with the vendors signing under their own domain. Then move the policy, and not before.</p>
<p>And know what happens to your dashboard when you do. A batch of latent spoofing at <code>p=none</code> is case (B): delivered, counted as a failure. At <code>p=reject</code> that same batch becomes case (A): rejected, still counted as a failure. Your failure count does not have to fall for enforcement to be working — the mail stopped reaching inboxes, which was the entire point. What should fall is the volume you cannot explain.</p>
<p>Aggregate reports summarise; they never contain the message itself. When one specific message is bothering you and you want to know what the receiving server actually concluded about it, paste its headers into our <a href="https://sentinely.eu/tools/email-header-analyzer/">email header analyzer</a>: it reads the receiver’s own declarations, needs no account, and keeps nothing.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
