Skip to main content
The Toolchain

Diagnosing SPF, DKIM and DMARC Alignment Gaps

How a purpose-built email authentication diagnostics tool resolves SPF chains, DKIM selectors and DMARC alignment to catch spoofing gaps DNS TXT lookups.

Diagnosing SPF, DKIM and DMARC Alignment Gaps
Marcus ThorneMarcus Thorne11 min read

In this review

Share

A domain can publish a syntactically valid SPF record, sign every outbound message with DKIM, and still fail DMARC enforcement in production. This is the single most common ticket that lands on a messaging or security engineer’s desk after a phishing simulation flags spoofed mail as delivered. The root cause is almost never a missing record; it is an identifier alignment failure buried three DNS lookups deep in a redirect chain. Building proper email authentication diagnostics tooling means moving beyond a single dig TXT query and modelling the full verification path the way a receiving MTA actually walks it.

This article covers the architecture of a diagnostic generator that resolves SPF, DKIM and DMARC records concurrently, reconstructs the alignment decision an inbox provider would make, and surfaces the exact mechanism causing a spoofing-adjacent failure before it shows up in a bounce report.

#The Alignment Gap: Why SPF and DKIM Passing Individually Isn’t Enough

RFC 7489 defines DMARC as a policy layer sitting on top of SPF (RFC 7208) and DKIM (RFC 6376), but the critical detail engineers miss is that DMARC does not care whether SPF or DKIM pass in isolation. It cares whether the domain that passed matches the RFC5322.From header domain, under either strict or relaxed alignment mode.

The classic failure: a company uses a third-party ESP (SendGrid, Mailchimp, HubSpot) to send transactional mail. SPF passes because the ESP’s sending IP is authorised via an include: mechanism. DKIM passes because the ESP signs the message. But the DKIM signature’s d= tag is sendgrid.net, not the organisation’s own domain, and the From header is billing@company.com. Under strict alignment (adkim=s), that DKIM pass is discarded for alignment purposes. If SPF alignment also fails (common when the envelope-from is rewritten to a subdomain the ESP controls), DMARC evaluates as a failure despite two green checkmarks upstream. This is precisely the gap that manual, single-record lookups never expose, and it is the reason email authentication diagnostics needs to model the alignment engine, not just the record parser.

#Architectural Breakdown of an Email Authentication Diagnostics Tool

A production-grade diagnostics tool has four discrete layers, each with its own failure surface. Treating them as a single monolithic “check the domain” function is the mistake most homegrown scripts make, and it is why they miss redirect-chain and subdomain-policy edge cases.

#
DNS Resolution Layer

All queries must go through a resolver that respects TTL, supports concurrent async lookups, and does not silently follow open redirects. Use a dedicated resolver instance (via dnspython or equivalent) rather than the OS stub resolver, so you can control timeout, retry count, and DNSSEC validation independently per record type.

#
SPF Chain Walker

SPF evaluation is recursive. A record can contain include:, redirect=, and mechanism macros that trigger further lookups. RFC 7208 hard-caps this at 10 DNS lookups per evaluation; exceeding it produces a permerror, which most receivers treat as a fail. The walker needs a lookup counter that mirrors this exactly, not an approximation.

email authentication diagnostics

#
DKIM Selector Verifier

DKIM has no discovery mechanism — you must already know the selector (from a captured header) to query selector._domainkey.domain.com. The verifier layer fetches the public key, checks key type and length (flagging any RSA key under 1024 bits as deprecated per current provider policy), and confirms the d= tag matches an expected organisational domain.

#
DMARC Policy Evaluator and Alignment Engine

This is where the actual decision is made. The evaluator pulls _dmarc.domain.com, parses p=, sp=, adkim=, aspf=, and pct=, then cross-references the SPF and DKIM results against the From domain using the alignment mode specified. The output is a pass/fail per mechanism plus an overall DMARC disposition — exactly what a receiving mail server computes internally.

Rendering diagram...

#Implementation Logic

The diagnostics workflow follows a strict sequence to avoid false positives caused by evaluation order mistakes:

  • Resolve the DMARC record first to capture adkim, aspf and sp before evaluating anything else — these values dictate how strict the alignment check must be.
  • Walk the SPF chain recursively, incrementing a lookup counter on every include, a, mx and redirect mechanism, aborting at 10 with an explicit permerror flag rather than a silent fail.
  • Extract the DKIM selector from a sample message header (or accept it as a tool input for proactive scanning) and fetch the public key record.
  • Run the alignment comparison: for relaxed mode, compare organisational domains (strip subdomains to the registrable domain); for strict mode, require an exact match against the From header domain.
  • Aggregate into a single disposition object mirroring RFC 7489 section 6.6.2’s evaluation logic.

#
Code and Configuration

A minimal async SPF and DMARC resolver core, using dnspython for the lookups:

1import dns.resolver
2
3def get_txt(domain):
4 try:
5 answers = dns.resolver.resolve(domain, "TXT", lifetime=3.0)
6 return [b"".join(r.strings).decode() for r in answers]
7 except dns.resolver.NXDOMAIN:
8 return []
9
10def walk_spf(domain, lookups=0, seen=None):
11 if seen is None:
12 seen = set()
13 if lookups > 10 or domain in seen:
14 return {"result": "permerror", "lookups": lookups}
15 seen.add(domain)
16 records = [r for r in get_txt(domain) if r.startswith("v=spf1")]
17 if not records:
18 return {"result": "none", "lookups": lookups}
19 mechanisms = records[0].split()
20 for m in mechanisms:
21 if m.startswith("include:"):
22 lookups += 1
23 walk_spf(m.split(":", 1)[1], lookups, seen)
24 if m.startswith("redirect="):
25 lookups += 1
26 return walk_spf(m.split("=", 1)[1], lookups, seen)
27 return {"result": "pass", "lookups": lookups}

