DKIM Troubleshooting: Cross-Symptom Diagnosis Guide (Symptom → Likely-Cause → Next-Step Decision Tree for Every dkim= Verdict — dkim=none, dkim=neutral, dkim=pass+DMARC=fail, dkim=fail Body-Hash Mismatch, dkim=fail Signature Mismatch, dkim=temperror, dkim=permerror, dkim=policy — Plus Multi-Signature Scenarios, Provider-Specific Quick References, and an Operator Decision Matrix)

The cross-symptom DKIM diagnosis synthesis page. Where the verdict catalogue (help_dkim_failures_explained.php) documents WHAT each verdict means and the tag anatomy (help_dkim_signatures_explained.php) documents WHAT each tag carries, this page documents WHAT TO DO when the dashboard shows you a specific dkim= verdict — the operator first-response reference for any DKIM regression. Pick the verdict you saw in the Authentication-Results header, follow the symptom branch, execute the next step. Covers all eight dkim= verdicts per RFC 6376 §3.9 and RFC 8601 §2.7.1 with per-branch most-likely-cause and next-step sub-sections, multi-signature scenarios with the RFC 7489 §3.1.1 "any aligns wins" rule, provider-specific quick references for the big cloud senders (M365 onmicrosoft.com gotcha, Workspace gappssmtp.com gotcha, SaaS platforms, transactional ESPs, self-hosted signers), the diagnostic tool surface, and a compact operator decision matrix.

This reference is provided in English. The Authentication-Results verdict strings (dkim=fail, dkim=permerror, dkim=temperror, dkim=policy, dkim=pass, dkim=neutral, dkim=none) and the canonical DKIM/DMARC vocabulary (d=, i=, bh=, b=, h=, c=, s=, adkim=r, adkim=s, "body hash mismatch", "signature did not verify", PSL, ARC) are protocol terms that do not translate. We keep the body in the source language so technical terms remain consistent with IETF RFC documentation.

TL;DR

  • First step: open the failing message in the Email Header Analyzer and read the Authentication-Results header. Modern receivers annotate every dkim= verdict with a reason="..." phrase — that phrase narrows the symptom to one of the eight branches below.
  • Most-common verdicts in practice: dkim=fail with body-hash mismatch (forwarder, mailing list, or content-scanning gateway rewrote the body) — about 80% of all dkim=fail traffic. dkim=pass with DMARC still failing (the M365 onmicrosoft.com and Workspace gappssmtp.com alignment gotchas) — the most-overlooked misconfiguration class.
  • dkim=pass does NOT mean DMARC passes. The cryptography succeeded; ALIGNMENT may still have failed. DMARC compares d= in the DKIM-Signature header to the From: header domain per RFC 7489 §3.1.1 — full algorithm in help_dkim_alignment_with_dmarc.php.
  • dkim=temperror is transient; receivers MAY retry. Small percentage = normal background. Sustained large percentage = your authoritative name servers are unreachable, rate-limited, or your TXT record is truncated at the UDP boundary.
  • dkim=permerror is structural: the selector TXT does not resolve, the TXT is malformed (missing v=DKIM1, truncated p=), or the DKIM-Signature header itself is broken (missing required tag, invalid base64).
  • dkim=policy means too weak: most commonly rsa-sha1 (deprecated by RFC 8301) or a 1024-bit RSA key (some receivers refuse anything under 2048 bits).
  • Cryptography fails ≠ configuration fails. A dkim=fail caused by a forwarder is NOT a configuration bug at your end — your only realistic options are SPF alignment via SRS, an ARC seal across the forwarding boundary, or accepting the per-flow failure when DMARC alignment can still pass via SPF. The verdict catalogue distinguishes operator- actionable from third-party-induced failures.
  • Tools you need: the Email Header Analyzer (parse a failing Authentication-Results), the Domain Security Checker (verify the selector TXT publishes correctly), and the DMARC Analyzer dashboard (drill down on the per-source-IP failure pattern).

How to use this page

DKIM (RFC 6376) failures present as one of eight verdict strings in the Authentication-Results header per RFC 8601 §2.7.1: dkim=none, dkim=neutral, dkim=pass, dkim=fail, dkim=policy, dkim=temperror, dkim=permerror, and the rarely-emitted dkim=softfail (which most receivers fold into dkim=fail for DMARC purposes). When the DMARC Analyzer dashboard surfaces a dkim=fail row in an aggregate report, the operator's first job is to map the verdict back to one of these symptoms — at which point the cause and the remediation become deterministic.

This page is the symptom-first decision tree. Pick the verdict you saw, follow the branch, and the next-step is at the bottom of each section. The companion pages serve different purposes:

  • help_dkim_failures_explained.php — verdict catalogue (what each verdict STRING means in normative terms per RFC 6376 §3.9 and RFC 8601 §2.7.1).
  • help_dkim_signatures_explained.php — tag anatomy (what each tag in the DKIM-Signature header carries: v= a= c= d= s= h= bh= b= i= q= l= t= x= z=).
  • help_dkim_alignment_with_dmarc.php — DMARC alignment algorithm from the DKIM angle (the d=-tag-as-alignment-anchor rule, adkim=r vs adkim=s, organizational-domain matching).
  • help_dkim_explained.php — protocol primer (the high-level signing and verification pipeline).
  • THIS page — symptom → cause → next-step decision tree.

