DKIM Verification Failures: A Support Email Attachment Workflow Explained
Short answer: resolving DKIM verification failures while integrating an email service is a chain-of-custody problem, not an attachment problem. For a customer-support report, keep the message template and the DKIM signing policy under one clearly named owner, publish the selector's public key exactly where the signing domain says it lives, and verify the signed message after the final MIME assembly. If the signature is missing, the DNS lookup is wrong, or a later relay changes signed content, the attachment can be perfectly valid and the message can still fail verification.
The operational constraint is ownership. A support team may own the words, an email service may own the signing key, and an application team may build the MIME message. That arrangement can work, but only if the boundaries are explicit. At 3am, the useful question is not "did the send API return success?" It is "what page fired, which domain signed, and which key did the recipient try to fetch?"
What does a DKIM failure actually say?
DKIM adds a cryptographic signature to selected message headers and the body. The verifier uses the public key published in DNS to check that signature, then evaluates the signing domain in the DKIM-Signature header. A failed result does not identify one universal cause. It says the verifier could not validate the signature it received.
That distinction prevents a common support mistake: rebuilding the PDF first. The report attachment is one MIME part inside the message. The signature covers the body as defined by the signing process; a change to line endings, transfer encoding, headers, or other signed content after signing can invalidate the result. A message can therefore carry an openable PDF and still have a broken DKIM result.
I first assumed the attachment was the useful evidence. It was not. I start by preserving the complete received message, including headers, before changing anything. Record the d= signing domain, the s= selector, the h= signed-header list, the result reported by the receiving system, and the exact time. Do not infer the selector from the visible From address. The visible address and the signing identity are related policy choices, not the same field.
Start with evidence.
How can I keep email DKIM verification failures from returning while integrating a service?
Use a short decision sequence rather than changing three DNS records and sending another sample.
- No signature: confirm that the outbound path is configured to sign this message class. A successful submission response only proves that a handoff was accepted; it does not prove that a recipient received a valid DKIM signature.
-
Signature present, key not found: query the exact DNS name formed from the selector and signing domain:
<selector>._domainkey.<signing-domain>. Check spelling, record type, delegation, and whether a stale record is being served. - Key found, verification fails: compare the received signature with the message as received. Look for a relay, footer, disclaimer, or MIME rewriter that changed signed material. Also check that the public key corresponds to the private key currently used by the signer.
- DKIM passes, alignment policy fails: inspect the relationship between the signing domain and the visible From domain. A cryptographically valid signature can still fail a separate alignment check if the domains do not satisfy the receiver's policy.
DNS is part of the production dependency graph. The selector is a key identifier, not a password, and the public record should be treated as configuration that needs review, ownership, and change history. A copied value with an invisible line break or an old selector can waste more time than the code that assembles the email, especially when the sender has already rotated to a new selector but the receiving resolver still exposes the previous record during a controlled test. RFC 6376 is the reference point I use for the signature model; the rest is operational discipline around that model.
Who should own the template and the signing key?
For a support report, I would separate content ownership from signing authority. Support owns the template's customer-facing text and the attachment contract: filename, media type, report period, and escalation wording. The delivery boundary owns DKIM key custody and the signing configuration. The application owns the data mapping and the decision to send.
That split gives the team a useful failure boundary. A copy edit should not require a private-key change. A key rotation should not require a template release. A change to the final MIME structure, however, belongs to the team that can prove what happens between assembly and delivery.
There is a trade-off. Signing inside the application gives the application team direct control and can make local tests straightforward, but it expands secret handling and makes every downstream mutation dangerous. Signing at a dedicated delivery boundary centralizes key custody, while increasing the need to inspect the exact message returned from that boundary. Choose the boundary that can answer both questions: who may change the template, and who can inspect the bytes after the last mutation?
A minimum contract can be represented with a generic interface. The point is to make ownership visible in code, not to prescribe a particular provider.
type SupportReport struct {
Recipient string
Subject string
Filename string
PDF []byte
}
type Mailer interface {
SendReport(ctx context.Context, report SupportReport) (MessageID string, err error)
}
func SendSupportReport(ctx context.Context, mailer Mailer, report SupportReport) error {
if report.Recipient == "" || len(report.PDF) == 0 {
return errors.New("incomplete support report")
}
_, err := mailer.SendReport(ctx, report)
return err
}
The application does not need to know the private key to enforce the attachment contract. It does need a testable agreement about the final message: which From domain is used, which signing domain is expected, and where a failed verification is recorded.
How do you make the failure observable?
Test the whole path with a mailbox that returns the received message, then inspect headers and MIME structure. A unit test can prove that the report has the right filename; it cannot prove that a relay preserved the signed body. Keep both tests.
For each controlled test, log a correlation ID, template version, signing domain, selector, message ID, and final verification result. Do not log private keys, full customer reports, or raw message bodies by default. Alert on a sustained increase in verification failures, a missing signature, and an unexpected selector. A single failed test message is a ticket; a changed selector across all report mail is a page.
The useful runbook also records the last known-good selector and the DNS record owner. During rotation, publish the new public key before sending with the new selector, keep the old public key available for messages still in flight, and remove it only after the retention window chosen by the team. The exact window is an operational decision; document it instead of hiding it in a deployment script.
When does this advice not apply?
If the receiving system reports a policy failure unrelated to DKIM, do not force every investigation through the signature path. SPF is a separate authorization mechanism, and DMARC alignment adds another policy relationship; a DKIM pass does not replace those checks. Likewise, if a trusted downstream system intentionally rewrites the message, the correct fix may be to sign after that rewrite or to change which headers and body transformations are part of the contract.
The invariant is narrower and more useful: verify the message at the boundary that owns the final bytes, and make the selector, signing domain, template version, and change owner visible. Once those facts are available, a DKIM failure is a bounded incident with evidence. Without them, the team is left staring at a dashboard that says "delivery succeeded" while the recipient says otherwise.
Top comments (1)
The chain-of-custody framing is useful because it keeps a support incident from turning into blind DNS edits. Preserving the received message and recording d=, s=, signed headers, and recipient result gives the next investigator a stable artifact. I would add a known-good test message per sending path after a configuration change; it makes later regressions distinguishable from a one-off relay or MIME rewrite.