Use change events to prove what your B2B SaaS asked for, authoritative DNS snapshots to prove what the Internet returned, and reconciliation records to connect the two. Neither source is a complete audit history by itself. For onboarding, the decisive evidence is a timestamped chain from expected record to observed answer, with resolver errors and later drift preserved rather than overwritten.
| Evidence source | What it can establish | What it cannot establish | Pick this when |
|---|---|---|---|
| Application change events | Actor, request ID, intended name, type, value, and workflow outcome | What an authoritative server actually served | You must explain who requested a verification change and why |
| Authoritative DNS snapshot | The answer, TTL, response code, and responding authority observed at a specific time | Who caused the state or what existed between samples | You must prove externally visible state at a checkpoint |
| Reconciliation record | Whether expected and observed state agreed under a declared comparison rule | Continuous history unless checks run continuously | You must make an onboarding decision reproducible |
Decision rule: approve ownership only from a successful observation that matches the expected challenge and is linked to its immutable change event. Keep later observations so compliance reviewers can distinguish initial proof from continuing configuration health.
Should application logs or live DNS zone reads define audit history?
DNS answers describe an observation now, not a ledger of earlier values. A current read cannot show that a TXT challenge existed last Tuesday, who placed it, or whether it disappeared for six hours. TTL controls how long resolvers may cache data; it does not create historical evidence. Negative answers have caching behavior too, defined by the zone's SOA data.
Application logs cover a different slice. They can record that tenant acme-42 requested verification of billing.example, that the service expected a TXT token, and that a workflow advanced. They still cannot prove the public DNS answer matched the request. A successful API write, a queued job, or a UI confirmation is intent. Deliverability evidence needs observation.
Keep those claims separate.
That gap matters.
For mail-related onboarding, the distinction becomes sharper. SPF publishes authorization in DNS, while DMARC policy and reporting build on authenticated identifiers and DNS-discovered policy. A log saying "DMARC configured" is weaker than a captured lookup containing the evaluated record and time. It is also incomplete without the expected policy used for comparison.
Pick events for causality, snapshots for reachability
Pick application events when the audit question begins with who, what request, or which workflow version. Emit a stable event ID before asynchronous verification starts. Include the normalized domain, record type, expected-value digest, tenant, actor or service principal, and correlation ID. Do not log a reusable ownership token in plaintext; a digest can support equality checks while reducing token exposure.
Pick authoritative snapshots when the question begins with what did DNS serve. Record the queried name and type, observation time, response code, answers, TTLs, and the nameserver that supplied the response. Querying the authoritative path avoids confusing a recursive resolver's cached view with the zone's current published view, but the observer's network and timing still belong in the evidence.
A useful diagram in words is: onboarding request -> immutable expectation event -> scheduled authoritative query -> normalized snapshot -> comparison result -> onboarding decision. Every arrow carries the same correlation ID. Retries append observations. They never revise the earlier ones.
Consider a reviewer investigating a domain approved at 14:03 UTC. The event says which tenant requested the check and contains the expected-value digest. The 14:02 observation is a mismatch, the 14:03 observation is a match from the named authority, and the 14:08 observation is NXDOMAIN after the tenant removes the challenge. A current lookup reveals only the last state. A bare verified=true log reveals only the decision. The joined records explain the entire sequence without pretending that any sample describes the minutes between samples. This is the level of precision an audit control should promise.
This has an operational payoff. Alert on stale pending verifications and repeated DNS error classes, not on every failed poll. A nonexistent name, a timeout, and a valid answer with the wrong TXT value are different signals. Folding them into one verified=false counter destroys the clue an operator needs.
Build the reconciliation seam in Node.js
The comparison rule should be small, deterministic, and versioned. The network adapter can change without changing what a match means. The example below uses only Node.js types and an injected authoritative reader, so the audit logic stays independent of a DNS service or storage product.
Make it replayable.
import { createHash } from "node:crypto";
type Expectation = {
eventId: string;
tenantId: string;
fqdn: string;
recordType: "TXT";
expectedDigest: string;
requestedAt: string;
ruleVersion: 1;
};
type TxtObservation = {
observedAt: string;
authority: string;
rcode: string;
values: string[];
ttls: number[];
};
type Reconciliation = {
eventId: string;
observation: TxtObservation;
outcome: "match" | "mismatch" | "dns_error";
ruleVersion: 1;
};
interface AuthoritativeReader {
readTxt(fqdn: string): Promise<TxtObservation>;
}
const digest = (value: string): string =>
createHash("sha256").update(value, "utf8").digest("hex");
export async function reconcile(
expected: Expectation,
reader: AuthoritativeReader,
): Promise<Reconciliation> {
const observation = await reader.readTxt(expected.fqdn);
if (observation.rcode !== "NOERROR") {
return {
eventId: expected.eventId,
observation,
outcome: "dns_error",
ruleVersion: expected.ruleVersion,
};
}
const matched = observation.values.some(
(value) => digest(value) === expected.expectedDigest,
);
return {
eventId: expected.eventId,
observation,
outcome: matched ? "match" : "mismatch",
ruleVersion: expected.ruleVersion,
};
}
TXT data needs deliberate normalization. RFC 1035 defines TXT RDATA as one or more character-strings, each with a one-octet length field; libraries may expose those chunks separately. Join chunks within one TXT record before hashing, but do not join distinct TXT records together. Preserve the raw parsed form beside the normalized value so a later rule revision can be replayed.
Store the expectation, observation, and result as append-only records. Add a content hash or use storage controls that make alteration detectable. Retention should follow the compliance obligation and the tenant's data lifecycle, not an arbitrary logging default. Access matters as much as retention because tenant identifiers, domains, and verification material can be sensitive operational data.
Tests should pin the uncomfortable cases: multiple TXT records, a multi-string TXT record, NXDOMAIN, NOERROR with no matching answer, timeouts, duplicate observations, mixed case in domain names, and a challenge that changes while a previous check is in flight. Use a clock abstraction so timestamps are deterministic. Then run integration tests against a disposable authoritative zone; mocks cannot demonstrate wire-format parsing or delegation behavior.
Make the evidence operable
Three metrics are enough to expose most failures without turning the audit store into a metrics backend: reconciliation outcomes by error class, age of the oldest pending expectation, and time from expectation to first match. Keep tenant and domain out of metric labels because their cardinality grows without a useful bound. Put those identifiers in structured events, where an operator can search by correlation ID.
Logs should describe transitions: expectation accepted, observation completed, comparison evaluated, decision recorded. A scheduled poll that finds the same mismatch repeatedly does not need a fresh high-severity log each time. The observation still belongs in the audit stream if the sampling policy promises it; the operational log can aggregate repeats.
Raw evidence stays.
Set alerts around the service objective for onboarding evidence. A rise in dns_error outcomes points toward query transport, delegation, or authority reachability. A rise in clean mismatches usually points toward customer configuration or a stale challenge. One alert cannot route both well.
The clean before/after is stark. Before: domain_verified=true appears in an application log, while the underlying DNS answer is gone and no one can reconstruct the decision. After: the decision references expectation evt_..., observation time, authority, normalized answer digest, comparison rule version, and outcome. An auditor can replay the decision without trusting a mutable status field.
Limits of this evidence model
The main trade-off is sampling cost versus historical resolution. Periodic reads still leave gaps between samples. DNSSEC can provide origin authentication and integrity for signed DNS data, but it does not turn DNS into a historical database. If the requirement is to prove every externally visible transition, collect observations at a cadence justified by that requirement and preserve zone-side change history where you control the zone. State the sampling interval in the control description.
This model is not suitable for packet-by-packet reconstruction, and it cannot prove what every recursive resolver returned. Use passive DNS or resolver telemetry when that broader observation scope is an explicit requirement, subject to its own retention, privacy, and coverage limitations. Use authoritative zone journals when you operate the zone and need every accepted change. Those sources complement the onboarding evidence; they do not replace the application-side actor and intent.
Also separate ownership proof from long-term mail readiness. A matching challenge demonstrates control at an observed moment. It does not guarantee that MX, SPF, DKIM, or DMARC will remain correct, nor that mail will be delivered. Those need their own checks and evidence.
The practical boundary is concise: events explain causality, snapshots establish observed DNS state, and reconciliation makes the onboarding decision reviewable. Keep all three.
Top comments (0)