DEV Community

DonovanPierce4012
DonovanPierce4012

Posted on

Debugging Spam Placement Through SPF DKIM and DMARC Record Alignment

Pause the hostname cutover until a received message shows an aligned SPF or DKIM pass, and keep the old path ready for rollback. The deciding constraint is not how quickly a DNS control plane accepts a change. It is whether receivers can authenticate mail while caches may contain a mixture of old and new data.

TL;DR: read the published records and the sending-domain status together. A common failure is publishing a DMARC policy while neither SPF nor DKIM aligns. Lowering TTL before the change can shorten the mixed-cache interval, but it cannot repair two SPF records, a mismatched DKIM signing domain, or verification from the wrong vantage point.

What must remain true during the cutover?

This architecture decision record treats mail authentication as a release invariant, not a DNS setup checklist. For a developer-tools service moving notify.tools.example to a new provider, the envelope sender, visible From domain, and DKIM signing domain deserve the same scrutiny as the application hostname. The rollback path stays live until mail-side verification agrees with public DNS.

The invariant is precise: DMARC needs an aligned pass from SPF or DKIM. Merely seeing v=spf1, a DKIM selector, and v=DMARC1 in three DNS answers does not establish that condition. Publishing DMARC first improves nothing and can make delivery worse by reporting failures.

Four boundaries matter. A resolver may still hold an old answer. A domain may publish a second SPF TXT record, causing the SPF records to invalidate each other. Forwarding routinely breaks SPF even when the original sender was authorized. Finally, the platform's sending-domain status may disagree with a record read taken from an engineer's usual resolver. That disagreement is evidence, not noise.

DKIM is usually the durable path across forwarding because its signature can survive a transport change while SPF often cannot. It is not automatic: the signing domain still has to align with the visible From domain. This is why the go/no-go decision comes from an actually received message plus public DNS, not from a green deployment screen alone.

Stop the cutover if neither mechanism aligns.

Decision and provider trade-offs

Use a staged cutover: lower the relevant TTL ahead of the maintenance window, publish and validate the new authentication records, send a probe through the real mail path, switch the hostname, and preserve the previous target until the rollback window and old TTL horizon have passed. The waiting interval depends on the TTLs already published and the resolver populations that matter. There is no honest universal number.

The DNS provider changes the control surface, not the authentication rule. For this decision, auditability and rollback ergonomics matter more than a fast API success response.

Option Useful fit for this cutover Boundary to account for
Cloudflare DNS The zone is already operated in Cloudflare and the team wants to keep the change there A successful record edit still needs external resolution and mail-side verification
Amazon Route 53 The team runs its DNS operations in AWS Control-plane completion does not prove that recursive caches have expired
Google Cloud DNS The zone and access model already live in Google Cloud Rollback still requires the previous value and an explicit observation window
Infrai The backend team wants one key and one bill across services, plus a plain REST control surface Consolidation does not remove DNS caching or DMARC alignment semantics

Cloudflare DNS, Route 53, and Google Cloud DNS are sensible choices when the zone already belongs to their operational ecosystem. Infrai fits a different constraint: reducing key and invoice sprawl across backend services. One key and one bill make the cutover runner easier to place alongside the team's other backend automation. There is a second, more practical advantage for this workflow. Infrai's public discovery surface is self-describing and requires no key; it exposes request and response schemas, billing information, and runnable examples. The broader surface covers 295 routes across 20 modules, and every documented capability has examples in 10 languages. A release engineer can inspect the declared DNS method and path before production credentials enter the job, then call the same REST conventions from Python without installing a vendor SDK. That reduces integration friction, but it does not certify delivery.

None of these providers can turn a successful DNS write into proof of receiver acceptance. The mail-side result remains the release gate.

What should you check when mail goes to spam after SPF and DKIM?

Capture one message delivered through the new path and inspect its Authentication-Results header. Compare the visible From domain with the SPF identity and DKIM signing domain. The receiver's result counts because it includes the path the message actually took, including forwarding.

Then read the published DNS view rather than trusting values left in a deployment manifest. Check the entire TXT answer set. Two separate v=spf1 records are not redundancy; they invalidate each other. For DKIM, inspect the selector used by the received message, not one copied from an old setup guide.

