DEV Community

callumreed2198
callumreed2198

Posted on

Domain Retirement Guard: Log Intent Before a Destructive Automation Operation

An e-commerce migration changes the answer when one automation account can see both customer-owned and platform-owned zones. Short answer: permit deletion only for an exact customer-zone allowlist, require a separate approval flag, persist the operator's intent before the call, and deduplicate retries with an operation ID. Platform-owned zones should never enter that deletion path.

I've been paged for missed cron jobs and duplicate queue deliveries. The lasting lesson wasn't “retry less.” It was that retries are normal, while an ambiguous destructive command is an incident waiting for a second delivery. DNS removal deserves the same treatment: the worker must be able to prove what it intends to remove before it touches the provider.

No guesswork.

How should DNS automation guard a destructive operation with explicit intent?

Treat deletion as a small state machine, not as a thin wrapper around a registrar or DNS provider call. A request starts unapproved. An operator reviews a plan whose scope is an exact set of zone names, supplies an explicit approval, and creates a unique operation ID. The executor then checks ownership, membership in that set, the approval, and prior completion. Only after every check passes does it write an intent record and call the deletion adapter.

The customer-owned versus platform-owned distinction has to be data, not a naming convention. shop.example cannot safely imply customer ownership just because it resembles other storefront domains. During an e-commerce migration, the platform may also hold checkout, email, tracking, or shared service zones. A suffix match such as HasSuffix(zone, "example") is too broad, and a substring check is worse. Normalize the trailing dot and case, then compare the full zone name against the reviewed set.

The approval flag is deliberately redundant with the allowlist. They answer different questions. The allowlist says, “Is this object in the reviewed change scope?” The flag says, “Did the operator authorize execution now?” Intent logging answers a third: “What did the system decide, under which operation ID, before the side effect?” Removing any one of those checks weakens the incident record.

The failure mode is confused ownership, not a bad delete call

Picture a migration batch containing brand-store.example, a customer-owned storefront zone, beside checkout.platform.example, a platform-owned zone used by many shops. The first belongs in the customer's registrar exit plan. The second does not. If both arrive on the same queue and the consumer only checks for --delete, the command-line flag has become the entire safety model.

I initially treated duplicate delivery as the main threat because that's the failure pattern that tends to wake an on-call engineer. The more important correction is this: deduplication prevents the same approved action from running twice, but it does nothing when the first action targets the wrong zone. Scope validation must happen before retry control. A worker receiving operation mig-042-zone-07 should reject the platform-owned record even if that operation ID has never appeared before; it should also reject a customer-owned record missing from the exact migration allowlist. Those refusals are expected guard behavior, not exceptional provider behavior.

DNS changes can also reach beyond web traffic. DMARC policy is published in DNS, and RFC 7489 defines discovery at a _dmarc name. That makes mail policy part of the zone-removal blast radius. The practical review question is broader than “Has the storefront moved?”: it must account for the records and delegated responsibilities contained in the zone, including mail-related policy, before deletion is approved.

The catch is that this pattern intentionally trades speed for reviewability. It is not suitable for deleting high-volume, disposable test zones where the control plane already gives each tenant an isolated account and short-lived namespace. In that case, lifecycle policy and account isolation may be the clearer boundary. For shared production credentials, keep the exact-scope gate.

Put the guard in the only execution path

The following Go sketch keeps provider details behind an adapter. More important, RemoveZone is the only method allowed to reach that adapter. A second “convenient” delete path would bypass the invariant.

package zoneguard

import (
    "context"
    "errors"
    "fmt"
    "strings"
)

type Ownership string

const (
    CustomerOwned Ownership = "customer-owned"
    PlatformOwned Ownership = "platform-owned"
)

type RemovalRequest struct {
    Zone        string
    Ownership   Ownership
    OperationID string
    Approved    bool
}

type Intent struct {
    OperationID string
    Zone        string
    Action      string
    Ownership   Ownership
}

type IntentStore interface {
    RecordBeforeExecution(context.Context, Intent) error
    Completed(context.Context, string) (bool, error)
    MarkCompleted(context.Context, string) error
}

type ZoneRemover interface {
    Remove(context.Context, string) error
}

type Guard struct {
    allowed map[string]struct{}
    intents IntentStore
    remover ZoneRemover
}

