DEV Community

FrozenSigh2853916
FrozenSigh2853916

Posted on

Node.js Explains Intended Versus Current DNS Configuration State During Mail Cutovers

A customer-support mail cutover has an awkward constraint: the intended state can be correct while the current DNS configuration still reflects cached data. The practical choice is to treat the intended records and the current observed records as two different datasets, then promote DMARC enforcement only after observations converge across time. Do not equate a successful write with a completed rollout.

TL;DR: store an immutable desired snapshot for SPF, DKIM, and DMARC; sample the public answers into timestamped observed snapshots; classify missing, mismatched, and still-propagating records; and gate each rollout step on evidence. The difference between desired and observed state is drift. Some drift means "not deployed." Some means "not visible here yet." Those cases need different responses.

The before and after mental model

Before, the workflow often looks like a ticket: "Add these three records," followed by a screenshot from one lookup. That compresses three distinct events into one checkbox. The authoritative configuration was accepted. A resolver returned an answer. Mail receivers evaluated authentication and alignment. Those events do not happen at the same instant, and one lookup cannot prove all three.

After, the workflow becomes a small reconciliation loop. A diagram in words looks like this:

desired snapshot -> DNS change -> repeated public observations -> semantic comparison -> rollout gate

The desired snapshot is what the support-mail domain should publish. The observed snapshot records what a named resolver returned, from which vantage point, and when. The comparison should understand record meaning instead of comparing presentation order. The gate decides whether to wait, retry an observation, investigate an unexpected value, or advance the DMARC policy.

Short version: writes are commands; observations are evidence.

That distinction matters for DMARC because the mechanism evaluates identifier alignment and tells receivers what policy the domain owner requests for mail that does not pass the relevant checks. RFC 7489 also defines aggregate and failure reporting. Those reports are downstream evidence. They complement DNS observations; they do not replace them.

How should intended DNS configuration converge with current state?

Treat drift as a typed result, not a red light. For a customer-support sender, useful categories are:

State What the observer saw Operational response
converged The expected semantic value is visible Continue collecting evidence or advance the gate
absent No expected record is visible Check delegation and publication, then observe again
mismatch A value is visible, but it differs from the desired snapshot Stop the cutover and resolve ownership of the change
mixed Different observations return old and new values Wait within the declared propagation window and resample
invalid A record is visible but cannot be parsed under the expected grammar Fix the desired value before enforcement

The mixed state is the one teams are tempted to flatten into failure. Resist that. A cache retaining an earlier answer and an authoritative source publishing an unintended answer can look identical in a single sample. Time and vantage-point metadata let the reconciler separate them.

There is also semantic drift. TXT strings may be presented in chunks, and mechanisms can be reordered without changing every relevant outcome. A raw string diff is therefore noisy. Parse the record type you own, normalize only the parts whose ordering or presentation is irrelevant, and keep the original answer beside the normalized form for debugging.

Be conservative here. Over-normalization can hide a real change.

A copyable Node.js comparison

The example below deliberately handles a narrow surface: exact desired names, timestamped observations, and stable TXT chunk joining. It does not pretend to be a full SPF, DKIM, or DMARC validator. That limitation is useful. Syntax validation and DNS convergence answer different questions.

import { resolveTxt } from "node:dns/promises";

type DesiredRecord = {
  name: string;
  kind: "TXT";
  value: string;
};

type Observation = {
  name: string;
  observedAt: string;
  resolver: string;
  values: string[];
  error?: string;
};

type Drift = "converged" | "absent" | "mismatch";

const normalizeTxt = (value: string): string =>
  value.trim().replace(/\s+/g, " ");

async function observeTxt(name: string): Promise<Observation> {
  const observedAt = new Date().toISOString();

  try {
    const answers = await resolveTxt(name);
    return {
      name,
      observedAt,
      resolver: "system-configured recursive resolver",
      values: answers.map((chunks) => chunks.join("")),
    };
  } catch (error) {
    return {
      name,
      observedAt,
      resolver: "system-configured recursive resolver",
      values: [],
      error: error instanceof Error ? error.message : String(error),
    };
  }
}

function classify(desired: DesiredRecord, observed: Observation): Drift {
  if (observed.values.length === 0) return "absent";

  const expected = normalizeTxt(desired.value);
  const actual = observed.values.map(normalizeTxt);
  return actual.includes(expected) ? "converged" : "mismatch";
}

