DEV Community

DaltonReed1289
DaltonReed1289

Posted on

Retiring a Mail Domain from a Shared DNS Zone — 4 Records You Delete Last

Use the zone's ownership boundary as the deletion rule. If the customer owns the zone, delete only the record values you published and never the zone object itself; if your platform created a delegated subzone for that one tenant and nothing else lives inside it, remove the whole zone in a single API call and retire the parent delegation with it. Most of what goes wrong in domain offboarding comes from confusing those two cases — a shared zone edited by name and type, one delete request that takes SPF, a site verification token and somebody's SSO proof down together.

I work on a freight platform that sends shipment-status mail on behalf of brokers, so every customer domain carries records we published: an SPF include, two DKIM selectors, a CNAME for the bounce and tracking host, and a DMARC policy whose rua address points at our report pipeline. When a broker leaves, those records have to go. The zone usually does not.

Granularity is the whole argument.

A TXT query at the apex of acme-freight.example returns every TXT value published at that name — your SPF include sits in the same record set as the customer's payroll provider verification string and two cloud tenancy proofs. The DNS API you are calling almost certainly deletes by name plus type. Delete TXT acme-freight.example and you have removed four unrelated pieces of infrastructure evidence while intending to remove one string, and nobody notices until an unrelated SSO login fails the next morning.

Should you delete the records or remove the whole zone for an offboarded domain?

Answer the ownership question first, in code, not from memory. Two shapes show up in practice.

The customer-owned apex zone is the common one. You hold delegated write access, or an API token scoped to a record set, and the zone contains records from marketing, from the CRM, from whoever set up the VPN. Here the only defensible operation is a value-level edit: read the current record set, remove your value, write the remainder back. The zone survives, the other tenants of that zone never learn an offboarding happened, and the risk of collateral deletion drops to whatever your read-modify-write does under concurrent edits — which is why a conditional update guarded by an ETag or a record identifier matters more than it looks.

The platform-owned subzone is the other shape: the broker delegated mail.acme-freight.example to your nameservers with an NS record set, and everything under that name was created by your automation. Nothing in it belongs to anyone else. That zone can be removed whole, because removing it is exactly as destructive as it appears.

The test I apply is narrow. If a record in the zone was created by a party who cannot be notified by your offboarding workflow, you do not get to delete the zone. There is no third case where guessing is acceptable.

Failure modes that outlive the delete call

The dangling CNAME is the one with a security consequence. Your automation retires the platform-side target t-8842.mail-edge.example while the customer's mail.acme-freight.example CNAME still points at it; if that target name is ever reclaimable, whoever claims it inherits a hostname that the broker's customers still trust for shipment links. Delete in the other order: customer-facing pointer first, platform-side target after, and keep the target name reserved rather than released.

Removing a zone is not the same as removing the names inside it. Under RFC 8020, an NXDOMAIN answer for a name asserts that nothing exists beneath it, and resolvers are entitled to cache that assertion and synthesize NXDOMAIN for every child until it expires. Negative caching is bounded by the SOA MINIMUM field, and RFC 2308 recommends a maximum of three hours. Drop the zone before you have drained the delegation and you own a three-hour window in which a name you meant to keep is provably absent.

Leftovers cost you differently. An SPF include that still resolves keeps consuming one of the ten DNS-querying mechanisms RFC 7208 allows, and a domain that ends up with two TXT records both starting v=spf1 returns permerror — which most receivers treat as a fail, on a domain you no longer operate but are still named in. An MX left pointing at your platform means you keep accepting mail for a tenant who has no account; RFC 7505 gives you the honest answer there, a null MX of MX 0 ., published for one TTL cycle before the record set goes away.

Three exit paths, compared by blast radius and cache lifetime

Exit path Who owns the zone Blast radius if wrong Drains in Main limitation
Delete your record values Customer One record set, recoverable from your own state Record TTL Needs value-level API and a read-modify-write guard
Remove the whole zone Your platform Every name under the zone, plus cached NXDOMAIN Record TTL, then negative TTL Only safe for single-tenant delegated subzones
Hand the zone back Transfers to customer Nothing deleted, ownership ambiguity instead Immediate Your automation must stop writing, or it recreates records