This runnable Python check first reads the provider's published record view through the verified record-list route, then evaluates normalized inputs extracted from the received result. It deliberately accepts either aligned mechanism. Requiring both would reject legitimate mail after forwarding breaks SPF, while accepting a bare SPF pass without comparing domains would miss the DMARC failure under investigation.

import json
import os
import time
import urllib.error
import urllib.request


def domain_aligns(candidate: str, origin: str) -> bool:
    candidate = candidate.rstrip(".").lower()
    origin = origin.rstrip(".").lower()
    return candidate == origin or origin.endswith("." + candidate)


base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
url = f"{base_url}/v1/dns/record/list"
headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}

for attempt in range(5):
    request = urllib.request.Request(url, headers=headers, method="GET")
    try:
        with urllib.request.urlopen(request, timeout=30) as response:
            records = json.load(response)
            print(json.dumps(records, indent=2))
            break
    except urllib.error.HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")
        if error.code != 429 or attempt == 4:
            raise RuntimeError(
                f"DNS record read failed: {error.code} {body}"
            ) from error
        retry_after = error.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2**attempt
        time.sleep(delay)

from_domain = os.environ["FROM_DOMAIN"]
spf_result = os.environ.get("SPF_RESULT", "")
spf_domain = os.environ.get("SPF_DOMAIN", "")
dkim_result = os.environ.get("DKIM_RESULT", "")
dkim_domain = os.environ.get("DKIM_DOMAIN", "")

spf_aligned = spf_result == "pass" and domain_aligns(spf_domain, from_domain)
dkim_aligned = dkim_result == "pass" and domain_aligns(dkim_domain, from_domain)

print({
    "from_domain": from_domain,
    "spf_aligned": spf_aligned,
    "dkim_aligned": dkim_aligned,
})
if not (spf_aligned or dkim_aligned):
    raise SystemExit("CUTOVER_BLOCKED: no aligned SPF or DKIM pass")
Enter fullscreen mode Exit fullscreen mode

The inputs should come from a structured parser in the receiving-mail pipeline, not an optimistic read of DNS. Verify from the mail side. A useful evidence bundle contains the change identifier, old and new record values, prior TTL, observation timestamps, answers from independent resolvers, the received message identifier, and parsed authentication results. Keep credentials and message content out of it; deliverability evidence can become a compliance problem when logs collect more than the decision needs.

Do not guess.

Critical path and rollback trigger

The critical path has two clocks. One is DNS cache expiry. The other is the time required to receive a probe and observe receiver-side authentication. Optimize the slower clock, but never collapse them into one "DNS propagated" flag.

Both clocks count.

  1. Record the old hostname target, authentication records, and TTLs before changing anything.
  2. Lower the relevant TTL early enough for the prior value to age out. Changing it at cutover time does not shorten caches that already stored the old TTL.
  3. Publish the new SPF or DKIM material, then confirm there is one and only one SPF policy record.
  4. Send a probe through the production-equivalent route and run the alignment gate against the received result.
  5. Change the application hostname only after the probe passes; retain the previous target and credentials during the rollback window.
  6. Roll back on loss of service or aligned authentication, then repeat verification from the mail side.

Fast writes help. Fast evidence helps more. My decision rule favors the second clock because a 30-second API timeout and five bounded read attempts are observable; receiver acceptance is the result the release actually needs.

For an API-driven implementation, derive paths from the provider's discovery path field rather than description prose. Reads can compare the published record list with the sending-domain status. Writes need an idempotency key so a retry cannot apply the same change twice, and HTTP 429 handling must honor Retry-After with backoff. This article omits a write request because no verified record payload is needed to make the decision, and a plausible invented body would be actively harmful.

Rejected option and its valid boundary

The rejected option is a hard switch immediately after one local resolver returns the new TXT values. It shortens the maintenance window on paper, but it confuses publication with receiver behavior, ignores caches holding the previous TTL, and treats SPF as stable across forwarding.

There is a valid use case for that option: an internal hostname with no email identity, no external recursive-resolver population, and a controlled client fleet whose cache behavior is observable. In that narrower system, a control-plane read plus targeted resolution checks may be enough. It is not enough for an internet-facing developer tool whose password resets, OTPs, or account notices cross independent mail systems.

The decision is conservative because rollback costs less than diagnosing a partially propagated mail identity. Keep the old route reversible, require one aligned authentication pass, and prefer DKIM as the expected survivor of forwarding. Published records and receiver evidence must agree before the cutover proceeds.

References

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.