If you have a specific failing message in hand, start at the Decision tree. If you have an aggregate-report row showing repeated failures from one source, start at the matching symptom section directly. If you are not yet sure which verdict you are troubleshooting, paste the Authentication-Results header into the Email Header Analyzer and the parsed output will name the verdict.

Decision tree

The verdict in the Authentication-Results header determines the branch. The reason="..." annotation (when present) narrows further. Receivers like Google and Microsoft emit this annotation aggressively; smaller receivers may omit it, in which case the Received-DKIM: header (if the receiver added one) or the Authentication-Results-Original: header carries the diagnostic detail.

Authentication-Results header
       │
       ├─► dkim=none           ───► Section "dkim=none"            (signer missing)
       │
       ├─► dkim=neutral        ───► Section "dkim=neutral"         (structural anomaly)
       │
       ├─► dkim=pass + DMARC=fail ► Section "dkim=pass + DMARC fail" (alignment gotcha)
       │
       ├─► dkim=fail           ───┬─► reason=body hash ────► Section "fail (body-hash)"
       │                          ├─► reason=signature ────► Section "fail (signature)"
       │                          └─► (no reason given) ───► Inspect both branches
       │
       ├─► dkim=temperror      ───► Section "dkim=temperror"       (DNS transient)
       │
       ├─► dkim=permerror      ───► Section "dkim=permerror"       (record / header malformed)
       │
       └─► dkim=policy         ───► Section "dkim=policy"          (algorithm too weak)

The eight branches below cover every operator- encountered verdict. After diagnosing your specific message, check the multi-signature scenarios section if more than one DKIM-Signature was present, then the provider-specific quick references if the failing source is a cloud sender, and finally the operator decision matrix for the at-a-glance cause-to-fix summary.

Symptom: dkim=none

What it means: the message had no DKIM-Signature header at all. The verifier did not even attempt cryptography; there was nothing to verify.