The zone-level delete is the option I reject as a default, and it is worth saying why rather than pretending it has no place. It is one call, it is atomic, it leaves nothing behind, and for a delegated subzone that your automation created from a template it is the correct choice — the audit trail is cleaner than a dozen record deletions. It stops being correct the moment a human has ever had a console open on that zone.

Providers disagree about how much they will protect you here. Route 53 refuses to delete a hosted zone that still contains anything other than its own NS and SOA sets, so the destructive call fails until you have emptied the zone deliberately. Others delete the zone and its contents in one request. external-dns takes a different approach to the same problem: it writes a TXT registry record alongside every record it manages, so an automated removal only touches names it can prove it created. Declarative tools such as octodns and DNSControl turn a tenant removal into a diff against a checked-in file, which is auditable and reviewable — the catch is that the file has to be the truth, and in a customer-owned zone it usually isn't.

The last four deletions, in the order the caches allow

Lower TTLs first. A week before the contract ends, push the records you control to 300 seconds; the offboarding then drains in five minutes instead of a day, and the cost of a mistake drops with it.

Audit before you touch anything, and keep the output. This is the single cheapest artifact in the whole process:

curl -sS --fail-with-body \
  -H "authorization: Bearer $DNS_TOKEN" \
  "https://dns.internal.example/zones/$ZONE_ID/records?name=acme-freight.example&type=TXT" \
  -o audit/acme-freight-txt-before.json
Enter fullscreen mode Exit fullscreen mode

Then delete by record identifier, never by name and type. The identifier is what makes the operation value-level, and --fail-with-body is what stops a 409 from being logged as a success:

curl -sS --fail-with-body -X DELETE \
  -H "authorization: Bearer $DNS_TOKEN" \
  -H "if-match: $RECORD_ETAG" \
  "https://dns.internal.example/zones/$ZONE_ID/records/$RECORD_ID"
Enter fullscreen mode Exit fullscreen mode

The order of the four DNS deletions is the part I would defend in review. Revoke the DKIM selector first by republishing it with an empty p= value, which RFC 6376 defines as revocation, and delete the selector record one TTL later — in-flight mail then fails verification for a stated reason instead of an absent one. Remove your value from the apex SPF record second, as an edit to the record set, leaving every other value untouched. Third, delete the customer-facing mail CNAME, before the platform-side target it points at. The _dmarc TXT goes last, because until sending has actually stopped it is the only record still asserting a policy for a name you have just stripped of authentication.

Verify from outside your own infrastructure. A resolver you don't operate is the only witness that matters:

curl -sS -H "accept: application/dns-json" \
  "https://doh.example.net/dns-query?name=_dmarc.acme-freight.example&type=TXT"
Enter fullscreen mode Exit fullscreen mode

Aggregate reports keep arriving after the domain is gone

This is where I count bytes. DMARC aggregate reports are generated per reporting receiver per domain per interval, and RFC 7489 sets the default ri at 86400 seconds. Run six hundred customer domains with forty receivers that report regularly and the arithmetic gives roughly twenty-four thousand XML documents a day arriving at one endpoint — before a single offboarding. Every domain you retire while leaving rua pointed at your pipeline keeps contributing to that number, and keeps its label alive in whatever time series you built on top of the reports.

Two decisions follow. Remove the rua address as part of the DMARC deletion rather than after it, so ingest stops at the same moment authority does. And decide the retention line separately from the ingest line: raw report XML is dispute evidence and is worth thirty days, the daily rollup per domain and disposition is worth a year, and the per-source-IP breakdown for a domain that no longer sends is worth nothing at all. I'm not certain thirty days is the right number for every contract — it is the number our disputes have actually needed, and a legal review might move it.

What you should keep forever is the audit artifact: the record set as it stood before the edit, the identifiers you deleted, the operator, the timestamp. It is a few kilobytes per offboarding. It is also the only thing that answers the question that eventually gets asked, which is not whether the records were deleted but whether anything else was.

References

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.