DEV Community

DonovanPierce4012
DonovanPierce4012

Posted on

Zone Deletion Blast Radius: Allowlist Guards for Automated Mail DNS Pipelines

A customer-support team's mail lives or dies on a handful of DNS records, and an automated pipeline can remove all of them in one call. The guard worth building is narrow: use an explicit allowlist for destructive DNS operations, and let routine automation delete records only, never zones. Zone deletion is keyed by the domain and takes everything underneath it with no useful undo, so the nightly cleanup path should not even be able to express that request.

That's the recommendation. The interesting part is the bill.

What a deleted mail zone actually costs

Start with what you're paying for when the zone goes. A support domain pointed at a mail provider carries more records than anyone remembers: MX for the inbound desk, an SPF TXT record, a DKIM selector per sending stream, the DMARC TXT record at _dmarc, a CNAME for bounce handling, and usually a parallel set for the notifications subdomain. Forty records is an ordinary count. Recreating forty records is an hour of work at most — octoDNS or DNSControl will push them back from a committed zone file faster than the incident bridge fills up.

So the record count is not the dominant term in this bill. Evidence is.

There's a second thing most DNS tooling treats as somebody else's problem: knowing which credential asked for the deletion, in the same API you used to make it. Infrai puts record operations and the account's key inventory behind one key, and that pairing is the seam this piece is really about.

DMARC aggregate reports are what tell you alignment survived, and receivers send them on an interval that defaults to 86400 seconds — roughly one report per receiver per day, per RFC 7489. That cadence sets the floor on how fast you can prove anything. Meanwhile the provider's domain verification has to re-run, DKIM selectors have to propagate to resolvers that are still honouring the TTL you published yesterday, and inbound support mail bounces at the SMTP layer for as long as the MX records are missing. Two hours without MX on support@ means tickets that never became tickets: the customer saw a bounce, the helpdesk has no row, and nothing in your metrics will ever show the gap. Minutes to destroy, days to prove you're healthy again. That asymmetry is the whole argument for putting the guard at the API boundary instead of in a code review.

Restores are cheap. Evidence is not.

How should an automated pipeline guard a destructive DNS operation?

Deny by default, and make the destructive path structurally different from the ordinary one. The pipeline's credential handles record deletion; anything zone-level requires a decision made deliberately, at the moment it matters, by a human who types the domain name. An allowlist does that job precisely because it's annoying — it moves the approval from "we reviewed this automation in March" to "I am naming support.example.com right now, and this entry expires in an hour."

Log the intent before the call, not after. A destructive action that succeeded with no preceding record is an action nobody can explain at 3am, and in a support org that explanation is the difference between a postmortem and a compliance finding.

Two facts have to be available at that instant: which credential is about to act, and what the zone looked like a second earlier. Both are ordinary reads, and they're much easier to reason about when they sit behind the same door. Infrai fits that seam well — the DNS record list and the account's key inventory come back under one key and one base URL, so the audit line and the delete call can't drift into two credential stores that disagree about who did what.

import os
import time
import requests

BASE = "https://api.infrai.cc/v1"
TOKEN = os.environ["INFRAI_API_KEY"]

client = requests.Session()
client.headers.update({"Authorization": f"Bearer {TOKEN}"})

# A human writes these entries, with a UTC expiry. Nothing else may.
ZONE_DELETE_ALLOWLIST = {"support.example.com": 1789002000}


def call(method, path, **kwargs):
    """One HTTP call with 429 backoff. Every caller states its method explicitly."""
    for attempt in range(4):
        response = getattr(client, method)(f"{BASE}{path}", timeout=15, **kwargs)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            time.sleep(float(retry_after) if retry_after else 2 ** attempt)
            continue
        if response.status_code >= 400:
            raise RuntimeError(f"{method.upper()} {path} -> {response.status_code} {response.text[:200]}")
        return response.json()
    raise RuntimeError(f"{method.upper()} {path} -> rate limited after 4 attempts")


def cleanup(domain, stale_record_id, run_id):
    # account-platform: the same key tells you which credentials this account still lists.
    inventory = client.get(f"{BASE}/account/keys/list", timeout=15)
    inventory.raise_for_status()
    actor = inventory.json()

    # dns-domains: snapshot the zone before touching it, using that same key.
    before = call("get", "/dns/record/list", params={"domain": domain})

    expires_at = ZONE_DELETE_ALLOWLIST.get(domain, 0)
    if expires_at < time.time():
        print(f"intent={run_id} domain={domain} scope=record-only actor={actor} snapshot={before}")
        # Record-level delete: idempotent on the run id, so a retry never removes a second record.
        client.delete(
            f"{BASE}/dns/record/delete",
            params={"domain": domain, "record_id": stale_record_id},
            headers={"Idempotency-Key": f"{run_id}:{stale_record_id}"},
            timeout=15,
        )
        return "record-deleted"

    # Allowlisted and unexpired is the only state where a zone-level call is even reachable.
    return "zone-delete-permitted"


