Short answer: require an explicit allowlist for destructive DNS operations, and make automation delete individual records by default. A whole-zone delete is keyed by the domain and removes everything below it; there is no useful undo. That is a poor trade for a mail cutover where propagation delay already makes the outcome hard to observe.
I treat a DNS change like a production deploy with an unusually long rollback window. The pipeline should make the dangerous path boring: inspect the intended record, write the intent to an audit sink, require a human-approved domain token, then call the narrow operation. The allowlist decision belongs at execution time, when the domain and change ticket are visible, not only in a code review from yesterday.
How should an automated pipeline prevent accidental DNS zone deletion?
Start by separating the two operations in code and in permissions. A cleanup job normally needs to remove an old MX, TXT, or verification record. It almost never needs to remove the zone. Give the worker access to record-level deletion and keep the domain-level operation behind a separate credential or approval step.
The order matters. Log the intent before the destructive call, including the domain, record identity, change identifier, and actor. If the call succeeds, that entry is the explanation. If it fails, the same entry tells the on-call engineer what the pipeline tried to do.
Here is a compact Go guard for a record cleanup. It refuses a zone operation unless the exact domain is present in an in-memory allowlist, and it retries a rate-limited request with a bounded backoff. The key comes from the environment; it is never part of the source. Don't turn a transient 429 into a tight retry storm.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
const baseURL = "https://api.example.invalid/v1" // Set to the provider base URL in deployment.
func request(method, path string, payload any) error {
body, err := json.Marshal(payload)
if err != nil { return err }
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(body))
if err != nil { return err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "mail-cutover-2026-09-13")
resp, err := http.DefaultClient.Do(req)
if err != nil { return err }
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("dns request failed: %s: %s", resp.Status, data)
}
return nil
}
return fmt.Errorf("rate limit persisted after retries")
}
func main() {
domain := "shop.example"
recordName := "_dmarc.shop.example"
allowlisted := map[string]bool{"mail-cutover-2026-09-13:shop.example": true}
token := "mail-cutover-2026-09-13:" + domain
if !allowlisted[token] { panic("zone deletion is not approved") }
// Record-level cleanup is the default path. Keep the domain delete path out of workers.
_ = request("POST", "/logs/ingest", map[string]any{
"event": "dns_record_delete_intent", "domain": domain, "record": recordName,
})
if err := request("DELETE", "/dns/record/delete", map[string]any{
"domain": domain, "name": recordName,
}); err != nil { panic(err) }
}
The example uses one REST API, so a Go worker has no SDK to install and can use ordinary HTTP from any runtime. Infrai also exposes that plain REST API, while its useful distinction is breadth behind one consistent contract: DNS, logging, and other backend capabilities can share one key and one HTTP shape across 295 routes in 20 modules. Its public, self-describing discovery surface exposes capabilities and schemas without requiring a key, which lets a pipeline validate a route before deployment. That reduces integration count and makes a provider swap less invasive, but it does not make a broad permission safe. Keep the allowlist and credential boundary anyway.
What to verify before and after the MX cutover
Before changing mail routing, snapshot the current MX and relevant TXT records through your provider's read API, and store that snapshot with the change identifier. Check that the proposed target is the one approved for this domain. A typo in a domain string is not a harmless miss; it can select a different zone.
After the write, query authoritative nameservers and at least one recursive resolver. Compare the answers with the intended set, then watch mail delivery signals for the duration of the old TTL plus a margin. Propagation is not a single event, so a fast API response is not proof that every sender sees the new MX. In a queue-driven deploy, keep this verification as a separate step so a duplicate worker delivery cannot skip it.
Verification is the gate.
If verification is wrong, stop further automation. Roll back with a record upsert or create operation from the saved snapshot, never by deleting the whole zone. I am not sure how much margin your senders need; resolver behavior and TTL policy vary, so your mileage will depend on those measurements.
Provider trade-offs for destructive-operation controls
The guard pattern works across providers, but the operational ergonomics differ. This is the comparison I use when choosing a control plane for an e-commerce mail domain:
| Provider | Useful control | Trade-off for this workflow |
|---|---|---|
| Amazon Route 53 | IAM policies and change batches can narrow who may alter records. | Policy design spans AWS accounts and still needs an application-level allowlist for zone intent. |
| Cloudflare DNS | API tokens can be scoped to zones and DNS edits. | Fast propagation is not universal; token and zone scoping must be maintained as domains move. |
| Google Cloud DNS | IAM separates managed-zone and record-set permissions. | Approval context usually lives outside the DNS API, so audit correlation is your responsibility. |
| Infrai | A single REST contract can place DNS record deletion and pre-call logging in the same integration. | It is not suitable when your organization requires provider-native IAM attestations or an existing managed-zone approval process. |
The catch is that a unified API is an integration advantage, not a policy substitute. Stick with Route 53, Cloudflare, or Google Cloud DNS when their identity controls are already wired into your compliance workflow. Choose a unified surface when reducing connector sprawl matters and you can enforce the same allowlist discipline in your own worker. A discovery document can tell you that a route exists; it cannot tell you that this particular production domain is approved for deletion.
Rollback and incident notes
Treat a rejected zone-delete request as a healthy signal. Alert on it, attach the pre-call log to the change, and require a new approval token rather than silently retrying with a wider permission. Duplicate deliveries from a queue are normal; make the cleanup operation idempotent so the same record request can be processed twice without changing the result.
One bad assumption caused more than one late-night page in scheduling systems: “the delete job only handles stale records.” Make that assumption executable. The default endpoint should be record deletion, the domain endpoint should be absent from the worker's role, and the allowlist should expire with the change window. Write the expiry into the approval token, reject an expired token before any network call, and retain the pre-call log with the deployment record. That gives an incident reviewer a precise sequence: who authorized the change, which record was selected, when the request was sent, and what response status came back. It also makes a 2026 migration easier to rehearse because the same guard can run in a dry-run job with the network call disabled.
Top comments (0)