const desired: DesiredRecord[] = [
  {
    name: "support.example.test",
    kind: "TXT",
    value: "v=spf1 include:_spf.sender.example -all",
  },
  {
    name: "selector1._domainkey.support.example.test",
    kind: "TXT",
    value: "v=DKIM1; k=rsa; p=REPLACE_WITH_PUBLIC_KEY",
  },
  {
    name: "_dmarc.support.example.test",
    kind: "TXT",
    value: "v=DMARC1; p=none; rua=mailto:dmarc-reports@example.test",
  },
];

const results = await Promise.all(
  desired.map(async (record) => {
    const observation = await observeTxt(record.name);
    return { record, observation, drift: classify(record, observation) };
  }),
);

console.log(JSON.stringify(results, null, 2));
Enter fullscreen mode Exit fullscreen mode

Replace every .example or .test placeholder before use. The example's invented values are scaffolding, not recommended policy. In particular, an SPF policy, DKIM key choice, and DMARC reporting mailbox must match the real sender inventory and operational controls.

Notice what the output preserves: the requested name, raw values, timestamp, resolver label, error, and classification. That is enough to build logs and a small metric set without pretending that a single process has a global view of DNS.

Emit one structured event per observation. Count classifications by record role and environment. Alert on a mismatch that persists beyond the rollout's declared wait window, not on the first disagreement. For a fast cutover, sample more frequently; for a cautious one, require a longer sequence of consistent observations. The policy is explicit, reviewable, and reversible.

How fast should the cutover move?

The decision is not "fast or safe" in the abstract. It is how much stale-answer risk the support operation can accept while messages are flowing.

Start before the change. Capture the old records and their DNS time-to-live values, inventory every legitimate sender, and establish DMARC reporting while the requested policy is p=none. RFC 7489 describes p=none as requesting no specific receiver action for messages that fail DMARC; it is an observation stage, not proof that the authentication setup is correct.

Then publish the desired SPF and DKIM material and observe it. Keep old signing or sending paths available during the overlap dictated by the prior cache lifetime and the mail system's own transition needs. Once repeated observations match and reporting shows the legitimate streams behaving as intended, tighten policy through a separately reviewed change. RFC 7489's pct tag can request that policy apply to a percentage of affected messages, but receivers may apply local policy, so it is a rollout control rather than a mathematical guarantee.

The crisp before/after is operational: before the gate, uncertainty pauses enforcement; after the gate, evidence permits the next step. Propagation delay is a condition to observe, while an unexplained mismatch is a condition to stop.

Doesn't DNS success prove the rollout worked?

No. It proves that a particular query path returned a particular answer at a particular time.

DMARC evaluation depends on authenticated identifiers and alignment. A correctly published DMARC record cannot make an unlisted support sender pass SPF, cannot make a signer use the expected DKIM domain, and cannot guarantee how every receiver handles a message. Aggregate reports defined by DMARC help reveal the sending sources and outcomes seen by participating receivers. Keep those reports tied to the same rollout identifier as the desired DNS snapshot, so operators can explain which configuration was intended during each reporting period.

This also changes alert design. A DNS mismatch alert should name the record, desired snapshot version, observation times, and affected rollout gate. A mail-authentication alert should use the downstream authentication and alignment evidence. Combining both into "email broken" destroys the clue an on-call engineer needs first.

Should the reconciler repair records automatically?

Usually, observation can be automatic sooner than mutation.

DNS zones are shared control planes. An automatic writer that sees a mismatch may overwrite an emergency change, another team's delegated record, or a value produced by a separate deployment pipeline. Begin with a read-only reconciler and explicit ownership metadata for each name. Make remediation a reviewed action until the system can prove exclusive authority over the record and preserve a rollback value.

Automatic repair also does nothing useful for expected mixed observations during cache expiry. Repeating the same write cannot flush recursive caches. It can, however, muddy the audit trail and reset provider-side timestamps. The trade-off is extra operational state: teams must retain snapshots and interpret repeated samples. For a tiny domain with one sender and a manually supervised change, that overhead may be unsuitable; a documented change checklist and direct DNS observations can be enough. Wait when the evidence says propagation. Stop when the evidence says conflict.

The finished design is small: versioned intent, timestamped observation, typed drift, and a gate. That gives a support team speed where speed is justified and restraint where DNS cannot offer instant global agreement. The result is not merely cleaner configuration. It is an explainable mail-authentication rollout.

Sources

Top comments (0)