Likely causes

  • The sender does not sign — plain SMTP traffic from a non-DKIM-deploying sender. This is expected for any From: domain that has not configured DKIM. For DMARC to pass via DKIM the sender must sign; otherwise DMARC alignment must come from SPF.
  • The signing step in your outbound path failed silently. The signer (mail server, ESP, gateway) processed the message without emitting a DKIM-Signature header. Common causes: private key missing on the signer host, signer service down, signer configured for a different domain than the From: header carries, signer bypassed by an outbound relay that intercepted before the signing hop.
  • The signing happened but a downstream hop stripped the signature. Some legacy gateways (notably old Exchange Edge Transport configurations) actively remove DKIM-Signature headers they did not generate themselves. See help_dmarc_for_office_365_hybrid.php for the Hybrid Exchange Default Frontend Connector DKIM-strip pitfall.
  • The signing happened but on a sub-domain that does not match the From:. The signer used a well-formed but UNRELATED d= (a third-party platform's signing domain). The dkim=none here is more accurately a no-aligned-signature outcome — receivers may emit dkim=none, dkim=fail, or omit the entry entirely depending on implementation.

Next steps

  1. Confirm your domain is supposed to be signing. If the From: header is example.com and you have not deployed DKIM at example.com, dkim=none is the expected verdict — deploy DKIM via help_dkim_explained.php for the protocol primer and help_dkim_for_microsoft_365.php or help_dkim_for_google_workspace.php for the cloud-specific recipe.
  2. If DKIM IS deployed, inspect your outbound path. Send a test message to a known verifier (e.g., a Gmail mailbox you own) and inspect the received message: does it carry a DKIM-Signature header? If yes, the signer works — investigate the specific path that produced dkim=none in the aggregate report. If no, the signer itself is broken.
  3. Check the DMARC aggregate report for the source IP that produced the dkim=none — the source-IP row tells you which outbound path is unsigned. Drill down in the DMARC Analyzer dashboard at source_ip_detail.php for the per-IP failure profile.

Symptom: dkim=neutral

What it means: the verifier found a signature and was able to fetch the public-key TXT, but the structural shape of the signature is something the verifier cannot classify as a clean pass or fail. The verifier is explicitly declining to render a verdict. Per RFC 6376 §3.9 this is the catch-all bucket for structural anomalies that are not strictly malformed (which would be permerror) and not transient (which would be temperror).

Likely causes

  • An unrecognized but tolerable tag in the DKIM-Signature header — a forward-compatibility scenario where a future extension added a tag the current verifier does not understand.
  • An experimental or rare algorithm in a=. Anything outside rsa-sha256 (RFC 6376 §3.3.1) and ed25519-sha256 (RFC 8463) may produce dkim=neutral at receivers that recognize the algorithm string but have not implemented it.
  • A policy-required header missing from h= at a receiver with a local policy mandating that header be signed. Some receivers will degrade to dkim=neutral (rather than dkim=policy) when the gap is structural rather than algorithmic.
  • Multiple DKIM-Signature headers where one is well-formed and one is malformed — a non-deterministic verifier might emit dkim=neutral as the compromise verdict. RFC 6376 §3.9 leaves this case implementation-defined.

Next steps

  1. Open the failing message in the Email Header Analyzer. The parsed DKIM-Signature will surface any unrecognized tag or algorithm — that is almost always the cause.
  2. If the cause is an unusual algorithm in a=, switch your signer to a=rsa-sha256 (the universally supported modern default per RFC 8301). Ed25519 (a=ed25519-sha256) from RFC 8463 is the only other widely-supported choice.
  3. If the cause is a policy-required header, review your signer's h= configuration — modern signers should cover From, Subject, Date, To, Message-ID, Reply-To, Sender, MIME-Version at minimum (RFC 6376 Appendix C reference set). The tag anatomy guide documents the canonical signed-header set.

Symptom: dkim=pass but DMARC still fails

What it means: the cryptographic verification succeeded — the body hash matched, the signature verified, the verifier is confident the message was signed by whoever published the key at s= under d=. But DMARC requires more than cryptography: DMARC requires ALIGNMENT between the signing domain d= and the From: header per RFC 7489 §3.1.1. Cryptography passed; alignment failed.

Likely causes (the two big ones)

  • Microsoft 365 tenant-default signing. M365 signs all outbound mail with d=tenantname.onmicrosoft.com unless a custom-domain DKIM key is explicitly enabled. The From: header carries the custom domain (user@example.com), but the DKIM-Signature carries d=tenantname.onmicrosoft.com. onmicrosoft.com is on the Public Suffix List, so relaxed alignment also fails. Fix per help_dkim_for_microsoft_365.php: enable custom-domain DKIM via Defender for Office 365, publish the two selector CNAMEs, and the signer switches to d=example.com.
  • Google Workspace gappssmtp.com signing. Workspace signs all outbound mail with d=tenantname.gappssmtp.com unless a custom-domain DKIM key is explicitly enabled. gappssmtp.com is on the Public Suffix List, so relaxed alignment also fails. Fix per help_dkim_for_google_workspace.php: generate a custom DKIM key in the Admin Console, publish the TXT record, and start authentication.
  • SaaS marketing platform without custom-domain DKIM. SaaS senders (Mailchimp, SendGrid, Mailgun, HubSpot, Klaviyo) sign with their own platform domain by default. Until you configure your custom-domain DKIM on the platform (a per-vendor recipe), every message they send appears as dkim=pass-but-DMARC-fail for messages with your From: domain.
  • Subdomain mismatch with adkim=s. If you set adkim=s (strict) in your DMARC record, the d= tag must be byte-identical to the From: domain. A signer using a subdomain (e.g., d=marketing.example.com with From: user@example.com) will fail strict alignment even though relaxed alignment would have passed. Switch to adkim=r (relaxed, the default) unless you have a specific reason for strict.

Next steps

  1. Inspect the failing message in the Email Header Analyzer. Read the d= tag in the DKIM-Signature header. Compare to the From: header domain. If they differ (different org domains per PSL), you have the alignment gotcha.
  2. Identify the signer from the d= value. Is it your M365 tenant (onmicrosoft.com), your Workspace tenant (gappssmtp.com), or a SaaS platform domain? The fix is platform- specific.
  3. For full alignment-algorithm coverage including subdomain semantics, the PSL organizational-domain matching algorithm, and the multiple-signature "any aligns wins" rule, see help_dkim_alignment_with_dmarc.php.

Symptom: dkim=fail (body-hash mismatch)

What it means: the cryptographic body hash computed by the verifier does not equal the bh= value in the DKIM-Signature header. The body was modified between signing and verification. The reason="body hash did not verify" annotation makes this verdict unambiguous at Google and Microsoft receivers.

This is the single most common dkim=fail in practice — about 80% of dkim=fail traffic.

Likely causes

  • Forwarder rewrote the body. A user mailbox that forwards to another address inherits the body but may strip attachments, normalize whitespace, or inject a forwarding annotation. Once the body changes a single byte, the body hash differs and dkim=fail follows. See help_spf_forwarding_issues.php for the same problem viewed through the SPF lens.
  • Mailing list rewrote the body. Mailman, Listserv, Discourse, and Google Groups inject a footer ("To unsubscribe...") and often a Subject prefix. The footer changes the body; dkim=fail follows. ARC-sealing at the list boundary (RFC 8617) is the modern fix.
  • Content-scanning gateway rewrote the body. Anti-spam / anti-phishing gateways frequently insert a "[EXTERNAL]" banner, a warning footer, or a tracking pixel. Each insertion modifies the body.
  • SaaS platform rewrote URLs. Click-tracking systems rewrite every link in the body to a per-recipient redirect URL. If the rewriting happens AFTER signing, the body hash fails.
  • MIME re-encoding by an intermediary. A gateway that canonicalizes content-transfer-encoding (e.g., switching base64 to quoted-printable or vice-versa) changes the body's byte-level representation even when the rendered content is unchanged.
  • Trailing CRLF normalization. RFC 6376 §3.4.4 relaxed body canonicalization ignores empty lines at the end of the body — most signers and verifiers use relaxed. With simple body canonicalization (c=*/simple), an intermediary's whitespace edit at the body tail breaks the hash.

Next steps

  1. Open the failing message in the Email Header Analyzer. Read the Received: header chain bottom-to-top. The signer is the last hop before the first non-aligned line in the chain. Any intermediary that appears between the signer and the verifier is a candidate body-rewriter.
  2. If the failing source is a mailing list or known forwarder, accept the per-flow failure and rely on the SPF path: configure SRS rewriting on the forwarder so SPF aligns, or get the list operator to apply ARC sealing (RFC 8617) so DMARC trusts the original DKIM result.
  3. If the failing source is your own outbound gateway (content scanning, tracking-link rewriting), reorder the pipeline so signing happens AFTER all content transformations — or switch the body canonicalization to c=relaxed/relaxed (the modern default, more forgiving of whitespace edits). See help_dkim_canonicalization.php for the full canonicalization trade-off.
  4. If the failing source is a SaaS platform you control, check the platform's "DKIM signing of redirected links" support setting — most modern platforms (Mailgun, SendGrid) sign AFTER URL rewriting precisely to avoid this failure.

Symptom: dkim=fail (signature mismatch)

What it means: the body hash matched (bh= verified correctly), but the cryptographic signature over the signed-header set did not verify against the public key. Either the signed-header set changed since signing, or the wrong public key was used. The reason="signature did not verify" annotation makes this verdict unambiguous.

Likely causes

  • A header named in h= was modified after signing. The most common culprit is a Subject prefix added by an anti-spoof gateway ("[External]", "*** SPAM ***", "[CAUTION]") or list-prefix injection ("[announce-list] ...").
  • A signed header was re-ordered or de-duplicated by an intermediary. Some MTAs rewrite the order of headers or collapse duplicates. With simple header canonicalization (c=simple/*), this breaks the signature.
  • The verifier fetched the wrong public key. The s= selector resolves to a TXT record but the key in DNS no longer matches the key used at signing time — typically a rotation gone wrong (see the provider- specific section) or a misconfigured selector publishing an old test key.
  • The signer emitted an invalid signature. Rare but seen: a signing host with a corrupt private key, a signing library bug, or a clock-skewed signer using a signature scheme that includes time-dependent randomness.

Next steps

  1. Inspect the Authentication-Results and the full headers in the Email Header Analyzer. Look for a Subject prefix that was clearly added by an intermediary (the original Subject line is recoverable from the z= tag if the signer emitted it).
  2. If the cause is a Subject-prefix rewrite, remove Subject from h= (NOT recommended — Subject is a critical identity-bearing header) OR talk to the rewriting gateway operator about disabling the prefix injection for legitimate authenticated mail. Many enterprise security suites offer a "do not rewrite if DKIM signed" mode.
  3. If the cause is a key-rotation regression, republish the previous selector record (most providers retain it for 30 days exactly for rollback) and validate caches via dig @resolver selector._domainkey.example.com TXT from multiple geographic resolvers before re-rotating with a longer overlap window.

Symptom: dkim=temperror

What it means: the verifier could not retrieve the public- key TXT at verification time but classifies the failure as transient. Per RFC 6376 §6.1.2 the receiver MAY retry — the verdict is not a permanent failure. In practice receivers do retry a small number of times before falling back to dkim=fail or dkim=permerror.

Likely causes

  • Transient DNS failure on the verifier's recursive resolver — packet loss, upstream rate-limiting, or an authoritative name server briefly unreachable.
  • UDP truncation without EDNS0 fallback. 2048-bit RSA public-key TXTs are large enough that the DNS response exceeds the legacy 512-byte UDP limit. Modern resolvers and authoritative servers negotiate EDNS0 to accept larger responses (per RFC 6891), but a misconfigured firewall or NAT can strip EDNS0, leaving the verifier with a truncated response.
  • Sustained authoritative name-server overload — your DNS provider is rate-limiting queries from the verifier's resolver subnet, or your DNS infrastructure is genuinely overwhelmed (a small DNS provider getting hit by query-volume from a large cloud receiver's IP block).
  • RFC 1035 §3.3.14 TXT chunking violated. A single-string TXT must be at most 255 bytes; longer payloads must be split into multiple consecutive 255-byte strings. A signer-provided record that exceeds 255 bytes as a single string is malformed at the DNS layer and many resolvers refuse to return it.

Next steps

  1. Quantify the percentage. dkim=temperror on under 1% of traffic is normal internet background noise — ignore. dkim=temperror on a sustained large percentage is a real problem.
  2. Run dig @8.8.8.8 selector._domainkey.example.com TXT from multiple resolvers (Google 8.8.8.8, Cloudflare 1.1.1.1, Quad9 9.9.9.9). Confirm consistent successful resolution with a sane response payload.
  3. Validate the TXT record size and chunking with dig +short selector._domainkey.example.com TXT | wc -c. If the payload is large (over ~300 bytes), confirm it is split into multiple quoted strings (each at most 255 bytes long).
  4. If the failing receiver is one specific cloud (e.g., always Microsoft, never Google), the issue may be that receiver's resolver infrastructure rate-limiting your authoritative servers — contact your DNS provider about scaling the authoritative tier.

Symptom: dkim=permerror

What it means: the verifier classified the failure as permanent — retrying would not help. Something structural is malformed: the selector record does not exist, the record is broken, or the DKIM-Signature header itself is structurally invalid.

Likely causes

  • Selector TXT does not exist (NXDOMAIN response). The signer is emitting a selector name that was never published, or was published under a different name. The most common cause is a signer-side typo (publishing selector1 but signing with s=default) or a DNS publication that never propagated.
  • TXT exists but contains no v=DKIM1. RFC 6376 §3.6.1 requires the record to begin with v=DKIM1 (or implicitly for backward-compat). A record that begins with k=rsa; p=... alone will be rejected by strict verifiers.
  • TXT contains malformed key data — truncated p= (the public-key base64 was clipped during DNS publication, often because the provider's web UI imposed a character limit), embedded whitespace inside the base64 (the publisher inserted line-breaks for readability and forgot to strip them), or the public key in a non-PEM format the verifier cannot parse.
  • DKIM-Signature header is malformed — missing v=1, missing required tag, base64 in b= or bh= contains non-base64 characters, h= tag has unbalanced colons, or the entire header is concatenated on a single line in a way that violates RFC 5322 line-length rules.
  • Unsupported algorithm in a=. Most modern receivers reject rsa-sha1 (deprecated per RFC 8301). A record using a=rsa-sha1 may emit dkim=permerror at receivers that have removed SHA-1 support.

Next steps

  1. Run dig +short selector._domainkey.example.com TXT. If you get NXDOMAIN, the record is not published — publish it per your provider's recipe (help_dkim_for_microsoft_365.php, help_dkim_for_google_workspace.php, etc.).
  2. If the record exists, paste the full response into the Domain Security Checker — it validates the record structurally (v=DKIM1 prefix, p= base64 well-formed, no truncation).
  3. If the record validates, inspect the failing DKIM-Signature header in the Email Header Analyzer — the parser will flag the specific malformed tag.
  4. If the algorithm is rsa-sha1, switch your signer to a=rsa-sha256 per RFC 8301. This requires a new key pair and a new selector record because the algorithm is baked into the key derivation.

Symptom: dkim=policy

What it means: the cryptographic verification would have succeeded, but the verifier rejects the signature on local-policy grounds — the algorithm is too weak, the key is too short, a required header was not signed, or some other policy gate failed. dkim=policy is the explicit "I refuse to validate this even though it is well-formed" verdict.

Likely causes

  • Algorithm too weak. Most modern receivers refuse rsa-sha1 signatures (RFC 8301 deprecates SHA-1 for DKIM). Switching to a=rsa-sha256 is the fix; this requires a new key pair and selector.
  • Key length too short. Some receivers refuse RSA keys under 1024 bits (clearly too short by 2026), and increasingly many refuse keys under 2048 bits. M365 auto-upgraded all tenant keys to 2048-bit in 2022-2023; Workspace and most cloud providers now default to 2048-bit. See help_dkim_key_rotation.php for the rotation discipline.
  • From: not in h=. RFC 6376 §5.4 MANDATES From signing. Receivers that enforce §5.4 strictly emit dkim=policy when h= omits From.
  • Local-policy required header missing. Some receivers require additional headers in h= beyond From — typically Date, Message-ID, and Subject. The modern best-practice signer covers a much larger set than the minimum.
  • Signature timestamp anomaly. If t= is in the future (clock-skewed signer) or x= is in the past (expired signature), some receivers emit dkim=policy.

Next steps

  1. Identify the policy failure mode from the Authentication-Results reason="..." annotation. Modern receivers say "algorithm not supported", "key too short", "from not signed", etc.
  2. If the cause is rsa-sha1, generate a new key pair using rsa-sha256, publish under a new selector, and switch the signer. Per RFC 8301 this is the modern default and the only future-proof choice besides ed25519-sha256 (RFC 8463).
  3. If the cause is key length, generate a 2048-bit RSA key (or Ed25519, much shorter records). Most cloud providers automate this; for self-hosted signers see help_dkim_cryptography_basics.php.
  4. If the cause is a missing signed header, expand the signer's h= configuration. The canonical modern set covers From, Subject, Date, To, Cc, Reply-To, Sender, Message-ID, MIME-Version, Content-Type, Content-Transfer-Encoding, List-Unsubscribe, In-Reply-To, References, Auto-Submitted.

Multi-signature scenarios

A message MAY carry more than one DKIM-Signature header. The verifier evaluates each independently per RFC 6376 §6.1 and emits one dkim= entry per signature in the Authentication-Results header. For DMARC purposes, the rule per RFC 7489 §3.1.1 is "any aligns wins": if ANY of the dkim=pass results has a d= aligned with the From: header, DMARC alignment is satisfied.

Common scenarios

  • Sender signs once, marketing platform signs again. When you send through a SaaS platform (Mailchimp, SendGrid), the platform signs with its own selector under its own d=, and if you have configured custom-domain DKIM the platform also signs with your domain's selector. The result is two DKIM-Signature headers — one with the platform's d= (will not align for DMARC) and one with yours (will align if configured correctly).
  • On-prem signs, M365 re-signs after hybrid relay. In hybrid Exchange topology (on-prem + Exchange Online), if both sides sign, two DKIM-Signatures appear. The receiver picks the one with the aligned d=. The non-deterministic behavior is the "dual-signer DKIM problem" documented in help_dmarc_for_office_365_hybrid.php.
  • Forwarder adds an ARC-Seal but not a new DKIM-Signature. ARC (RFC 8617) is a layered authentication chain, not a DKIM extension. A forwarder that ARC-seals does NOT add a second DKIM-Signature; instead it adds an ARC-Seal, ARC-Message-Signature, and ARC-Authentication-Results. The original DKIM-Signature may still fail at the receiver, but the ARC chain carries forward the forwarder's earlier cv=pass verdict — receivers that trust the forwarder (Defender Trusted ARC Sealers, Google's ARC trust list) accept the message on the ARC-attested earlier DKIM result.
  • Multiple signatures with conflicting results. Two DKIM-Signature headers: one dkim=pass, one dkim=fail. The receiver emits both verdicts in Authentication-Results. For DMARC alignment the dkim=pass with the aligned d= wins. For operator diagnosis the dkim=fail is still worth understanding — it may indicate a layered-signing pipeline where one stage works and another is broken.

Diagnosis

Open the message in the Email Header Analyzer. It surfaces all DKIM-Signature instances and pairs each with the corresponding Authentication-Results dkim= entry. For each instance read the d= value and the resulting verdict; identify which signature would have aligned with the From: header, and whether that aligning signature passed cryptographically. If the answer is "no aligning signature passes", you have an alignment configuration gap — see the alignment-gotcha section.

Provider-specific quick references

The big cloud senders have well-known failure modes that map cleanly to the symptom branches above. Use this section as a quick reference when the failing source IP belongs to one of the major providers.

Microsoft 365 (Exchange Online)

  • Default behavior: signs with d=tenantname.onmicrosoft.com and selectors selector1 / selector2 hosted at selector1-tenantname.onmicrosoft.com.
  • Most common regression: dkim=pass + DMARC fail because the tenant signs with the onmicrosoft.com identity rather than the custom domain. Fix: enable custom-domain DKIM in Defender for Office 365, publish the two CNAMEs at selector1._domainkey.example.com and selector2._domainkey.example.com, then enable signing. Recipe: help_dkim_for_microsoft_365.php.
  • Dashed-domain transformation gotcha: hyphenated domains (example-uk.com) require the CNAME target to use a double-dash transformation (example--uk-com). Hand-constructing the target is a common failure source — use Microsoft's portal-provided values verbatim.
  • Rotation: M365 rotates automatically every 6 months between selector1 and selector2 with no operator action required. If you see dkim=fail spikes every 6 months the rotation is working but propagation is slow — usually a 4-hour TTL window per published CNAME.

Google Workspace

  • Default behavior: signs with d=tenantname.gappssmtp.com if no custom-domain key is configured. gappssmtp.com is on the Public Suffix List, so relaxed alignment also fails.
  • Most common regression: dkim=pass + DMARC fail because of the gappssmtp.com signing identity. Fix: generate a custom-domain 2048-bit DKIM key in the Workspace Admin Console (Apps → Google Workspace → Gmail → Authenticate email), publish the TXT record to google._domainkey.example.com verbatim from the Admin Console (the value is a single long string with embedded v=DKIM1; k=rsa; p=...), wait for DNS propagation, then click "Start authentication". Recipe: help_dkim_for_google_workspace.php.
  • 255-byte TXT chunking rule: the 2048-bit DKIM TXT record from Workspace is over 255 bytes long, requiring RFC 1035 §3.3.14 multi-string splitting. Most modern DNS providers handle this automatically but legacy panels may force you to chunk by hand using quoted strings.
  • Rotation: Workspace requires manual rotation — no automatic rotation like M365. Generate a new selector record in the Admin Console, publish, wait for propagation (1 week recommended), then click "Switch to new key".

SendGrid / Mailgun / Mailchimp / SaaS senders

  • Default behavior: sign with the platform's own domain (d=sendgrid.net, d=mailgun.org, d=mailchimp.com). Receivers see dkim=pass but DMARC alignment fails against your custom From: domain.
  • Most common regression: dkim=pass + DMARC fail because you have not configured custom-domain DKIM on the platform. Fix: every SaaS sender has a "domain authentication" or "sender identity" workflow — find your platform's recipe, publish the per-platform selector CNAMEs, and the signer switches to a custom-domain identity.
  • Click-tracking rewriting interferes: platforms that rewrite URLs for click tracking may break body hashes if rewriting happens after signing. Modern platforms sign AFTER URL rewriting precisely to avoid this — confirm in the platform's settings that the signing pipeline order is correct.

Amazon SES / Postmark / SparkPost / cloud relays

  • Default behavior: all major transactional ESPs support custom-domain DKIM as first-class configuration. Selectors and TXT records are provided in the platform console.
  • Most common regression: the provided TXT was published with embedded line-breaks or truncated by the DNS provider UI. dkim=permerror follows. Fix: republish from the platform console verbatim, preserving any quote marks or chunked structure.

Self-hosted signers (Postfix, OpenDKIM, Exim, Sendmail)

  • Default behavior: you choose the selector, algorithm, and key size. Modern best-practice: a=rsa-sha256 (per RFC 8301), c=relaxed/relaxed, 2048-bit RSA, selector identifier including the rotation date (e.g., 20260522).
  • Common regression: clock skew on the signer host (NTP not synchronized) produces signatures with t= values that receivers reject as clock-anomalous. Ensure chrony or ntpd is running and stratum-2 or better.
  • Common regression: the signing milter is not in the outbound path for all message flows — only some messages get signed. Validate with the Email Header Analyzer on test sends from each outbound flow.

Tools for diagnosis

The DMARC Analyzer toolchain provides the primary diagnostic surfaces for every symptom on this page. External tools listed underneath are recommended second-opinion checks.

Email Header Analyzer (header_analyzer.php)

Paste any raw email source or just the headers. The parser surfaces:

  • Every DKIM-Signature instance, tag-by-tag, with the tag-anatomy reference applied to each value.
  • The Authentication-Results verdict pairs (dkim=X plus any reason= annotation).
  • The Received: header chain annotated with the signing hop (last aligned line) and subsequent intermediaries (potential body-rewriters).
  • Any malformed-tag detection per the tag-anatomy reference's structural rules.

Domain Security Checker (domain_checker.php)

Validates the public-key TXT side of DKIM. Paste your domain and selector; the checker:

  • Resolves selector._domainkey.example.com from multiple geographic resolvers and reports the TXT response.
  • Validates the record begins with v=DKIM1 and contains a structurally valid p= public key.
  • Checks for the 255-byte chunking rule (RFC 1035 §3.3.14) and warns if chunking looks wrong.
  • Surfaces the underlying DNS error class when resolution fails (NXDOMAIN, SERVFAIL, timeout).

DMARC Analyzer Dashboard (index.php)

The dashboard surfaces aggregate- report rows by reporter and by source IP. Drill down on a failing row to see the per-IP failure profile — source_ip_detail.php shows every aggregate-reported failure attributed to that IP over the chosen time window with the DKIM and SPF auth results inline.

External diagnostic helpers

  • dig +short selector._domainkey.example.com TXT — the canonical command-line DNS lookup. Use from multiple geographic resolvers (8.8.8.8, 1.1.1.1, 9.9.9.9).
  • opendkim-testkey — validates a published TXT matches a local private key. Useful for self-hosted signers verifying their key pair publication.
  • mxtoolbox.com, dkimvalidator.com — second-opinion validators for the published TXT and for sample messages.

Operator decision matrix

At-a-glance summary: pick the symptom row to find the most likely cause and the highest- leverage next step.

Symptom Most likely cause Highest-leverage next step
dkim=none Sender does not sign, or signing step failed silently in your outbound path Send a test message to a Gmail mailbox; inspect for the DKIM-Signature header. If missing, debug the signer
dkim=neutral Unrecognized tag, experimental algorithm, or structural anomaly that is not strictly malformed Open in the Email Header Analyzer; switch to a=rsa-sha256 if non-standard algorithm
dkim=pass + DMARC=fail d= does not align with From: — M365 onmicrosoft.com, Workspace gappssmtp.com, or SaaS platform without custom-domain DKIM Configure custom-domain DKIM at the platform; publish the platform-provided records verbatim
dkim=fail (body-hash) Forwarder, mailing list, content-scanner, or click-tracker rewrote the body after signing Accept the per-flow failure; rely on SPF alignment via SRS, or apply ARC sealing at the boundary
dkim=fail (signature) Header named in h= was modified after signing — typically Subject prefix injection by an anti-spoof gateway Disable the rewriting on the gateway for DKIM-signed mail, or remove the affected header from h=
dkim=temperror Transient DNS failure, UDP truncation without EDNS0, or sustained authoritative name-server overload Quantify the percentage; if sustained large, scale DNS authoritative tier or fix EDNS0 path
dkim=permerror Selector TXT does not exist or is malformed, or DKIM-Signature header itself is broken dig +short selector._domainkey.example.com TXT — if NXDOMAIN, republish from provider verbatim
dkim=policy Algorithm too weak (rsa-sha1 deprecated per RFC 8301), key too short (under 2048-bit RSA), or From: not in h= Generate a new 2048-bit RSA key with rsa-sha256; publish under a new selector; switch the signer

FAQ

My DMARC aggregate report shows dkim=fail. What is the first thing to check?
Start by determining whether the failure is a body-hash mismatch (bh=) or a signature mismatch (b=). Open the affected message in the Email Header Analyzer and read the Authentication-Results header — receivers like Google and Microsoft now annotate dkim=fail with the specific reason (e.g., "dkim=fail reason="signature did not verify"" vs "dkim=fail reason="body hash did not verify""). About 80% of dkim=fail in practice is a body-hash mismatch caused by a forwarder, mailing list, or content-scanning gateway rewriting the body after signing. The other 20% is a signature mismatch caused by a header being added or modified after signing (most commonly a Subject prefix added by an anti-spoof gateway). Once you know which hash failed, follow the symptom branch on this page: body-hash mismatch → forwarder/list/gateway investigation; signature mismatch → header-rewrite investigation.
Authentication-Results says dkim=pass but DMARC still fails. Why?
dkim=pass means the cryptographic verification succeeded — the body hash matched and the signature verified against the public key in DNS. It does NOT mean DMARC passed. DMARC additionally requires ALIGNMENT: the d= tag in the DKIM-Signature header must match the From: header domain (relaxed) or be byte-identical to it (strict). The two most common sources of dkim=pass + DMARC=fail are: (a) Microsoft 365 tenants where the cloud signs with d=tenantname.onmicrosoft.com instead of the custom domain (tenant-default signing — fix by publishing the two selector CNAMEs from Defender for Office 365), and (b) Google Workspace tenants where the cloud signs with d=tenantname.gappssmtp.com instead of the custom domain (gappssmtp.com is on the Public Suffix List, so the relaxed organizational-domain match also fails — fix by generating a custom-domain DKIM key in the Workspace Admin Console). The full alignment evaluation algorithm is documented in help_dkim_alignment_with_dmarc.php.
I see dkim=temperror in my logs. Is this a problem?
dkim=temperror means the verifier could not retrieve the public-key TXT record at verification time but the failure is transient (DNS timeout, intermittent resolver issue, recursive name server outage). RFC 6376 §6.1.2 mandates that receivers MAY retry temperror-classified messages. dkim=temperror on a small percentage (under 1%) of your traffic is normal background noise from internet routing turbulence. dkim=temperror on a sustained large percentage of your traffic is a sign of a real problem: your authoritative name servers are overloaded or unreachable, or your TXT record is too large for UDP (over 512 bytes without EDNS0) and is being truncated. Diagnose with: (a) dig @8.8.8.8 selector._domainkey.example.com TXT from multiple resolvers to confirm consistent successful resolution, (b) check the size of the TXT response — 2048-bit RSA keys often need EDNS0 or chunked TXT splitting per RFC 1035 §3.3.14, (c) check authoritative name server logs for query-volume spikes that could be triggering rate limits.
How do I tell if a body-hash mismatch was caused by a forwarder or by a content-scanning gateway?
The Received: header chain reveals which intermediaries handled the message. Open the failing message in the Email Header Analyzer and read the Received: headers from bottom to top (oldest hop first). The signer is the last hop before the first non-aligned Received: line — anything that handled the message AFTER signing and BEFORE the verifier could have rewritten the body. Common culprits identified by Received: signatures: forwarders show ".forwarder." or "mailman.example.com" lines; mailing lists show "list-id:" headers and "List-Unsubscribe:" footer injection; content-scanning gateways show "antispam-" or "scanned by" annotations; SaaS marketing platforms show "x-original-sender:" or "x-mailer:" branding. Once identified, the fix is either (a) switch the upstream to relaxed body canonicalization which tolerates whitespace edits, (b) get an ARC seal across the boundary so DMARC trusts the original DKIM result, or (c) accept the dkim=fail and rely on SPF alignment via the forwarder (only works if SRS rewriting is in place).
What does dkim=permerror mean and how do I fix it?
dkim=permerror is the verifier saying "the signature is malformed at a structural level — retrying would not help". The most common causes: (a) the selector._domainkey TXT record does not exist (NXDOMAIN) — you forgot to publish it, or you published it under the wrong selector name; (b) the TXT record exists but contains malformed key data (truncated p= value, accidental whitespace inside the base64-encoded key, wrong v=DKIM1 prefix); (c) the DKIM-Signature header itself is malformed (missing v=1, missing required tag, b= or bh= not valid base64, h= tag with unbalanced colons); (d) the algorithm in a= is not supported by the verifier (most receivers now reject rsa-sha1 per RFC 8301). Fix by: (1) dig +short selector._domainkey.example.com TXT to confirm the record resolves and the response looks sane (single line, v=DKIM1, p=<long base64>), (2) paste the DKIM-Signature header into the Email Header Analyzer to see which tag the parser flagged, (3) re-publish the record using the operator-provided DNS records from your provider (Microsoft 365 / Google Workspace / etc.) — do not hand-edit.
My provider rotated keys and now everything fails. What happened?
DKIM key rotation requires dual-selector overlap — the new selector must be published in DNS and verified to resolve from multiple geographic resolvers BEFORE the signer is switched over. If the new selector was not published, or if the TTL on the old selector record was so short that caches missed the rotation window, in-flight messages will be signed with selector N but receivers will look up selector N+1 (or vice versa) — every dkim=fail until the lookup catches up. Fix in the immediate term: republish the old selector record exactly as it was (your provider almost always keeps it for 30 days post-rotation specifically for rollback). Then wait for caches to drain (look at the original record TTL — usually 1 hour for cloud providers). Long-term fix: enforce a minimum 7-day overlap between old and new selectors when rotating. Microsoft 365 handles this automatically via the alternating selector1/selector2 design; Google Workspace requires manual rotation discipline per help_dkim_for_google_workspace.php.
How do I troubleshoot multiple DKIM signatures on a single message?
A message MAY carry more than one DKIM-Signature header — a sender can add multiple signatures (one per sub-brand) and an intermediary like a marketing platform may add its own signature on top. Per RFC 6376 §6.1 the verifier evaluates each signature independently and produces one Authentication-Results entry per signature. For DMARC purposes, the alignment rule per RFC 7489 §3.1.1 is "any aligns wins" — if ANY of the dkim=pass results has a d= tag aligned with the From: header, DMARC alignment is satisfied. Troubleshooting steps: (1) read the Authentication-Results header — every dkim= entry corresponds to one DKIM-Signature header; (2) identify which signature has the aligned d= (matches your From: domain); (3) if NO signature has an aligned d=, you have an alignment-not-cryptography failure — fix by publishing your own custom-domain DKIM key with the signer (the cloud-default tenant-domain signing is not enough); (4) if MULTIPLE signatures have aligned d= but at least one fails, the failures are usually a layered-signing artifact (an outer ARC sealer also re-signed) and not actionable — focus on the aligned passing signature.
What is the difference between dkim=neutral and dkim=none?
dkim=none means the message had NO DKIM-Signature header at all — the sender never signed it. This is the verdict for plain SMTP traffic from non-signing senders, and from any path where the sender did not deploy DKIM. dkim=neutral is rare and means "I found a signature and a key but I am unable to render a decisive verdict" — most commonly when the verifier encountered a structural anomaly that is not strictly malformed (an unrecognized but tolerable tag, an experimental algorithm, a policy-required header that is missing). Operationally: dkim=none is fine for a domain that has not deployed DKIM (deploy DKIM if you want DMARC to pass via DKIM); dkim=none on a domain that HAS deployed DKIM means the signing step in your outbound path is failing (check upstream signer logs). dkim=neutral on a domain that has deployed DKIM means a configuration anomaly worth investigating in the Email Header Analyzer.

Diagnose a failing message now

DMARC Analyzer ingests every aggregate report and surfaces the per-source-IP failure profile in the dashboard. When you find a failing source, drill down on the per-IP detail view and the per-reporter detail view to see the failure-mode pattern over time. Then use the Email Header Analyzer to parse any representative failing message end-to-end — the symptom branch on this page will tell you the next step.

Sign up — Free plan Analyze a failing header