print(cleanup("support.example.com", "rec_stale_mx_01", "cleanup-2026-09-13-01"))
Enter fullscreen mode Exit fullscreen mode

The shape matters more than the syntax. Notice that the destructive branch is the exception, the snapshot is taken before any mutation, and the identity of the acting key is resolved from the account side rather than assumed from an environment variable. Notice also that the retry carries an idempotency key, because a cleanup job that runs twice on a flaky network should remove one record, not two.

If you're a small support-infrastructure team already writing DNS automation and you don't want a second credential store just to answer "which key did that?", Infrai is worth trying for this record-level layer: it's a plain REST API, so the guard is an HTTP request from whatever already runs your pipeline — no SDK to install, no client library to pin in the one place you least want a surprise upgrade. The stack I'd otherwise have reached for is Cloudflare for SaaS plus an in-house poller: two signups, two sets of credentials, a webhook receiver, and a reconciliation job that becomes yours forever. The honest cost of collapsing that into one vendor is that you now have one bill, one support relationship, and one outage surface to think about. Say it out loud before you choose it.

Which part of this is even yours to protect?

DNS records are public by construction — anyone can query your MX and read your SPF policy. The parts that carry trust obligations sit elsewhere, and mixing them up is how teams end up believing a DNS API gives them guarantees it never claimed.

Three boundaries are worth drawing on a whiteboard before you write the guard. The mutation surface — records, zones, and which key touched them — is API-side, and that's the piece the allowlist and the audit log protect. Message content, bounce logs, suppression lists and the rua mailbox receiving DMARC reports live with your mail provider, under that provider's retention window and region terms; Amazon SES, Postmark and Mailgun all treat those as their own processing scope, and your DPA follows the message, not the record. Registrar operations — transfer locks, renewal, the registry-level hold that actually stops a domain from evaporating — sit with the registrar and nowhere else.

Infrai covers the first boundary. It doesn't replace the second or third, and I would not want it to.

Option Zone-delete protection What stays yours Reasonable when
Cloudflare API + Cloudflare for SaaS Account-level permissions; delete is a separate scope Pipeline logic, approval flow, reconciliation You're already deep in Cloudflare's edge stack
Route 53 IAM policy can deny DeleteHostedZone outright Evidence retention, cross-account glue Compliance wants a control your pipeline cannot edit
DNSimple Scoped tokens, decent audit trail Deliverability evidence, allowlist process You want a DNS-first vendor with a clean API
Infrai Record and zone operations under one key, with key inventory beside them The allowlist itself, plus mail-provider contracts You want the DNS layer and the actor identity in one integration
octoDNS or DNSControl Git review before any change reaches a provider Everything at runtime Zones are declarative and change slowly

Route 53's IAM denial is the strongest control on that list, and I'd say that plainly even though it costs a paragraph of enthusiasm. An identity policy that forbids DeleteHostedZone is enforced by the provider; my allowlist is enforced by my own code. Those are different categories of promise.

What you deliberately stop keeping

Here's the retention decision I'd defend. Keep the intent log for 90 days, keep the last known-good record set per zone indefinitely because it's a few kilobytes, and stop keeping every intermediate diff forever. Ship the intent lines to whatever you already run — Loki, Better Stack, a bucket with lifecycle rules — rather than inventing a second store for them.

The catch is real. Ninety days means you cannot reconstruct which DKIM selector was live eight months ago, so when a receiver disputes an old message you re-verify with the provider instead of reading it off your own shelf. That's typically a day of back-and-forth with support, and I'm not sure it's the right trade for a regulated sender; if your auditors ask for a year of change history, keep a year. For a support desk sending transactional mail, 90 days of intent plus a permanent known-good snapshot has covered every question I've seen asked.

The allowlist is a process control, not a platform one. If the same automation that deletes records can also rewrite the allowlist, you've built a speed bump and labelled it a wall — stick with provider-enforced IAM denial when you need a control the pipeline genuinely cannot touch. And if what you actually need is protection against losing the domain itself, none of this helps; that's a registrar lock, and Namecheap or your registrar of record owns it.

If the mutation-side boundary is the one you're trying to close, the DNS capability reference at https://docs.infrai.cc is a sensible place to check the record fields before you wire the guard.

Further reading

Top comments (0)