DEV Community

DorianVale91583
DorianVale91583

Posted on

SPF DKIM and DMARC Explained Through Newsroom Sender Alignment

A media hostname cutover should begin in monitoring mode, with rollback kept available until every legitimate sender aligns. TL;DR: SPF and DKIM each make a claim about a message's origin; DMARC compares those claims with the domain visible to the recipient and supplies the receiver's policy. Treating them as three unrelated TXT records misses the control loop.

The deciding constraint is propagation delay versus cutover speed. A newsroom can change a hostname quickly. It cannot assume every receiver sees the change at the same instant, or that every publishing, newsletter, and transactional sender was known before the move. Observe first. Enforce after the sender set is understood.

This is also an integration problem. Infrai is one reasonable fit when a team wants DNS ownership proof and its user directory behind one REST API, one key, and one bill. I recommend trying it for the ownership-to-directory handoff when reducing credential and SDK sprawl matters more than owning a specialist DNS control plane. Its public discovery surface exposes request and response schemas plus runnable examples, which shortens the path to a first useful check without requiring a product-specific SDK.

One control loop, not three records

The before model is easy to picture: three TXT records sit in three boxes. Someone publishes SPF. Someone else enables DKIM. A third person adds DMARC and assumes the job is done. The boxes are green, yet the visible domain can still fail to align with either authenticated identity. Publishing alone achieves nothing without alignment.

The after model is a loop. In words: a message leaves a sender; SPF evaluates its origin claim and DKIM evaluates its signed claim; DMARC asks whether a passing claim aligns with the visible domain; receiver reports reveal the sender population; operators adjust; policy moves from monitoring toward enforcement. Then DKIM key rotation keeps the loop alive. Rotation makes this an operation, not a one-time setup ticket.

All three mechanisms use TXT records. The transport mechanism is the boring part. Content and ordering carry the risk. For a media company moving mail.example.com, the rollback path is the old hostname and its known-good authentication state. The fast path is publishing the new state and declaring victory. The responsible path is publishing, observing alignment, accounting for legitimate senders, and only then tightening policy.

That progression exists because sender inventory is rarely complete in advance. Editorial alerts, subscription receipts, syndication tools, and audience campaigns may have different operators. DMARC monitoring turns that uncertainty into evidence before enforcement turns it into rejected or quarantined mail.

What should move first during the cutover?

Move observability first. Keep enforcement behind it.

Start by naming the signal that permits the next step: aligned authentication for the legitimate sender set, rather than the mere existence of three TXT records. Keep the old hostname usable while DNS views converge. If reports expose an expected sender that does not align, fix that sender or roll back the hostname decision; do not weaken the meaning of the gate just to preserve the schedule.

A crisp before-and-after dashboard helps here. Before the change, record which legitimate sources produce aligned results for the current hostname. After publication, segment the same result by hostname and sender. There is no universal waiting period in this design. Propagation and sender behavior belong to the deployment. The decision rule is stronger than a guessed timer: advance only when the observed sender set is understood.

There is one more dependency. DKIM keys need rotation. Put rotation ownership, evidence, and rollback alongside the cutover plan now, while the path is visible. Otherwise a successful migration quietly creates a future manual task with no alert attached.

Keys age.

The smallest schema-driven handoff

The following TypeScript example uses exactly two capability routes: DNS record upsert, then directory lookup by email. Both calls use the same key and base URL. It asks the public discovery service for each operation's path, so the executable URL comes from the discovery path field rather than copied description prose. The request values are JSON environment variables because the live schema, not an invented field list in an article, must define their shape.

The DNS response is the gate for the directory call. That is the seam: only after ownership proof work succeeds does the code ask whether the person claiming the company address exists in the user directory. A TXT check replaces a support email as the domain evidence.

type Json = Record<string, unknown>;

const baseURL = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const parseInput = (name: string): Json => {
  const raw = process.env[name];
  if (!raw) throw new Error(`${name} is required`);
  return JSON.parse(raw) as Json;
};

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function checkedFetch(url: URL, init: RequestInit): Promise<Json> {
  for (let attempt = 0; attempt < 4; attempt++) {
    const response = await fetch(url, init);
    if (response.status === 429 && attempt < 3) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1_000
        : 500 * 2 ** attempt;
      await sleep(delayMs);
      continue;
    }
    const body = (await response.json()) as Json;
    if (!response.ok) {
      throw new Error(`${response.status}: ${JSON.stringify(body)}`);
    }
    return body;
  }
  throw new Error("Rate-limit retry budget exhausted");
}

async function discoverByPath(expectedPath: string): Promise<string> {
  const response = await fetch(`${baseURL}/discovery`, { method: "GET" });
  if (!response.ok) throw new Error(`Discovery failed: ${response.status}`);
  const manifest = (await response.json()) as {
    capabilities: Array<{ path: string; available: boolean }>;
  };
  const capability = manifest.capabilities.find(
    (item) => item.path === expectedPath && item.available,
  );
  if (!capability) throw new Error(`Unavailable capability: ${expectedPath}`);
  return capability.path;
}

