DEV Community

MagnusNilsson2124
MagnusNilsson2124

Posted on

Five-Stage Marketplace DNS Evidence Pipeline for Customer Verification Documents

Marketplace email breaks in an awkward place: the team can know exactly what a customer should publish, while the customer-facing PDF and the DNS checker each use a different version of that knowledge. The useful design constraint is to make one signed evidence object drive both outputs. DNS instructions are a projection of that object; a verification PDF is another projection. Neither should be an independently edited document.

Short answer: store the intended SPF, DKIM, and DMARC records with their validation rules, render customer instructions and the verification PDF from that same version, and compare observed DNS against the version the customer was given.

Why does intent drift between DNS instructions and customer evidence?

The first failure is usually mundane. A marketplace adds a sending domain, an operator copies a TXT value into a ticket, and a documentation job later renders a PDF from a template. Then a selector changes, a DKIM key rotates, or a DMARC policy moves from p=none to p=quarantine. The checker sees the new intent; the customer still has the old instructions. Both artifacts look plausible in isolation.

I have spent enough time around spam filters, rate limits, and OTP delivery gaps to distrust “the record is in the database” as a completion signal. A record has at least three identities: the desired value, the value communicated to a customer, and the value observed through DNS. A useful record ID and an immutable revision tie those identities together.

The source object should contain the owner domain, record type, name, value, selector where relevant, and operational metadata such as TTL expectations. It should also carry a canonicalization policy. TXT values are especially easy to damage when a renderer inserts smart quotes, wraps a long value, or escapes a semicolon. The renderer must preserve bytes, not merely the meaning a human thinks they saw.

How should customer DNS instructions and verification documents share one evidence model?

Treat the evidence model as a small contract, not as a page-shaped blob. A JSON-like representation is enough, provided the fields are versioned and validated before publication:

from dataclasses import dataclass
from typing import Literal


@dataclass(frozen=True)
class DnsIntent:
    revision: int
    domain: str
    kind: Literal["SPF", "DKIM", "DMARC"]
    name: str
    value: str
    selector: str | None
    ttl_seconds: int | None


def customer_instruction(intent: DnsIntent) -> dict[str, str | int | None]:
    return {
        "record_type": intent.kind,
        "host": intent.name,
        "value": intent.value,
        "selector": intent.selector,
        "ttl_seconds": intent.ttl_seconds,
        "revision": intent.revision,
    }


def verification_claim(intent: DnsIntent) -> dict[str, str | int]:
    return {
        "domain": intent.domain,
        "record_type": intent.kind,
        "host": intent.name,
        "expected_value": intent.value,
        "intent_revision": intent.revision,
    }
Enter fullscreen mode Exit fullscreen mode

The PDF renderer can turn customer_instruction into readable steps, while a checker can turn verification_claim into a lookup and comparison. The important detail is that both functions receive the same frozen object. A PDF job should fail closed if the object is missing a revision or contains an invalid record type; producing a polished document with incomplete evidence creates a support incident later.

Keep it boring.

Keep the canonical value separate from display text. For DKIM, the selector belongs in the DNS name (selector._domainkey.example), while the public key is in the TXT value. For DMARC, the name is normally _dmarc.example; the policy tags inside the value have their own syntax and semantics. SPF is also a TXT record, but it describes authorized sending sources and has lookup limits defined by its standard. These are different validation rules, even though a customer may see “TXT” three times in a table.

One short paragraph can prevent a week of confusion: show the exact host, record type, value, and revision in the PDF, then show the same revision in the verification result. A screenshot of a DNS console is not evidence of which intent was active.

What checks catch drift before a marketplace sends mail?

Use checks that operate at different boundaries. The first is a schema check: reject an empty host, malformed DMARC tag syntax, or a DKIM value that is not represented exactly as stored. The second is a render check: parse the generated instructions and PDF data, then assert that every expected field and revision appears. The third is an observation check: query authoritative DNS, normalize only according to the relevant standard, and compare the result with the intended value.

The comparison should retain history. A customer may have published revision 4 while the marketplace has already prepared revision 5. That is not automatically a failure; it is a known transition. Mark it as pending rotation, keep both revisions in the audit trail, and avoid telling the customer to replace a working key until the new one is ready. For DMARC, policy changes deserve a staged rollout because aggregate reports can reveal senders the inventory missed.

Consider a marketplace that onboards a regional seller on Monday. The onboarding service creates revision 12 for mail.seller.example, and the PDF worker renders the selector mkt-2026-01. On Tuesday, security rotates the key and creates revision 13, but a delayed worker delivers the revision-12 PDF through the support portal. If the checker merely asks whether some DKIM key exists, the seller appears healthy while mail is signed with a selector the document never mentioned. A revision-aware checker instead reports that the observed selector belongs to revision 13 and the downloaded artifact names revision 12. That message gives support a precise repair: regenerate or relink the artifact, then ask the seller to confirm the current host. No one has to guess whether DNS propagation, a copied value, or a stale render caused the mismatch. This is why the revision is evidence, not decoration; it lets an operator explain a discrepancy without changing a live record blindly.

The useful failure message names the boundary: “observed TXT at _dmarc.shop.example does not match intent revision 7,” followed by the expected and observed hashes. Do not paste a full DKIM key into a log visible to every support role. Hashes make drift searchable without turning logs into a second secret store.

Here is a compact test shape. It is deliberately independent of a DNS provider:

def compare(intent: DnsIntent, observed_values: list[str]) -> str:
    if intent.value in observed_values:
        return "match"
    if not observed_values:
        return "missing"
    return "drift"
Enter fullscreen mode Exit fullscreen mode

Your mileage may vary on resolver timing. A recursive resolver can serve an older answer until its cache expires, so record the resolver, query time, and TTL with each observation. That context distinguishes propagation delay from a customer editing the wrong host.

Which architecture keeps the workflow auditable?

A practical pipeline has five stages: intent creation, validation, rendering, publication, and observation. Each stage emits an event containing the domain and revision. The PDF and the instruction page are artifacts of the rendering stage, not alternate sources. Publication records who approved a change; observation records what DNS returned and when.

Keep the write path narrow. One service owns intent revisions, while workers receive immutable messages to render artifacts and run checks. Idempotency matters because a queue can deliver the same render request twice. The output path should include the revision, making it impossible for a late worker to overwrite a newer PDF under the same filename.

The catch is operational complexity. This model is not suitable when a small team only sends from one domain and can review records manually; a signed spreadsheet and a documented change review may be enough there. It also does not replace DNS provider controls, registrar access, or DMARC report analysis. Stick with a simpler runbook when the audit requirement is low, and adopt the versioned pipeline when several teams, tenants, or rotating selectors make drift likely.

A compact rollout for customer-facing systems

Start by importing the records that customers have already received and assign explicit revisions; do not silently declare old PDFs current. Generate a new instruction page and PDF from the imported intent, then run render and observation checks in a staging domain. Add a dashboard keyed by domain and revision, with separate states for match, missing, drift, and pending propagation.

Before enabling enforcement, sample DMARC aggregate reports and reconcile every legitimate sender. Publish a low-risk policy first, watch reports, and raise enforcement only after the source inventory is credible. The final decision rule is simple: a customer is “verified” only when observed DNS matches the exact intent revision linked from the customer artifact.

References

Top comments (0)