Why DKIM fails when the record is already there
Absent, present-but-stale and never actually measured all show up as one failure. Two of the three get worse if you do the obvious thing, and a fourth is not a record problem at all.
Yowl tells you which of these three you have. That is the whole feature: it measures every record your provider currently asks for, one at a time, and reports present and matching, absent, present but not the value wanted, or we could not read it.
So a key that is real but stale reads as stale, and you are told to replace it rather than to add it again — which is the difference between fixing it in one move and pasting the same dead value four times. When a lookup fails, we say the lookup failed and ask you for nothing.
You get the diagnosis, not a red cross
Here is what comes back per record, with the value your provider wants beside the value your DNS is actually serving:
{ "type": "TXT", "name": "resend._domainkey",
"expected": "p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8MGqVPX+vBaw4w+A4tyIv...",
"published": ["p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8MGqVPX+vBaw4w+A4tyIv..."],
"state": "ok" }
Alongside it: a flag for the stale-key case specifically, so it is called by name rather than left for you to infer. The DNS host we detected, and what that host does to you — Cloudflare proxies records by default and mail records must be unproxied, which is its own afternoon. And an append-only trail of every step this domain has been through with us, timestamped, so “it was working last week” is a claim you can check.
Ask it in English. Connect Claude to the organisation over MCP and “why won’t our DKIM verify?” reads all of that and answers in the terms below — which one of the three it is, and what to do about it. The loop this page describes is a person re-pasting a key because nothing told them the key was the wrong one; that loop does not survive being asked out loud.
The three record bugs, and a fourth that is not one
Someone adds the DKIM record their provider gave them. The dashboard keeps saying not verified, so they go round the loop again: re-paste the key, wait, re-check. The record is there. They can dig it themselves. Nothing moves.
Three different bugs produce that exact symptom, and two of them get worse if you do the obvious thing. A fourth produces it while the record is genuinely perfect, and it is last on this page because it is the one people reach for the DNS panel over anyway. Setting DKIM up in the first place is a separate and much shorter job.
One: present, but not the key they want now
This is the one that sends people in circles. The key in DNS is a real, valid, well-formed DKIM key. It is simply not the key this registration is signing with — the domain was set up against an earlier registration, or against a reservation that has since been reaped and reissued.
From outside it is indistinguishable from never having added the record at all. Verification fails either way. But the fix is the reverse: one person has to add a record, the other has to replace one they will tell you, correctly, is already there and looks perfectly good. A checker that reports both as “not found” sends the second person back to re-paste the same stale value, which is why they can do it four times and get four identical results.
So the useful question is not “is a key published?” but “does the published key match the key this signer is using right now?” Read the s= tag out of a real message’s DKIM-Signature, look up that exact selector, and compare. A leftover key at an older selector name is not this bug — that is litter, harmless until someone tidies DNS and deletes the wrong one.
Two: you followed the instructions, and the instructions moved
Provider setup instructions are a point-in-time snapshot. The required set grows underneath them.
We hit this on a domain of our own. Every record from its original instructions was present and correct, and it never verified, because the provider had since introduced an additional CNAME our own checker did not know to look for. That was worse than an error: the member was told nothing else needed touching while a required record was missing.
Worth being precise, because this one is easy to overstate. A record the provider added later does not retroactively invalidate existing signatures — receivers verify the selector named in the message in front of them. What breaks is completeness: the provider’s own verification, and any checker comparing your zone against a remembered list rather than against what that provider asks for today. “I followed the instructions” and “my records are complete” are different claims, and the second is the one being tested.
Three: nobody actually measured it
The third case should never reach you, and usually does.
A DNS lookup can just fail. A timeout, a SERVFAIL, a lame nameserver, a record type the tool does not know how to read. Almost every setup wizard files all of that under the same “not found” as a genuinely absent record, then tells you to add something that may well already be there.
Four states per record instead of two fixes it:
| State | Means |
|---|---|
ok |
published, and matching the value wanted |
missing |
the name answers with nothing of that type |
different |
present, but not the value wanted |
unknown |
the lookup itself failed |
The rule that makes the taxonomy worth having is what you do with the fourth one, which is nothing. A record we could not read does not go on your to-do list, because “I could not check” is not “it is broken”, and a confident instruction built on a failed lookup is how someone gets talked into breaking a working zone. A record type the checker cannot read counts as unmeasured too, never as fine. Two of the states are still lossier than reality: missing does not separate a name with no TXT from a name that does not exist, and unknown does not say which way the lookup died.
Four: the record is right, and the message was changed
Every failure above is about the record. This one is not, and it is worth knowing before you touch DNS, because the record is already correct and editing it cannot help.
DKIM does two things: it publishes a key in DNS, and it signs each message with it. The fourth bug is the second half failing — the key is fine, the signature was valid when it left, and something in the delivery path edited the message. The result string tells you which:
dkim=neutral (body hash did not verify)
That sentence means DNS is not your problem. The verifier fetched your key, used it, and found that the body it received hashes to something other than the bh= value the sender sealed in. A missing or stale key reads as dkim=permerror or dkim=fail with a no key for signature reason instead — different words, different half of the system.
The scope of “body” is what makes this easy to hit. It is every octet after the blank line that ends the message headers — so in a multipart/alternative message the part boundaries and the per-part Content-Type: lines are body, not headers. A gateway that re-parses and re-emits MIME rewrites those lines as a matter of course. This is enough:
Content-Type: text/plain; charset=UTF-8 → Content-Type: text/plain; charset="UTF-8"
Two quote characters, inside the signed body, and the signature is void.
⛔ Do not expect canonicalisation to absorb it. relaxed body canonicalisation compresses whitespace runs inside a line, strips trailing whitespace, and ignores empty lines at the end of the body. That is the whole list. relaxed header canonicalisation unfolds, lowercases field names and compresses whitespace. Neither normalises quoting, so there is no signing choice that would have survived this — and chasing one rewrite out of a gateway only surfaces the next.
The honest conclusion is that a modifying hop cannot be made to preserve DKIM, which is why ARC exists: the hop records what authentication looked like before it interfered, and a receiver that trusts it can honour that over the later failure. If forwarding is what broke this, the alignment half of the story is here.
Narrowing it yourself
# The selector that matters is the one the SIGNER chose. Read s= and d= from
# the DKIM-Signature header of a message you actually sent, then:
dig +short TXT <selector>._domainkey.example.com
dig +short CNAME <selector>._domainkey.example.com
Compare that value against what the provider shows you today. Matching-but-old is failure one; absent is ordinary; no answer or a SERVFAIL is failure three and proves nothing either way. Then re-read the provider’s current record list rather than your notes from setup — a record you have never seen before is failure two.
If the record checks out and messages still fail, stop looking at DNS and verify one of them end to end:
formail -c -X DKIM-Signature: < message.eml # which headers were signed (h=), and the bh= it expects
opendkim-testmsg < message.eml # verifies, and says which hash disagreed
A body-hash disagreement on a record that measures ok is failure four, and the thing to go and look at is every hop between the sender and you.
No DNS check replaces sending a real message and reading what came back. If you are reading those headers in code rather than by eye, the format has more variance in it than the grammar allows: parsing Authentication-Results against two real edges.