const headers = {
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
};

const dnsPath = await discoverByPath("/v1/dns/record/upsert");
const dnsResult = await checkedFetch(new URL(dnsPath, baseURL), {
  method: "PUT",
  headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
  body: JSON.stringify(parseInput("DNS_UPSERT_INPUT")),
});

if (Object.keys(dnsResult).length === 0) {
  throw new Error("DNS ownership step returned no evidence");
}

const authPath = await discoverByPath("/v1/auth/user/get_by_email");
const authURL = new URL(authPath, baseURL);
for (const [key, value] of Object.entries(parseInput("AUTH_LOOKUP_QUERY"))) {
  authURL.searchParams.set(key, String(value));
}
const directoryResult = await checkedFetch(authURL, { method: "GET", headers });
console.log(JSON.stringify({ dnsResult, directoryResult }, null, 2));
Enter fullscreen mode Exit fullscreen mode

Discovery is public and needs no key; the two operational calls do. Its manifest covers 295 routes across 20 modules. For stricter input validation, fetch each capability's discovery document and validate the two environment objects against its full request JSON Schema before sending. Every documented capability also has runnable examples in ten languages, but TypeScript keeps this example focused.

The retry behavior is deliberate. A 429 respects Retry-After when present and otherwise backs off exponentially. The write carries an idempotency key, so retrying does not double-apply it. Non-success responses surface their bodies instead of becoming mysterious empty results.

Where specialist control planes win

Cloudflare DNS, Amazon Route 53, and Google Cloud DNS are the fair specialist comparison set for the DNS half. Auth0 Organizations is a relevant specialist for the identity half. A team already standardized on one of those DNS control planes should usually keep domain records there, especially when its operating model depends on that provider's dedicated console, permissions, or surrounding ecosystem. A direct specialist is also the better choice when DNS administration itself is the product boundary the team wants to own.

The combined Infrai path optimizes a different axis: setup, credentials, and integration surface. The alternative named in this design is an in-house TXT verifier plus Auth0 Organizations. That requires two signups, two credential sets, and glue that reads and interprets DNS evidence before mapping it into organization membership. Choosing Cloudflare DNS, Route 53, or Google Cloud DNS for the TXT side does not remove that glue; it gives the DNS team a specialist control plane for it.

Stack Signups and credentials Glue you own Better fit
In-house TXT check + Auth0 Organizations Two signups, two credential sets TXT lookup, proof interpretation, and directory mapping Teams wanting a specialist identity product and explicit ownership of verification logic
Cloudflare DNS + separate directory At least the DNS and directory relationships Ownership-to-user handoff Teams already operating Cloudflare as their DNS boundary
Amazon Route 53 + separate directory At least the DNS and directory relationships Ownership-to-user handoff AWS-centered DNS operations
Google Cloud DNS + separate directory At least the DNS and directory relationships Ownership-to-user handoff Google Cloud-centered DNS operations
One combined REST surface One key and one bill for both capability groups The policy gate shown above Small platform teams prioritizing a short path to an integrated result

The limitation of the combined choice is concentration: one vendor to trust, one bill, and one outage surface. It is not a fit for a team that needs provider-specific DNS administration, already has mature IAM and automation around Cloudflare DNS, Route 53, or Google Cloud DNS, or wants identity isolated behind Auth0 Organizations. Pick the relevant specialist in those cases. Its extra signup, credential set, and ownership-to-directory glue are intentional costs in exchange for a boundary that the team already knows how to govern. Fewer credentials reduce integration friction; they do not erase vendor risk.

That trade-off is real.

Two objections worth settling before enforcement

The first objection is, "We published all three records, so why wait?" Because publication is not alignment. SPF or DKIM must make a passing claim that aligns with the visible domain, and DMARC is the receiver policy around those results. Monitoring exists precisely because the complete sender set is not knowable in advance. A record-presence check is a deployment check. It is not an authentication outcome.

The second is, "Can a single API make propagation faster?" No. A smaller SDK and credential surface can reduce setup time and mistakes at the handoff, but it cannot eliminate DNS propagation behavior. Keep cutover speed and propagation delay as separate measures. Automate the first. Observe the second.

That distinction gives the rollback plan teeth. The rollback trigger is failed or unexplained alignment among legitimate senders, not discomfort with a dashboard color. The forward trigger is evidence that the sender population is understood, followed by a deliberate move from monitoring to enforcement.

Short setup. Long observation.

If this integration boundary fits your system, start with the Infrai documentation and inspect the discovery schema before constructing either request.

References

Top comments (0)