ARC Sealing: Keeping Authentication Alive Through Forwarders and Mailing Lists
A message that passed SPF, DKIM and DMARC at the origin can still fail DMARC at the final MX after one list hop. ARC does not fix that break — it gives receivers a verifiable chain of custody to decide whether to trust the original verdict anyway. Here is who seals, who validates, and why a passing chain is only ever an input to local policy.
EvilMail TeamJuly 18, 202611 min read
A newsletter subscriber posts to your announce list. The message left the origin with spf=pass, dkim=pass, and a DMARC policy of p=reject that was fully aligned. It arrives at the final MX with spf=fail, dkim=fail (body hash mismatch), and dmarc=fail. Your receiver, doing exactly what you told it to, rejects a message that was legitimately authenticated one hop earlier.
Nothing is misconfigured. This is what store-and-forward does to SPF and DKIM, and no amount of retuning your DMARC record fixes it. ARC (Authenticated Received Chain, RFC 8617) is the mechanism that makes the break survivable — not by resurrecting SPF or DKIM, and not by overriding DMARC, but by giving each intermediary a way to cryptographically state "here is what authentication looked like when I received this, and here is proof I did not tamper with that verdict." The receiver still decides whether to believe it.
Why forwarding breaks authentication
There are two independent failures, and they are worth keeping separate.
ARC Sealing Explained: Preserve DMARC Through Forwarders & Lists — EvilMail Blog
SPF dies on the envelope.
SPF authorizes the sending IP against the domain in the
RFC5321.MailFrom
(the envelope return-path). A plain forwarder relays the message from *its own* IP but keeps the original envelope sender. So the receiver evaluates
origin.com
's SPF policy against the forwarder's IP, which is not in
origin.com
's SPF record, and you get
softfail
or
fail
. SRS (Sender Rewriting Scheme) is often cited as the fix, but SRS only rewrites the return-path so bounces route correctly — it changes the envelope domain to the forwarder's, which means SPF now passes for the *forwarder*, not for
origin.com
. That is not DMARC alignment. DMARC needs the passing mechanism to align with the
From:
header domain, and after SRS the SPF-authenticated domain is the forwarder's, not the author's.
DKIM dies on the body and headers. A mailing list is a content mutator by design. It prepends [list-name] to the Subject, appends an unsubscribe footer to the body, and adds List-Id, List-Unsubscribe, and friends. The DKIM signature commits to a body hash (bh=) and a specific set of signed headers (h=). Change one byte of the signed body and bh= no longer matches; rewrite a signed header and the signature is invalid. The DKIM check returns fail, and it is not recoverable — the receiver has no way to reconstruct the original bytes.
DMARC requires at least one aligned, passing mechanism — SPF *or* DKIM. After a list hop you have zero. That is the whole problem: two mechanisms that were both green at the origin are both red at the destination, and DMARC has nothing left to stand on.
What ARC actually is: three headers, one chain
ARC adds three header fields per hop, each tagged with an instance number i=N (i=1 at the first sealer, incrementing at every hop that reseals).
ARC-Authentication-Results (AAR) — a frozen copy of the Authentication-Results the sealer computed *at ingress*, before it touched anything. This is the important payload: it records that at hop N the message showed dkim=pass spf=pass dmarc=pass.
ARC-Message-Signature (AMS) — a DKIM-style signature over the message headers and body *as this hop forwards it*. Algorithmically it is DKIM: a=rsa-sha256, a body hash bh=, a signature b=, a selector s=, a domain d=. Its job is to let the next hop confirm the message was not altered after this seal was applied.
ARC-Seal (AS) — a signature over the ordered set of all prior ARC header fields (every AAR, AMS and AS before it, plus this instance's own AAR and AMS). This is the novel part. The AS has no `bh=` — it does not cover the body, it covers the chain. It is what binds instance i=2 to i=1 so the sequence cannot be reordered or spliced.
The chain-validation tag cv= lives on the AS. At i=1 it is cv=none (there is nothing before it to validate). At every later hop it is cv=pass if every prior seal verified, or cv=fail if any did not. Once a hop records `cv=fail`, it stays `fail` for the rest of the chain — a downstream sealer that inherits a broken chain seals cv=fail and cannot launder it back to pass. The chain is exactly as trustworthy as its weakest link.
If you already run DKIM, the mental model is: AMS is DKIM with a different header name, and AS is the genuinely new primitive.
Reading a real chain, top to bottom
Here is a two-hop chain as it arrives at the final MX. Read it bottom-up: i=1 is the mailing list's seal — the list saw the message while it was still intact — and i=2 is a downstream relay's seal, applied after the list had already rewritten the Subject and appended its footer.
The whole rescue lives in the two AAR fields. The i=2 AAR is honest: by the time the relay handled the message, the list had already broken it, so DKIM fails and the only passing SPF is for lists.example.org — which does not align with the From: domain, so dmarc=fail. That is exactly the failure this article opened with. But the i=1 AAR froze the truth the list saw at ingress, before it mutated anything: dkim=pass, spf=pass, dmarc=pass (p=REJECT). The cv=pass on the i=2 seal proves the chain is intact and that the i=1 record was not forged after the fact.
Sealing on your own infrastructure
Sealing needs a key, a DNS record, and a milter or module. The key and DNS record are identical in format to DKIM — ARC introduces no new record type.
The one ordering rule that trips people up: capture the AAR before mutation, apply the AMS and AS after mutation. Your pipeline must evaluate SPF/DKIM/DMARC on ingress, hold that verdict, let the list machinery rewrite the Subject and append the footer, and only then sign. Seal too early and the AMS commits to a body that no longer exists by the time it leaves — the next hop computes cv=fail and your seal is worse than useless.
Validating and trusting an inbound chain
Flip to the receiver's seat, because this is where ARC is most often misunderstood. Seeing arc=pass in Authentication-Results means exactly one thing: the chain is structurally intact and every seal verified. It says nothing about whether the *original* message passed. A spammer can stand up a domain, seal their own garbage with a perfectly valid cv=pass, and hand you arc=pass. Honoring that blindly is a gaping hole.
So the trust decision is local policy, and it has three conjuncts that must *all* hold before you override a DMARC fail:
1.arc=pass — the chain verified.
2.The outermost sealer (d= on the highest-instance AS) is on an explicit allowlist you maintain — Google, Microsoft, your own forwarders, the specific list operators you deal with.
3.The `i=1` AAR shows the original dmarc=pass (or an aligned SPF/DKIM pass). You must dereference it; arc=pass alone never tells you this.
This is why there is no global "honor all ARC" switch anywhere in the ecosystem and never will be. An arc=pass from a sealer you have never heard of carries no more weight than an unauthenticated message. Google and Microsoft seal by default, which is why mail forwarded *through* their infrastructure to your MX so often survives DMARC — you are implicitly trusting their sealer identity.
Checklist and gotchas
Publish an ARC selector as a TXT record reusing the DKIM key format (v=DKIM1; k=rsa; p=...). An existing DKIM key can back both.
Capture `Authentication-Results` at ingress, seal at egress. The AAR must reflect the message before your own mutations; the AMS and AS must cover the message after them.
If you forward, validate-and-reseal — never strip. Removing inbound ARC headers destroys the chain of custody your downstream depends on.
As a receiver, only honor `arc=pass` from an explicit sealer allowlist. Untrusted arc=pass is worthless.
Always dereference the `i=1` AAR before overriding DMARC. A passing chain around a failing original is not a reason to deliver.
Monitor `cv=fail` rates. A sudden spike almost always means a hop of yours is mutating content *after* it seals — the classic ordering bug.
Watch the small stuff. Clock skew on the t= timestamps can cause spurious verification failures; avoid l= body-length limits on the AMS, which invite footer-injection tricks; and a missing AAR means you can never recover the original verdict, so treat a chain without one as unusable.
ARC is not a DMARC bypass, and it is not a fix for broken SPF or DKIM. It is a chain of custody — proof of who touched a message and what they saw when they touched it. Deploy it thinking like a receiver deciding whom to believe, and it does exactly one job well: it lets legitimate forwarded and list mail survive a strict p=reject policy without opening the door to anyone who can run openssl genrsa.