func New(allowedZones []string, intents IntentStore, remover ZoneRemover) *Guard {
    allowed := make(map[string]struct{}, len(allowedZones))
    for _, zone := range allowedZones {
        allowed[normalize(zone)] = struct{}{}
    }
    return &Guard{allowed: allowed, intents: intents, remover: remover}
}

func normalize(zone string) string {
    return strings.TrimSuffix(strings.ToLower(strings.TrimSpace(zone)), ".")
}

func (g *Guard) RemoveZone(ctx context.Context, req RemovalRequest) error {
    zone := normalize(req.Zone)
    if req.OperationID == "" {
        return errors.New("operation ID is required")
    }
    if !req.Approved {
        return errors.New("explicit approval is required")
    }
    if req.Ownership != CustomerOwned {
        return fmt.Errorf("zone %q is outside the customer-owned deletion path", zone)
    }
    if _, ok := g.allowed[zone]; !ok {
        return fmt.Errorf("zone %q is outside the reviewed migration scope", zone)
    }

    done, err := g.intents.Completed(ctx, req.OperationID)
    if err != nil {
        return fmt.Errorf("check operation state: %w", err)
    }
    if done {
        return nil
    }

    intent := Intent{
        OperationID: req.OperationID,
        Zone:        zone,
        Action:      "remove-zone",
        Ownership:   req.Ownership,
    }
    if err := g.intents.RecordBeforeExecution(ctx, intent); err != nil {
        return fmt.Errorf("record removal intent: %w", err)
    }
    if err := g.remover.Remove(ctx, zone); err != nil {
        return fmt.Errorf("remove zone: %w", err)
    }
    if err := g.intents.MarkCompleted(ctx, req.OperationID); err != nil {
        return fmt.Errorf("mark operation complete: %w", err)
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

There is a subtle concurrency requirement behind this example. Completed plus RecordBeforeExecution must be backed by a store that can claim an operation ID atomically; two workers cannot both pass a read and then create independent claims. The interface is abbreviated so the deletion policy remains visible, but the production implementation should make the claim unique and treat the operation ID as immutable. Reusing an ID for a different zone must be rejected. I'm not sure which transaction primitive fits your control plane without seeing its datastore; a uniqueness constraint, conditional write, or compare-and-set can each work if the claim and its target are bound together.

Test refusals before testing success

The test matrix should begin with cases that never call ZoneRemover: platform-owned zone, absent approval, empty operation ID, a customer-owned zone outside the allowlist, and an operation ID already bound to another target. Then test the happy path and duplicate delivery. This ordering matters because a mock that merely confirms one successful call can leave the actual guard untested.

Use example data that resembles the production boundary. A table makes the review compact:

Zone Ownership In reviewed set Approved Expected result
brand-store.example customer-owned yes yes record intent, then remove
brand-store.example customer-owned yes no reject before adapter call
returns.example customer-owned no yes reject before adapter call
checkout.platform.example platform-owned no yes reject before adapter call

Also inject failures into the intent store. If recording intent fails, deletion must not begin. Verify that invariant directly with a remover that counts calls. For retry tests, deliver the same operation ID twice and assert that a completed operation does not produce a second removal call. Then race two workers against the same ID; this is where an in-memory test double can give false confidence if it does not model atomic claims.

Watch the denials in production. Counters by reason are useful, but zone names may be sensitive operational data, so put full targets in the access-controlled intent record rather than in every metric label. Alert on repeated ownership mismatches and out-of-scope requests. A single rejection may be an operator correcting a plan; a run of them can mean the migration inventory was built from the wrong account or dataset.

Release the migration as a bounded change

Generate the allowlist from the reviewed migration plan, freeze it for the run, and show a dry-run diff before accepting approval. The approval should cover that immutable plan, not whatever a fresh discovery call happens to return later. If discovery changes after review, stop and produce a new plan.

Roll out with a small customer-owned batch and reconcile intent records against completed operations before expanding. Keep platform-owned zones in a separate inventory with no route to the removal adapter. After cutover, retain the audit record according to your organization's policy and remove the temporary permission that allowed the migration worker to delete zones.

This is slower than a raw loop over provider results. Good. Destructive DNS automation should optimize for a bounded blast radius and an explainable decision trail; throughput comes after the guard can refuse the wrong work.

Sources

Top comments (0)