Email authentication is one of the easiest parts of an infrastructure stack to misconfigure during a release because staging environments rarely mirror production DNS topology. A transactional email pipeline that passes every unit test against a local mock SMTP server can still fail catastrophically in production if an SPF include: chain exceeds the 10-lookup RFC limit, a staging proxy mutates MIME body whitespace and invalidates the DKIM body hash, or a misaligned Return-Path triggers a DMARC quarantine rule.
This guide walks through how SPF, DKIM, and DMARC interact at the protocol level, how to structure isolated DNS zones for staging and preview environments, and how to validate raw Authentication-Results headers end-to-end using a real receiving MTA such as tenmin.app.
Why local SMTP mocks cannot test authentication
Tools like Mailpit, MailHog, and Ethereal intercept outbound SMTP frames before they leave your private network or VPC. Because no public receiving Mail Transfer Agent (MTA) ever evaluates the message against public DNS records, local catchers cannot verify:
- Whether the egress IP of your ESP (Amazon SES, Postmark, SendGrid, Resend) is authorized by the domain listed in the
RFC 5321.MailFromenvelope. - Whether the public RSA or Ed25519 key published at
<selector>._domainkey.<domain>matches the private key mounted in your staging secrets manager. - Whether the visible
RFC 5322.Fromheader domain aligns (strictly or in relaxed organizational mode) with either the authenticated SPF domain or thed=tag in the DKIM signature.
To validate those three invariants, your staging environment must transmit a real message across the public internet to an external receiver that performs standard SPF, DKIM, and DMARC verification and exposes the resulting RFC 8601 Authentication-Results header.
Step 1: Isolate staging DNS on a dedicated subdomain
Never point staging mailers at your apex production domain (example.com) using relaxed test policies, and never loosen your production DMARC policy to p=none just to debug a staging service. Instead, delegate a dedicated subdomain such as staging-mail.example.com or stg.example.com.
; Staging SPF record (RFC 7208) stg.example.com. 300 IN TXT "v=spf1 include:amazonses.com -all" ; Staging custom Return-Path (bounce) subdomain bounces.stg.example.com. 300 IN CNAME feedback-smtp.us-east-1.amazonses.com. bounces.stg.example.com. 300 IN TXT "v=spf1 include:amazonses.com -all" ; Staging DKIM public key (RFC 6376) s2026q4._domainkey.stg.example.com. 300 IN TXT ( "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..." ) ; Staging DMARC policy (RFC 7489) _dmarc.stg.example.com. 300 IN TXT ( "v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; rua=mailto:dmarc-stg@example.com" )
Keep the DNS TTL low (300 seconds) on staging records so key rotations and policy adjustments propagate within five minutes during debugging sessions.
Step 2: Audit SPF and the 10-DNS-lookup ceiling
Under RFC 7208 Section 4.6.4, an SPF verifier must abort evaluation and return permerror if checking a record requires more than 10 DNS-querying mechanisms (include, a, mx, ptr, exists, and redirect). Static ip4: and ip6: CIDR blocks do not count toward the limit.
A common staging failure happens when teams append a new transactional provider to an already-crowded root SPF record that includes Google Workspace, HubSpot, Zendesk, and Salesforce. Each nested include: counts toward the same global cap of 10.
# Query the TXT record and trace nested includes from a terminal dig +short TXT stg.example.com dig +short TXT _spf.google.com
By isolating transactional mail onto a subdomain (and configuring your ESP to use a custom Return-Path domain like bounces.stg.example.com), your SPF record only needs a single include: for that specific sender, requiring just 1 or 2 DNS lookups total.
Step 3: Diagnose DKIM signature and body-hash failures
A DKIM-Signature header contains several critical tags that must match both your DNS record and the exact byte stream of the message at the moment it reaches the receiving MTA:
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=stg.example.com; s=s2026q4; t=1761384000; h=from:to:subject:date:message-id:mime-version:content-type; bh=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=; b=X9a8K...
- Selector mismatch (
s=andd=): The verifier queries<s>._domainkey.<d>over DNS TXT. If the record is missing, malformed, or wrapped across quotes with stray whitespace inside the base64 key, DKIM returnstemperrororpermerror (no key for signature). - Body hash mismatch (
bh=): If your application signs the message in-process before handing it to an outbound relay or link-rewriting proxy that injects tracking pixels, re-wraps lines past 76 characters, or converts8bitcontent toquoted-printable, the recomputed SHA-256 body hash will not matchbh=. Always sign at the final outbound MTA boundary and usec=relaxed/relaxedcanonicalization so minor whitespace normalization in transit does not break verification. - Oversigned or missing headers (
h=): Always includefrom,to,subject,date, andmessage-idin the signed header list to prevent replay and header-tampering attacks.
Step 4: Verify DMARC identifier alignment
A message can pass both SPF and DKIM in isolation and still fail DMARC. DMARC requires identifier alignment: the domain in the human-visible From: header (RFC 5322.From) must match the domain validated by SPF (the Return-Path domain) and/or the domain validated by DKIM (the d= tag).
- Relaxed alignment (
aspf=r; adkim=r, the default): Any subdomain of the same organizational domain aligns. For example,Return-Path: <bounce@bounces.stg.example.com>aligns withFrom: <no-reply@stg.example.com>because both share the organizational rootexample.com. - Strict alignment (
aspf=s; adkim=s): The domains must match character-for-character. If you setaspf=s, a bounce address onbounces.stg.example.comwill fail SPF alignment when sending fromno-reply@stg.example.com, meaning DMARC survival depends entirely on an exact-match DKIM signature (d=stg.example.com).
Step 5: Inspect the live Authentication-Results header
Once your staging DNS records are live, trigger a test email from your staging pipeline to a fresh @tenmin.app address. When the message arrives in the inbox, click Raw .eml to download the unmodified RFC 5322 payload and inspect the top Authentication-Results header written by the receiving inbound MTA:
Authentication-Results: mx.tenmin.app; spf=pass smtp.mailfrom=bounces.stg.example.com; dkim=pass header.i=@stg.example.com header.s=s2026q4 header.b=X9a8K2pQ; dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=stg.example.com
Before promoting a DNS or ESP change to production, verify that all three verdicts report pass and that dis=NONE (disposition: none) confirms the message satisfied your strict alignment rules. Because tenmin.app expires every test inbox automatically after 600 seconds, no staging credentials, magic links, or pre-release templates persist after your verification run completes.
Further reading
- A developer's guide to testing transactional email with a disposable inbox covers end-to-end QA assertions for password resets, magic links, and HTML rendering.
- How RFC 5322 MIME parsing works in serverless edge environments explains how inbound byte streams are parsed after SMTP authentication.
- Email & DNS Glossary for quick definitions of SPF, DKIM, DMARC, MX, and HELO/EHLO terminology.
Need an ephemeral test inbox right now?
Generate a fresh @tenmin.app address in one click. Zero registration, sandboxed HTML rendering, automatic 600-second TTL deletion.