LocalEndpoint / Endpoint Strategy
DNS-Based Proof of Ownership for ErrorNotifier.com
Report summary
For ErrorNotifier.com, the most robust and practical way to add domain proof-of-ownership is to ship TXT-based verification as the default path , offer CNAME-based verification as a second path , and treat DNSSEC validation as a security enhancement on top of either challenge type rather than as a s
Key topics
- LocalEndpoint / Endpoint Strategy
- LocalEndpoint
- Endpoint Strategy
- Python
- Privacy
- Semantic Systems
- Strategy
- Audit
- Architecture
Research provenance
For citation, use the report title and canonical URL. Archival presence does not establish authorship or promote report statements into portfolio evidence.
Source availability: 35 citation markers in the source export have no recoverable source links. Those markers are omitted from this reader; any supplied bibliography and ordinary links remain. Check the original sources before relying on the cited claims.
This page renders the archived Markdown as safe, formatted HTML. It is background research and does not become a portfolio claim without evidence review.
Full report
On this page
Executive summary
For ErrorNotifier.com, the most robust and practical way to add domain proof-of-ownership is to ship TXT-based verification as the default path, offer CNAME-based verification as a second path, and treat DNSSEC validation as a security enhancement on top of either challenge type rather than as a standalone onboarding method. TXT and CNAME are the dominant operational patterns used by major platforms for domain verification, while DNSSEC’s role is to authenticate DNS data origin and integrity, not to replace the challenge itself. TLSA and DANE are real standards, but they are specialized for binding TLS material to a host and port; they are rarely the best first-run ownership UX for a general SaaS product.
The recommended product design is to create a dedicated verification label such as _errornotifier-challenge.<domain> and to generate a versioned, namespaced token, for example errornotifier-verification=v1:<random-token>. Using a dedicated label avoids collisions with other TXT records at the apex and avoids the hard DNS rule that a CNAME name should not coexist with other data. If you support CNAME verification, use the same dedicated label and point it to a vendor-controlled target such as <token>.verify.errornotifier.com..
Operationally, verification should be asynchronous. After a user starts verification, ErrorNotifier should create a challenge row, show exact DNS instructions in the UI, enqueue a background check, and then poll with exponential backoff and jitter until success or expiry. This is necessary because propagation is governed by DNS TTL, cache behavior, and negative caching; a record that was missing minutes ago may still look absent to the verifier even after the user fixes it. Route 53 says propagation is typically under a minute on its own infrastructure, but Google’s Search Console documentation warns that manual DNS verification can still take two to three days to appear consistently to verifiers.
From a security standpoint, the biggest design choice is how ErrorNotifier trusts DNS answers. Without DNSSEC, TXT or CNAME verification is only as trustworthy as the recursive resolver path you use. DNSSEC adds origin authentication and integrity, but not confidentiality. If ErrorNotifier relies on the AD bit from a validating recursive resolver, that signal should traverse a secure “last mile” such as DoH or DoT, or you should run your own local validating resolver. If you later expose DNSSEC as an enterprise-grade option, do it first in observation mode and only then decide whether any ErrorNotifier features should require validated DNS answers.
The highest-confidence rollout plan is therefore: stage TXT first, then add CNAME, then add DNSSEC-aware auditing, and only later consider DNSSEC-enforced verification for selected features. TLSA/DANE should remain optional and niche unless ErrorNotifier has a concrete product requirement to validate a customer’s TLS endpoint at a specific host and port.
Verification methods and recommended record formats
Major platforms commonly use TXT and CNAME ownership proofs. Google documents TXT and CNAME verification methods, Microsoft 365 gives users a TXT value like MS=msXXXXXXXX, Amazon SES returns a TXT verification token, and AWS Certificate Manager uses CNAME name/value pairs as proof of domain control. That ecosystem precedent matters because it reduces user confusion and registrar-support friction.
DNSSEC is different in nature. The core DNSSEC RFCs define DNSKEY, DS, RRSIG, and authenticated denial of existence; they provide origin authentication and integrity for DNS data, but they do not themselves define a domain-ownership onboarding record for SaaS products. DANE, in turn, uses TLSA records under DNSSEC to associate certificates or public keys with a host and port. That makes DNSSEC and DANE valuable as trust amplifiers or specialized service-verification tools, but usually not as the only ownership-verification UX for a product like ErrorNotifier.
Recommended comparison
| Method | Recommended role in ErrorNotifier | Security assessment | Ease of use | Propagation sensitivity | Implementation complexity |
|---|---|---|---|---|---|
| TXT challenge | Primary method | Medium by default; high if DNSSEC-validated | High | Medium | Low |
| CNAME challenge | Secondary method | Medium by default; high if DNSSEC-validated | Medium-high | Medium | Low-medium |
| DNSSEC-validated TXT/CNAME | Security enhancement | High | Medium-low | Medium | Medium-high |
| TLSA record | Advanced optional signal | High for host+port scope | Low | Medium | High |
| DANE workflow | Niche, product-specific | High for TLS endpoint validation | Low | Medium | High |
That table is a practical engineering assessment. The factual underpinnings are: TXT carries arbitrary descriptive strings; CNAME aliases one name to another and should not share its owner name with other data; DNSSEC adds origin authentication and integrity; TLSA binds keys or certificates to a host and port; and DANE depends on DNSSEC-valid TLSA usage.
Practical record formats
The following formats are the most sensible for ErrorNotifier to support:
| Purpose | Host / Name | Type | Example value | Notes |
|---|---|---|---|---|
| TXT challenge | _errornotifier-challenge.example.com | TXT | "errornotifier-verification=v1:W5m0dQ4qv3aB8bQz2yK8bW7x8w" | Best default. Use a dedicated label. |
| CNAME challenge | _errornotifier-challenge.example.com | CNAME | env-6f3f6d8f.verify.errornotifier.com. | Best alternate path for long-lived proofs. |
| DNSSEC support | parent: example.com / child: example.com | DS + DNSKEY + RRSIG + NSEC/NSEC3 | example.com. IN DS <keytag> <alg> <digest-type> <digest> | ErrorNotifier should validate these if present, not ask users to create them for onboarding. |
| TLSA record | _443._tcp.alerts.example.com | TLSA | 3 1 1 <sha256-of-spki> | Only useful if ErrorNotifier verifies a TLS endpoint. |
| DANE usage | _443._tcp.alerts.example.com | TLSA under DNSSEC | same as above | DANE is the TLSA + DNSSEC operational model. |
The TXT row is grounded in RFC 1035’s TXT format and in the mainstream SaaS verification flows used by Google, Microsoft, and SES. The CNAME row is grounded in RFC 1034’s aliasing rules and ACM’s validation model. TLSA formatting and owner-name construction come from RFC 6698, including the _port._transport.hostname naming pattern for TLSA lookups.
What ErrorNotifier should actually ship
The recommended shipping policy is simple:
- Default: TXT on
_errornotifier-challenge.<domain>. - Optional: CNAME on
_errornotifier-challenge.<domain>for users who prefer alias-based management. - Security upgrade: validate DNSSEC when present and store the result in audit metadata.
- Do not require TLSA/DANE unless ErrorNotifier has a concrete feature that needs to prove control of a TLS endpoint on a specific host and port.
This strikes the best balance among interoperability, user comprehension, implementation cost, and security.
Architecture and product changes
A clean ErrorNotifier design separates the user-facing verification intent from the DNS resolution machinery. The request/response API should start the process, the database should persist the challenge state, and a background verifier should perform the actual DNS checks and retries.
Recommended verification flow
flowchart TD
A[User enters domain in ErrorNotifier UI] --> B[API normalizes domain and creates challenge]
B --> C[System generates versioned token and RR instructions]
C --> D[UI shows Host, Type, Value, TTL guidance, and troubleshooting tips]
D --> E[User publishes DNS record]
B --> F[Background job enqueued]
F --> G[Trusted resolver checks expected RR]
G --> H{Record found?}
H -- No --> I[Store result, apply backoff, retry until expiry]
I --> G
H -- Yes --> J[If DNSSEC available, evaluate validation status]
J --> K{Policy satisfied?}
K -- No --> I
K -- Yes --> L[Mark verified, emit audit event, unlock feature]
Recommended component layout
flowchart LR
UI[Web UI / Admin UI]
API[Verification API]
SVC[Domain Verification Service]
DB[(Primary DB)]
Q[Job Queue]
WRK[Verifier Worker]
RES[Trusted Recursive Resolver or DoH/DoT]
AUD[(Audit Log / Event Store)]
MET[Metrics / Alerts]
UI --> API
API --> SVC
SVC --> DB
SVC --> Q
Q --> WRK
WRK --> RES
WRK --> DB
WRK --> AUD
API --> AUD
API --> MET
WRK --> MET
API endpoints and response model
A good REST shape for ErrorNotifier is:
Starts or reuses an active challenge. Returns domain, normalized domain, method, host/name, type, expected value, recommended TTL, expiry, and current status.
Returns current status, last check time, next check time, attempt count, latest error code, and DNSSEC status.
User-triggered immediate recheck. Rate-limit aggressively.
Cancels a pending challenge.
Optional, provider-specific webhook entrypoint that merely accelerates a recheck; it should never be the sole source of truth.
POST /v1/domain-verificationsGET /v1/domain-verifications/{id}POST /v1/domain-verifications/{id}/checkDELETE /v1/domain-verifications/{id}POST /v1/provider-events/dns-updated
Suggested status values are pending_dns, pending_propagation, verified, verified_dnssec, failed_timeout, failed_misconfigured, and canceled. Suggested HTTP responses are 201/202 for start, 200 for normal readback, 409 for already-verified conflicts, 422 for invalid or unsupported domains, 429 for excessive manual checks, and 503 for resolver or validator outages. This API design is an ErrorNotifier recommendation, while the underlying propagation and recheck concerns are driven by DNS TTL and caching behavior.
UI changes and user messaging
The UI should show exactly four fields for copy/paste:
Example: _errornotifier-challenge
TXT or CNAME
Example: errornotifier-verification=v1:... or env-....verify.errornotifier.com
Recommend 300 seconds if the provider allows it
- Host / Name
- Type
- Value / Target
- TTL
This is important because DNS providers vary in how they interpret the “name” field. Google explicitly tells users that for host/name they may need to leave it blank or use @, Microsoft shows a provider-generated TXT value for manual entry, and AWS ACM documents common problems with leading underscores, trailing dots, and registrars that expect the owner name without the apex repeated. Cloudflare also warns that proxied CNAME behavior changes how answers are returned; for challenge CNAMEs, ErrorNotifier should instruct users to use DNS only, not proxying.
The UI should also show user-friendly propagation language such as: “DNS changes often appear within minutes, but some providers and caches may take up to 48–72 hours.” That message is well-supported by provider documentation and is far better UX than implying instant consistency.
Database schema changes
A robust relational design usually needs three tables.
domains Store the long-lived object:
| Column | Purpose |
|---|---|
id | primary key |
account_id | tenant owner |
display_domain | user-entered domain |
normalized_domain | lower-case punycode FQDN without trailing dot |
verification_status | not_started / pending / verified / revoked |
verified_at | timestamp |
verified_by_user_id | actor who initiated successful flow |
active_verification_method | txt / cname / tlsa |
dnssec_policy | ignore / observe / require |
last_reverified_at | timestamp for periodic checks |
revoked_at | revocation timestamp |
domain_verification_challenges Store each challenge attempt:
| Column | Purpose |
|---|---|
id | primary key |
domain_id | FK |
method | txt / cname / tlsa |
rr_name_fqdn | exact owner name queried |
rr_type | TXT / CNAME / TLSA |
expected_value | encrypted or protected expected RR value |
expected_value_hash | lookup-safe hash |
token_last4 | UX/debug only |
state | pending / verified / failed / canceled / expired |
attempt_count | poll count |
last_checked_at | timestamp |
next_check_at | scheduler cursor |
expires_at | challenge deadline |
verified_at | success timestamp |
resolver_path | which resolver / DoH path made decision |
dnssec_status | unsigned / secure / bogus / unknown |
failure_code | enum like NXDOMAIN, NODATA, MISMATCH, SERVFAIL, TIMEOUT, DNSSEC_BOGUS |
failure_detail_json | structured diagnostics |
domain_verification_events Store an audit trail:
| Column | Purpose |
|---|---|
id | primary key |
challenge_id | FK |
event_type | created / check_started / record_found / verified / expired / canceled |
actor_type | user / system / webhook |
actor_id | nullable |
metadata_json | structured evidence |
occurred_at | timestamp |
For security, do not put raw tokens in general application logs. Store the expected record value in encrypted form if support engineers need to see it, and otherwise rely on a hash plus a masked suffix for diagnostics. That recommendation follows from the fact that the DNS record itself is a bearer-style proof during the challenge lifetime.
Background jobs, rate limits, and error handling
The verifier should run as a queued worker with backoff and jitter. A practical schedule is: immediately, then 2 minutes, 5 minutes, 15 minutes, 30 minutes, 1 hour, 3 hours, 6 hours, 12 hours, and 24 hours until expiry at 72 hours. That expiry aligns well with SES’s documented failure window and comfortably covers Google’s more conservative “up to two or three days” language.
The worker should normalize results carefully:
- For TXT, join multi-string TXT chunks into one logical record before comparing. Both RFC 1035 and Node’s DNS docs make clear that one TXT RR can contain one or more character strings, and Google’s DoH examples show TXT answers as multiple quoted strings in some cases.
- For CNAME, compare the canonical target after lower-casing and trimming the trailing dot.
- Treat NXDOMAIN and NODATA as expected propagation states, not exceptions.
- Treat SERVFAIL, timeouts, and DNSSEC bogus states as retryable unless the failure persists beyond a threshold. RFC 2308 defines negative caching, and later DNSSEC clarifications note that some failures can be cached for minutes.
- Detect obvious misconfiguration: wrong label, wrong token, extra provider-added domain suffix, proxied CNAME, or CNAME on a name that also has other data.
Manual “Check now” should be limited to something like 1 per minute per challenge, 10 per hour per domain, 60 per hour per account. The worker itself should cap concurrent DNS checks per tenant and per resolver path so one noisy account cannot starve the queue.
Logging and audit trail
Every verification flow should emit durable events for:
- challenge created
- instructions viewed or copied
- manual recheck requested
- worker check started
- DNS answer received
- DNSSEC state observed
- verified / expired / canceled / revoked
Audit metadata should include rr_name, rr_type, normalized answer values, TTLs if available, resolver used, RCODE, DNSSEC state, and a redacted expected token suffix. This creates evidence for support, abuse review, and any future compliance needs.
Where implementation differs by stack
Node / Express. In a Node stack, the request layer usually lives in Express routes and middleware, and the DNS resolver path can use Node’s built-in node:dns promises API for TXT, CNAME, and—on newer Node releases—TLSA lookups. Express routing and middleware are the natural place for start/check endpoints; the verification worker should usually run in a separate process or worker pool rather than inside the web process.
Python / Flask / Django. In Flask, verification endpoints typically live in blueprints and view functions. In Django, they fit naturally in function-based or class-based views, with periodic or administrative operations exposed through management commands or your existing task queue. For DNS, dnspython is the most capable choice among common Python options because it includes a stub resolver, DoH support, and DNSSEC helpers.
Ruby / Rails. In Rails, use a controller for the start/check endpoints, Active Record migrations for schema changes, and Active Job for queued verification work. That gives a conventional Rails implementation with clean request-cycle separation and durable background retries.
Go. In Go, the HTTP entrypoints naturally map to net/http handlers. The standard library covers the HTTP server layer and general name resolution, but you should expect to isolate DNS verification logic in a dedicated package or service boundary so the HTTP layer stays thin.
Java / Spring. In Spring, place start/check endpoints in @Controller or @RestController classes and run scheduled or asynchronous rechecks through TaskExecutor, TaskScheduler, or your existing worker infrastructure. Spring’s annotations make it straightforward to expose the HTTP API and separate background scheduling concerns.
Implementation examples
The two code examples below implement the same core pattern:
- generate a versioned challenge token
- show DNS instructions
- resolve TXT or CNAME
- tolerate propagation delays with retry/backoff
- verify DNSSEC when available
- return a state model that ErrorNotifier can persist
The pragmatic DNSSEC check shown here uses a validating resolver over DoH and inspects the AD bit. That is a reasonable operational pattern because RFC 4035 warns stub resolvers not to place full trust in AD unless the path to the validating resolver is appropriately trusted, and RFC 6698 similarly requires a secure “last mile” if the local system is not doing its own DNSSEC validation. Google’s DoH JSON API exposes both AD and the do / cd flags the code uses.
Node.js example
// package.json should run on a recent Node release with global fetch.
// For TXT and CNAME, built-in node:dns is sufficient.
// For DNSSEC status, this example asks a validating resolver over DoH.
import crypto from "node:crypto";
import { Resolver } from "node:dns/promises";
const resolver = new Resolver();
// Prefer your own trusted recursive resolver fleet in production.
// resolver.setServers(["127.0.0.1"]); // Example if you run local Unbound
function normalizeDomain(input) {
return input.trim().toLowerCase().replace(/\.$/, "");
}
function generateChallenge(domain, method = "TXT") {
const normalized = normalizeDomain(domain);
const token = crypto.randomBytes(24).toString("base64url"); // 192 bits
const rrName = `_errornotifier-challenge.${normalized}`;
if (method === "TXT") {
return {
domain: normalized,
method,
rrName,
rrType: "TXT",
rrValue: `errornotifier-verification=v1:${token}`,
token,
expiresAt: Date.now() + 72 * 60 * 60 * 1000,
};
}
if (method === "CNAME") {
return {
domain: normalized,
method,
rrName,
rrType: "CNAME",
rrValue: `${token}.verify.errornotifier.com`,
token,
expiresAt: Date.now() + 72 * 60 * 60 * 1000,
};
}
throw new Error(`Unsupported method: ${method}`);
}
function buildInstructions(challenge) {
return {
domain: challenge.domain,
host: challenge.rrName,
type: challenge.rrType,
value: challenge.rrValue,
ttl: 300,
message:
`Add a ${challenge.rrType} record for ${challenge.rrName} with value ` +
`${challenge.rrValue}. If your DNS provider asks only for Host/Name, use ` +
`_errornotifier-challenge. Use TTL 300 seconds if allowed.`,
};
}
async function resolveExpected(challenge) {
try {
if (challenge.method === "TXT") {
const rows = await resolver.resolveTxt(challenge.rrName);
// Node returns TXT as array-of-arrays; each sub-array is one TXT RR made of chunks.
return rows.map((chunks) => chunks.join(""));
}
if (challenge.method === "CNAME") {
const rows = await resolver.resolveCname(challenge.rrName);
return rows.map((target) => target.toLowerCase().replace(/\.$/, ""));
}
return [];
} catch (err) {
// Common propagation states
if (
["ENODATA", "ENOTFOUND", "ETIMEOUT", "ESERVFAIL", "EREFUSED"].includes(
err.code
)
) {
return [];
}
throw err;
}
}
async function fetchDnssecStatus(name, type) {
const url = new URL("https://dns.google/resolve");
url.searchParams.set("name", `${name}.`);
url.searchParams.set("type", type);
url.searchParams.set("do", "1"); // include DNSSEC records where relevant
url.searchParams.set("cd", "0"); // do NOT disable validation
url.searchParams.set("edns_client_subnet", "0.0.0.0/0"); // privacy
const response = await fetch(url, {
headers: { accept: "application/dns-json" },
});
if (!response.ok) {
throw new Error(`DoH request failed with HTTP ${response.status}`);
}
const body = await response.json();
return {
rcode: body.Status,
ad: Boolean(body.AD), // true => validating resolver says data validated
answer: body.Answer ?? [],
comment: body.Comment ?? null,
};
}
export async function verifyChallenge(challenge, options = {}) {
const deadlineMs = options.deadlineMs ?? 72 * 60 * 60 * 1000;
const baseDelayMs = options.baseDelayMs ?? 30_000;
const maxDelayMs = options.maxDelayMs ?? 60 * 60 * 1000;
const started = Date.now();
let attempt = 0;
let delay = baseDelayMs;
while (Date.now() - started < deadlineMs && Date.now() < challenge.expiresAt) {
attempt += 1;
const observed = await resolveExpected(challenge);
const expected =
challenge.method === "TXT"
? challenge.rrValue
: challenge.rrValue.toLowerCase().replace(/\.$/, "");
const match = observed.includes(expected);
if (match) {
let dnssec = { ad: false, rcode: null, answer: [] };
try {
dnssec = await fetchDnssecStatus(challenge.rrName, challenge.rrType);
} catch {
// DNSSEC status is optional metadata here; verification still succeeds
}
return {
status: dnssec.ad ? "verified_dnssec" : "verified",
verified: true,
dnssecValidated: dnssec.ad,
attempts: attempt,
observed,
dnssec,
};
}
const jitter = Math.floor(Math.random() * 5_000);
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
delay = Math.min(Math.floor(delay * 1.8), maxDelayMs);
}
return {
status: "failed_timeout",
verified: false,
attempts: attempt,
};
}
// Example usage:
/*
const challenge = generateChallenge("example.com", "TXT");
console.log(buildInstructions(challenge));
const result = await verifyChallenge(challenge);
console.log(result);
*/
This example follows Node’s DNS APIs directly. The important implementation detail is that resolveTxt() returns a two-dimensional array because a single TXT RR can contain multiple text chunks; the code joins those chunks before comparing. Node also natively exposes resolveCname() and, on newer releases, resolveTlsa() for future DANE/TLSA support.
Python example
import secrets
import time
from dataclasses import dataclass
from typing import List
import dns.flags
import dns.message
import dns.query
import dns.rcode
import dns.resolver
import dns.exception
resolver = dns.resolver.Resolver()
# Prefer your own trusted recursive resolver fleet in production:
# resolver.nameservers = ["127.0.0.1"]
def normalize_domain(value: str) -> str:
return value.strip().lower().rstrip(".")
@dataclass
class Challenge:
domain: str
method: str
rr_name: str
rr_type: str
rr_value: str
token: str
expires_at: float
def generate_challenge(domain: str, method: str = "TXT") -> Challenge:
normalized = normalize_domain(domain)
token = secrets.token_urlsafe(24) # ~192 bits
rr_name = f"_errornotifier-challenge.{normalized}"
if method == "TXT":
return Challenge(
domain=normalized,
method="TXT",
rr_name=rr_name,
rr_type="TXT",
rr_value=f"errornotifier-verification=v1:{token}",
token=token,
expires_at=time.time() + 72 * 60 * 60,
)
if method == "CNAME":
return Challenge(
domain=normalized,
method="CNAME",
rr_name=rr_name,
rr_type="CNAME",
rr_value=f"{token}.verify.errornotifier.com",
token=token,
expires_at=time.time() + 72 * 60 * 60,
)
raise ValueError(f"Unsupported method: {method}")
def build_instructions(challenge: Challenge) -> dict:
return {
"domain": challenge.domain,
"host": challenge.rr_name,
"type": challenge.rr_type,
"value": challenge.rr_value,
"ttl": 300,
"message": (
f"Add a {challenge.rr_type} record for {challenge.rr_name} "
f"with value {challenge.rr_value}. If your provider asks for only "
f"Host/Name, enter _errornotifier-challenge. Use TTL 300 if possible."
),
}
def resolve_expected(challenge: Challenge) -> List[str]:
try:
if challenge.method == "TXT":
answer = resolver.resolve(challenge.rr_name, "TXT", lifetime=3)
normalized = []
for rr in answer:
# dnspython TXT records are lists of byte strings
if hasattr(rr, "strings"):
normalized.append("".join(part.decode("utf-8") for part in rr.strings))
else:
normalized.append(rr.to_text().strip('"'))
return normalized
if challenge.method == "CNAME":
answer = resolver.resolve(challenge.rr_name, "CNAME", lifetime=3)
return [rr.target.to_text().rstrip(".").lower() for rr in answer]
return []
except (
dns.resolver.NXDOMAIN,
dns.resolver.NoAnswer,
dns.resolver.NoNameservers,
dns.exception.Timeout,
):
return []
def dnssec_status(name: str, rdtype: str) -> dict:
# Pragmatic DNSSEC status check via DoH to a validating resolver.
# For stronger control, use your own validating resolver over DoT/DoH
# or perform full local validation with trust anchors.
query = dns.message.make_query(f"{name}.", rdtype, want_dnssec=True)
response = dns.query.https(
query,
"https://dns.google/dns-query",
post=True,
verify=True,
)
return {
"rcode": dns.rcode.to_text(response.rcode()),
"ad": bool(response.flags & dns.flags.AD),
}
def verify_challenge(challenge: Challenge,
deadline_seconds: int = 72 * 60 * 60,
base_delay_seconds: int = 30,
max_delay_seconds: int = 60 * 60) -> dict:
start = time.time()
attempt = 0
delay = base_delay_seconds
while time.time() - start < deadline_seconds and time.time() < challenge.expires_at:
attempt += 1
observed = resolve_expected(challenge)
expected = (
challenge.rr_value
if challenge.method == "TXT"
else challenge.rr_value.rstrip(".").lower()
)
if expected in observed:
dnssec = {"rcode": "UNKNOWN", "ad": False}
try:
dnssec = dnssec_status(challenge.rr_name, challenge.rr_type)
except Exception:
pass
return {
"status": "verified_dnssec" if dnssec["ad"] else "verified",
"verified": True,
"dnssec_validated": dnssec["ad"],
"attempts": attempt,
"observed": observed,
"dnssec": dnssec,
}
time.sleep(delay + min(5, attempt)) # small jitter
delay = min(int(delay * 1.8), max_delay_seconds)
return {
"status": "failed_timeout",
"verified": False,
"attempts": attempt,
}
# Example:
# challenge = generate_challenge("example.com", "TXT")
# print(build_instructions(challenge))
# print(verify_challenge(challenge))
This Python example uses dnspython’s resolver for normal DNS queries, dns.message.make_query(..., want_dnssec=True) to request DNSSEC-related data, and dns.query.https() for DoH. dnspython’s documentation explicitly covers the stub resolver, DoH support, message creation, and DNSSEC helpers.
Optional TLSA and DANE support
If ErrorNotifier ever needs to verify that a customer controls a TLS endpoint rather than merely a DNS zone, you can add an advanced flow that checks _443._tcp.<host> (or another port/transport) for TLSA records and, if policy requires, verifies that the presented certificate or SPKI matches the TLSA association. RFC 6698 defines the TLSA owner-name syntax, wire format, presentation format, and the certificate-usage / selector / matching-type fields. This should be treated as a specialized feature for advanced customers, not the default onboarding path.
Security, testing, and observability
Security considerations
The core security rule is that ownership verification is only as good as the verifier’s trust in DNS answers. RFC 5452 explains how forged DNS answers can poison a resolver, and RFC 4033 makes clear that DNSSEC adds origin authentication and integrity but not confidentiality. For ErrorNotifier, the safest operational pattern is to verify through a trusted validating resolver path—ideally one you control—and to record whether the answer validated under DNSSEC.
Do not equate “record found” with “record secure.” Your data model should distinguish at least three states for DNSSEC metadata: unsigned / secure / bogus. RFC 4035 describes authenticated denial of existence and the AD bit, while RFC 6840 clarifies how AD, DO, and CD behave in practice. If ErrorNotifier relies on a validating resolver’s AD bit instead of local cryptographic validation, it should do so only over a secure transport such as DoH or DoT.
Use short TTL guidance—for example 300 seconds—where customers control the zone, but never promise immediate verification. TTL controls how long records may be cached, Cloudflare explicitly documents the tradeoff between longer TTL and update speed, and negative responses can also be cached under RFC 2308. In practice this means a user who adds the correct record can still see a temporary NXDOMAIN or NODATA response from some resolvers until the negative cache expires.
When comparing TXT values, normalize by joining chunks, lower-casing only where appropriate, and preserving semantic exactness of the token string. When comparing domains or CNAME targets, normalize case and trailing dot. When logging, redact or hash the token. When revoking or expiring a challenge, invalidate any pending queue work by version or generation counter so stale workers cannot mark a challenge verified after cancellation.
Finally, design against registrar and provider quirks. Common support cases include:
- entering the full owner name when a provider expects only the relative host value
- including a trailing period where the provider already appends one
- using a proxied CNAME rather than a DNS-only CNAME
- publishing a CNAME on a name that already has other data
- failing to account for leading underscores in challenge labels
Verification flows beyond the first success
ErrorNotifier should support three verification modes:
One-time verification. Best for simple onboarding when ownership proof is only needed to unlock the domain once. This is straightforward but weak if the domain later changes hands without notice.
Periodic re-check. Best when the verified domain grants ongoing privileged behavior, such as custom domains, certificate renewal, or send-as-domain permissions. Search Console states that verification lasts only while the token remains present and that the service periodically checks for its continued validity. AWS ACM similarly relies on the continued presence of the validation CNAME for automatic renewal.
Webhook-triggered re-check. Useful only as an accelerator. DNS has no universal push webhook for public-zone changes, so any such trigger will be provider-specific. If ErrorNotifier integrates with a DNS provider API or receives a zone-updated event from internal infrastructure, it should enqueue an immediate re-check but still verify through DNS resolution before changing state.
Test plan and automated tests
A serious feature like this needs three layers of test coverage.
Unit tests
Unit-test deterministic logic:
- token generation length, version prefix, and uniqueness
- domain normalization, punycode handling, and trailing-dot handling
- TXT normalization across one or multiple chunks
- CNAME normalization
- failure-code mapping from resolver exceptions
- backoff schedule and expiry handling
- secure comparison and masking/redaction of tokens
Integration tests
Run integration tests against realistic DNS behavior:
- a local authoritative DNS server for TXT and CNAME success paths
- negative caching behavior for missing records, then record creation
- CNAME loops and excessive chains
- malformed values, wrong tokens, wrong labels, and conflicting records
- DNSSEC-signed zones returning secure answers
- DNSSEC-bogus zones returning validation failures
The highest-value integration environment is a disposable DNS lab using BIND, Knot, NSD, CoreDNS, Unbound, or a managed test zone in a real provider account. The goal is to test actual recursive behavior, not just mocked answers.
End-to-end tests
E2E tests should automate the user flow:
- create domain verification via API
- capture instructions
- publish record through test-zone API
- poll until verified
- observe UI state transition and audit event emission
- revoke or remove the record and confirm recheck behavior if periodic verification is enabled
Provider compatibility tests
At minimum, validate against Cloudflare, Route 53, GoDaddy, and one “simple registrar UI” provider, because provider UX differences are a major source of support cases. AWS and Cloudflare documentation already illustrate why this matters.
Monitoring and alerting
Expose at least these metrics:
domain_verification_started_totaldomain_verification_verified_totaldomain_verification_timeout_totaldomain_verification_check_attempts_totaldomain_verification_duration_secondsdomain_verification_dnssec_secure_totaldomain_verification_failures_total{code=...}domain_verification_manual_check_rate_limited_totaldomain_verification_queue_backlogresolver_errors_total{resolver=...,rcode=...}
Alert on:
- sudden drop in success rate
- spike in
SERVFAIL, timeout, or bogus-DNSSEC rates - queue backlog growth
- p95 time-to-verify exceeding a threshold
- manual-check 429 spikes, which often indicate a resolver outage or UX confusion
- discrepancy between “record found” and “mark verified” counts, which usually indicates a persistence or state-machine bug
Your dashboard should break down verification time by method and by DNSSEC state. If you capture anonymized provider or nameserver-family hints, a provider-level heatmap can be extremely helpful during incidents.
Migration, rollout, and limitations
Migration plan
Use a two-phase migration.
Schema phase
Deploy the new tables and nullable columns first:
domainsverification columnsdomain_verification_challengesdomain_verification_events
Do not switch any user-visible behavior yet. This keeps the first migration easy to roll back.
Application phase
Deploy the API and worker behind feature flags:
dns_verification_v1dns_verification_txt_enableddns_verification_cname_enableddns_verification_dnssec_observe_onlydns_verification_periodic_recheck
If ErrorNotifier already has customer domains in production, backfill them as legacy_verified or manual_verified first rather than forcing immediate re-verification. Then prompt for re-verification on risky actions such as editing the domain, changing account ownership, or enabling features that materially depend on ongoing control.
Staged rollout strategy
A staged rollout with feature flags is the safest path.
Internal stage Verify only ErrorNotifier-owned domains and test zones. Exercise TXT first, then CNAME, then DNSSEC observation.
Canary stage Enable for a small set of trusted tenants. Capture support tickets and provider-specific issues.
Broad opt-in stage Expose the feature to all tenants but only on new domains. Keep existing domains grandfathered.
General availability Make TXT the default. Add CNAME as an alternate method in the UI. Continue storing DNSSEC results in observation mode.
Hardening stage Only after several weeks of stable data should you decide whether any ErrorNotifier feature warrants dnssec_policy=require. RFC 9364 calls DNSSEC the best current practice for origin authentication, but it does not make DNSSEC universal in deployment; operationally, that means observation mode should come before enforcement.
Open questions and limitations
The design above is high-confidence, but a few product-specific choices still depend on how ErrorNotifier uses verified domains:
- If verification unlocks only a one-time permission, TXT-only may be enough.
- If verification authorizes long-lived capabilities such as custom domains or certificate renewal, periodic re-checks are strongly advisable.
- If ErrorNotifier expects enterprise-grade proof, you should decide whether DNSSEC is merely recorded or eventually required.
- If you want webhook-triggered rechecks, you must decide which DNS providers are worth integrating, because there is no universal public-DNS webhook standard.
- If you plan to support TLSA/DANE, clarify whether the actual business need is “prove zone ownership” or “prove control of a TLS service endpoint,” because those are different problems.
The strongest overall recommendation remains unchanged: ship TXT first, support CNAME second, validate DNSSEC when available, and treat TLSA/DANE as advanced optional capabilities rather than the default proof-of-ownership flow for ErrorNotifier.com.