DEV Community

HayesSterling2614
HayesSterling2614

Posted on

Support Domain Offboarding — Delete Tenant Records or Remove a Whole Zone, Audit API Risk

When a customer-support tenant leaves, the dangerous operation is rarely deleting a DNS record. It is proving that the record belonged to that tenant, that no other tenant still depends on the zone, and that mail authentication did not regress during the change. Short answer: delete tenant-owned records by identity, keep a shared zone unless ownership evidence says it is empty, and make deliverability proof part of the offboarding transaction.

I learned this during an offboarding drill for a support product that issued acme.example.com to each tenant. The runbook said “remove the domain.” An operator translated that into deleting the whole zone. The zone also held the platform's MX and DMARC records, so the delete would have removed the evidence we needed to explain why password-reset mail was accepted or rejected. We stopped before production, but the near miss exposed a bad invariant: a DNS name is not an ownership boundary. The review took 47 minutes because three systems disagreed about whether the tenant owned the apex, and our first query returned a stale cache. We had to compare the registrar's delegation, the authoritative answer, and the internal inventory row before anyone could state what would be safe. That tedious comparison became the design: every destructive request must carry an ownership proof and an observation timestamp, and the job must fail closed when either is missing.

The safer boundary is an application record containing tenant ID, zone ID, normalized owner name, record type, and the exact value we published. Offboarding then becomes a compare-and-delete operation. A zone is retained when any record has a different owner, an active hold, or an unresolved mail dependency. This sounds slower. It is faster than reconstructing a shared zone from provider logs after an incident.

Keep the proof.

What did the offboarding incident actually teach us?

The incident was bounded, but the failure mode generalizes. A support tenant can own t42.support.example.com while the parent zone carries SPF, DKIM selectors, MX, CAA, and a DMARC policy for several tenants. Removing the zone confuses a tenant lifecycle event with a DNS delegation event. Those are different state machines.

The first state machine records intent: active, pending-removal, removed, or legal-hold. The second records observed DNS: the answer returned by authoritative servers, its TTL, and when a resolver last saw it. I require both before declaring success. A control-plane row that says “deleted” is not deliverability evidence; a resolver answer without an owner record is not safe authorization.

For mail, preserve an export of the final SPF, DKIM, and DMARC values before mutation. RFC 7489 describes DMARC reporting and policy evaluation, which gives us a useful vocabulary for the evidence trail even when the tenant's application traffic is gone. Store the report destination, policy (p=), alignment mode, and the timestamp of the last observation. Redact message content; the DNS proof does not need it.

One short rule helps during review: delete the smallest set that makes the tenant unreachable, not the largest set that makes the dashboard look tidy.

How should a DNS API delete records in a shared zone?

Treat deletion as an idempotent, conditional workflow. First resolve the requested hostname to the canonical record identity. Then check that the stored tenant ID matches, that the current value still equals the value we issued, and that no hold or replacement operation is pending. Only then send the provider mutation. A retry should produce the same final state, not a second destructive action.

Here is the shape of the guard in Go. The DNS interface is deliberately generic so the policy can be tested against a fake server and later mapped to a provider API without changing the offboarding decision.

package offboard

import (
    "context"
    "errors"
)

type Record struct {
    ID, ZoneID, Name, Type, Value, TenantID string
}

type DNS interface {
    Find(ctx context.Context, zoneID, name, typ string) (Record, error)
    Delete(ctx context.Context, zoneID, recordID string) error
}

func RemoveTenantRecord(ctx context.Context, dns DNS, wanted Record) error {
    got, err := dns.Find(ctx, wanted.ZoneID, wanted.Name, wanted.Type)
    if err != nil {
        return err
    }
    if got.TenantID != wanted.TenantID || got.Value != wanted.Value {
        return errors.New("record ownership or value changed")
    }
    if err := dns.Delete(ctx, got.ZoneID, got.ID); err != nil {
        return err
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The compare step is the important part. If a tenant rotated a DKIM value after the offboarding job was queued, deleting by name alone could remove the replacement. If the provider reports “already absent,” classify that as success only after a fresh lookup and an audit entry; do not mask an authorization mismatch as a harmless retry.

Use a queue with a deduplication key such as tenantID/recordID/offboardingVersion. Set a deadline tied to your SLO, and emit counters for lookup mismatches, held records, propagation checks, and verification failures. Alert on a growing mismatch rate, not on every individual tenant: one mismatch is often a legitimate rotation, while a spike indicates drift in the control plane.

When is a whole-zone removal justified?

Only when the zone has a single owner and delegation is also being retired. “No records for this tenant” is insufficient. Query the ownership index for every record, including apex records and provider-managed defaults. Check nameservers and registrar delegation separately; deleting data and removing delegation have different blast radii and rollback times.

For a shared zone, keep the zone and remove only records whose ownership proof is complete. If the product must move a tenant to a customer-owned zone, publish the replacement, observe it through at least one resolver sampling window, and retain the old record until the cutover SLO is met. TTL is a cache hint, not a guarantee that every recursive resolver has converged.

The operational evidence should answer four questions without a human guessing: what was intended, what the authoritative server returned, what a public resolver returned, and which tenant authorized the change. Hashing the value in the audit index can reduce exposure while still detecting drift. Keep the unhashed export only where your retention policy permits it.

Buy, build, or keep the boundary in your platform?

The decision is less about API style than about who owns the evidence and the on-call page.

Approach Strength Trade-off Fit for tenant offboarding
Provider SDK Fast access to provider features SDK retries and error types vary; upgrades become part of the control plane Good when one provider owns every zone and its audit log is exportable
Provider REST API Explicit HTTP calls and portable test doubles You must implement pagination, retry, and identity checks Good for a small set of providers behind one internal adapter
Self-hosted authoritative DNS Full policy and evidence ownership You own anycast, signing, capacity, and incident response Appropriate when DNS is a core product capability
Internal DNS service One policy surface over several backends Adapter maintenance and provider-specific edge cases remain Usually the best boundary when tenants span customer- and platform-owned zones

Do not choose a whole-zone delete because it is the shortest API call. Choose it when your inventory proves exclusive ownership and your rollback plan can restore delegation, records, and mail evidence within the SLO. Otherwise, record-level deletion is the boring option, which is exactly why it survives a 02:00 review.

What should the runbook verify before closing an exit?

The final check is a small state reconciliation job, not a screenshot. It reads the intended record set, queries the authoritative nameservers, and samples the same public resolver class your deliverability monitors use. For DMARC, retain aggregate-report expectations from RFC 7489 and compare the next reporting interval against the pre-offboarding baseline. A missing report is a signal to investigate, not proof that mail is broken.

I also keep a “do not delete” list for shared MX, SPF includes, DKIM selectors still referenced by another tenant, CAA, and the zone apex. Your mileage may vary on the exact list because delegation models differ, but the ownership test does not change. If a provider cannot expose stable record IDs or an audit trail, the boundary is not suitable for unattended offboarding; put a human approval step around that adapter.

The catch is that record-level workflows cost more control-plane work and can leave stale data during a long propagation window. Stick with whole-zone removal only for disposable, exclusively owned zones, and only after deliverability evidence has been exported. For customer-support systems with shared mail infrastructure, that condition is uncommon.

References

Top comments (0)