The DKIM and DMARC lookups follow the same TXT retrieval path but target fixed subdomains:

1dig +short TXT selector1._domainkey.company.com
2dig +short TXT _dmarc.company.com

The tool’s output should be a structured disposition, not raw text, so it can feed downstream alerting or CI gates for domain onboarding:

Diagnosing SPF, DKIM and DMARC Alignment Gaps architecture diagram 2
1{
2 "domain": "company.com",
3 "spf": { "result": "pass", "lookups": 4 },
4 "dkim": { "result": "pass", "d_domain": "sendgrid.net" },
5 "dmarc": {
6 "policy": "quarantine",
7 "adkim": "s",
8 "aspf": "r",
9 "alignment": {
10 "spf_aligned": true,
11 "dkim_aligned": false
12 },
13 "disposition": "fail"
14 }
15}

That single dkim_aligned: false field is the entire value proposition of proper email authentication diagnostics over a manual record check — it tells the engineer exactly which mechanism to fix, rather than forcing a manual trace through raw TXT output.

#Failure Modes and Edge Cases

Several non-obvious conditions break naive implementations. Each of the following has been observed causing silent authentication failures in production environments that a single-record check would never catch.

Failure ModeRoot CauseDiagnostic Signal
SPF permerrorChain exceeds 10 DNS lookups via nested includesLookup counter hits cap before resolving final mechanism
DKIM alignment fail under strict modeESP signs with its own d= domain, not the sender’sd_domain differs from From header registrable domain
DMARC subdomain bypassMissing sp= tag lets subdomains inherit a looser defaultSubdomain evaluated against organisational policy instead of explicit sp=
SPF void lookup exhaustionMultiple includes resolve to NXDOMAIN, each still consuming a lookup slotRFC 7208 void-lookup limit (2) reached before permerror threshold
DKIM key rotation driftOld selector still referenced by mail headers after key rolloverPublic key fetch returns a record whose signing timestamp predates rotation
Partial rollout false passpct= below 100 lets a fraction of failing mail through unenforcedDisposition marked “fail” but delivery logs show no rejection

The partial rollout case deserves particular attention because it produces the most confusing support tickets: the diagnostics tool correctly reports a DMARC fail, yet the message was delivered anyway. That is expected behaviour under pct= sampling, not a tool defect, and the diagnostic output should surface the pct value alongside the disposition to prevent engineers chasing a phantom bug.

#Scaling and Security Trade-offs

Running email authentication diagnostics against a handful of domains during incident response is trivial. Running it continuously across thousands of domains — as a platform capability rather than a one-off script — introduces trade-offs that mirror broader architectural patterns used in distributed scanning systems:

  • Resolver throttling: aggressive concurrent SPF chain walks against the same authoritative nameserver can trigger rate limiting; batching with jittered concurrency limits (typically 20-50 in-flight queries per worker) avoids false permerrors caused by resolver timeouts rather than genuine chain length.
  • DNS amplification risk: a maliciously crafted SPF record with deeply nested includes can be used to force a scanner into excessive lookup volume against third-party infrastructure; enforcing the RFC 7208 lookup cap in code, not just in the report, prevents the tool itself becoming an unwitting amplification vector.
  • Cache freshness versus load: respecting DNS TTL reduces resolver load but risks reporting a stale DKIM key immediately after rotation; a dual-mode cache (respect TTL for scheduled scans, bypass for on-demand diagnostics) balances the two.
  • Alignment mode assumptions: defaulting to relaxed alignment in the tool when a domain’s DMARC record omits adkim/aspf (per RFC 7489 default) must be explicit in the output, since silently assuming strict mode produces false negatives that mask real deliverability risk.
  • Credential exposure: selectors and DKIM public keys are not secrets, but scanning infrastructure that also ingests raw message headers to extract selectors must be scoped so it never persists message bodies, keeping the diagnostic surface limited to authentication metadata only.

None of these trade-offs are solved by a better regex on a TXT record. They are solved by treating email authentication diagnostics as a stateful evaluation pipeline — DNS resolution, chain walking, key verification, and alignment computation as separate, testable stages — so that when a domain fails DMARC in production, the tool tells you which of those four stages broke, rather than leaving that reconstruction to a tired engineer at 2am with a copy of dig and a hunch.

Evidence trail

Sources and verification

Primary documentation and external technical references used in this article.

  1. 01dnspythondnspython.readthedocs.io
Marcus Thorne

Marcus Thorne

Toolchain Reviewer

Marcus Thorne is a pragmatic software architect focused on highly concurrent, distributed transactional systems.

Published Last changed
View Profile
Reader Interaction

Comments

Add a thoughtful note on Diagnosing SPF, DKIM and DMARC Alignment Gaps. Comments are checked for spam and held for moderation before appearing.

Loading comments...
Comment submission is disabled until Cloudflare Turnstile keys are configured.

Learn More About KBY

Was this useful?

New engineering tools, ready to use.

Receive new calculators, diagnostic tools, Engineering Labs and technical reference systems.