DKIM canonicalization issue (simple vs relaxed)
The DKIM signature was generated with `simple` canonicalization, so any whitespace or header change by a relaying MTA invalidates the signature.
simple/simple canonicalization requires the message to arrive at the verifier byte-for-byte identical to how it was signed. In practice, mailing lists, forwarders, SMTP gateways, and some anti-spam appliances rewrite headers, fold long lines, or strip trailing whitespace, which breaks the body hash or header hash and produces dkim=fail. The result is intermittent DKIM failures that only appear on certain paths, which cascades into DMARC failures and spam folder placement for a subset of recipients.
What causes it
- Signing software defaults to
c=simple/simple(older OpenDKIM, legacy Sendmail/milter configs, and some appliance defaults) instead of the far more tolerantc=relaxed/relaxed. - A mailing list manager (Mailman, Google Groups, listserv) folds, wraps, or adds
List-*headers, altering the exact byte stream thesimpleheader canonicalization requires. - An outbound relay or security gateway (Mimecast, Proofpoint, Barracuda) rewrites headers, normalizes line endings, or adds a disclaimer, invalidating
simplebody canonicalization. - A forwarding server converts CRLF to LF (or vice versa), or strips trailing whitespace from body lines — tolerated under
relaxedbut fatal undersimple. - Content filters or DLP engines inject or modify headers (e.g.
X-tags,Received-SPF) between the signer and the verifier, breaking header canonicalization when the signer included those headers inh=.
How to fix it
Confirm which canonicalization is actually in use
Inspect the DKIM-Signature header on a received message and look at the c= tag. c=simple/simple (or an omitted c=, which defaults to simple/simple) is the brittle case; c=relaxed/relaxed is what you want.
# Pull a recent message from your mailbox and inspect the signature
grep -i '^DKIM-Signature' /path/to/raw-message.eml | tr ';' '\n' | grep -i 'c=' Switch the signer to relaxed/relaxed
In OpenDKIM, set Canonicalization relaxed/relaxed in /etc/opendkim.conf and reload. In Postfix with dkimpy-milter, set Canonicalization relaxed/relaxed. For application-level signers (Node nodemailer, Python dkimpy, PHP PHPMailer), pass the canonicalization option explicitly rather than relying on library defaults.
# /etc/opendkim.conf
Canonicalization relaxed/relaxed
SignatureAlgorithm rsa-sha256
# Then:
systemctl reload opendkim Re-sign after any downstream content modification
If a gateway or DLP must modify messages after signing, move the DKIM signer to the final hop so the signature is applied to the exact bytes that leave your network. Signing before a relay that rewrites content guarantees failure regardless of canonicalization.
Validate the fix against a real receiver
Send a test message through the full production path (including any list, gateway, or forwarder) and verify dkim=pass in the receiving server's Authentication-Results header. Do not trust a loopback test — the breakage is specifically about what intermediaries do in transit.
# On the receiving MTA, inspect the delivered message
grep -i 'Authentication-Results' /var/mail/user | head -5 Fixed it? Run a fresh test to confirm.
Test my inbox →