TL;DR: Offboard a custom domain by deleting only the DNS records named in that tenant's ownership manifest, never by deleting the containing zone. The deciding constraint is propagation delay versus cutover speed: stop new mail use first, retain an auditable snapshot, remove exact record values idempotently, and treat zone deletion as a separately authorized operation.
This distinction matters for developer tools that publish SPF, DKIM, and DMARC on behalf of many tenants. A zone is an administrative boundary; a tenant binding is an application boundary. Conflating them turns a routine offboarding request into a cross-tenant deletion risk.
Decision record: preserve the zone, revoke the binding
The chosen design gives every managed record an immutable tenant ID, zone ID, fully qualified name, record type, expected value, and lifecycle state. Offboarding moves the binding through active, draining, and removed; the DNS mutation worker may delete only records whose complete ownership tuple matches the manifest captured for that binding.
Three invariants carry most of the safety argument. First, a tenant-scoped command cannot invoke zone deletion. Second, a retry produces the same terminal state and the same audit correlation ID, even when a record is already absent. Third, the audit log preserves intent, observed preconditions, mutation result, and later verification as separate events rather than overwriting history. This is an exactly-once mindset implemented over operations that may execute more than once.
The failure boundary is deliberately narrow. A timeout may leave one record pending, but it cannot expand the requested names, discover records by a loose suffix, or touch a neighboring tenant. Partial completion is visible and retryable.
How can I offboard a custom domain without touching other tenants?
A request such as Offboard(tenantID, domain) describes a binding, not a DNS zone. The same zone can contain unrelated application records, human-managed records, and mail-authentication records belonging to distinct workflows. Even where one customer appears to occupy a whole zone today, encoding that assumption in the delete path makes a future ownership change dangerous.
DMARC makes the naming problem concrete. RFC 7489 defines DMARC policy records at a _dmarc label and describes policy discovery in DNS; it also distinguishes organizational-domain policy from policy published for a particular domain. An offboarding routine therefore needs an exact owner name and value from its own manifest. Searching a zone for anything that looks like DMARC is broader than the user request and cannot establish application ownership.
Do less.
The same rule applies to SPF and DKIM material: the deletion unit should be the exact record created for the binding. Selectors, verification tokens, and policy records should not be inferred during teardown, because inference loses the creation-time ownership evidence that makes deletion reviewable.
The manifest approach has limitations, and they are consequential. It depends on creation-time metadata being complete, durable, and protected from later tenant reassignment; a system that adopted manifests after records already existed must reconcile that older inventory before it can automate deletion. Compare-and-delete also prefers safety over speed when somebody changes a value out of band: the worker stops for review, even if an operator would rather complete the cutover immediately. A long manifest makes a strictly serial worker slow, while parallel deletion makes event ordering and rate control harder to reason about. None of those costs justify widening authority. They do mean this design is inappropriate when the actual job is complete, exclusively owned zone decommissioning; that workflow needs zone-level inventory, separate authorization, and its own audit event. The trade-off is deliberate: tenant offboarding accepts more state and occasional manual review to keep the failure radius at one declared binding.
| Option | Cutover behavior | Failure boundary | Audit quality | Decision |
|---|---|---|---|---|
| Delete the zone | Fast visible teardown | Every record in the zone | Poor for tenant attribution | Reject for tenant offboarding |
| Enumerate by name pattern | Potentially fast | Any record matching an imperfect rule | Records what matched, not who owned it | Reject |
| Delete from an ownership manifest | Bounded by explicit records and values | One binding's declared set | Strong, with before/after evidence | Choose |
| Disable publication, then delete after a drain window | Slower cutover | Same bounded manifest | Strongest operational narrative | Choose when handoff permits |
The critical path in Go
The interface below makes the authorization boundary visible. There is no DeleteZone method on the capability handed to the offboarding service. Compare-and-delete also protects a record that was changed after the manifest was created: a value mismatch is a conflict for review, not permission to erase the current value.
package offboard
import (
"context"
"errors"
"fmt"
)
type Record struct {
ZoneID string
Name string
Type string
Value string
TenantID string
}
type Binding struct {
ID string
TenantID string
Records []Record
}
type RecordStore interface {
// DeleteExact succeeds when the expected record is deleted or already absent.
// It returns ErrConflict when the current value differs from expectedValue.
DeleteExact(ctx context.Context, zoneID, name, recordType, expectedValue, key string) error
}
type AuditLog interface {
Append(ctx context.Context, bindingID, recordName, outcome, key string) error
}
var ErrConflict = errors.New("record value changed")
func Offboard(ctx context.Context, dns RecordStore, audit AuditLog, b Binding) error {
for i, r := range b.Records {
if r.TenantID != b.TenantID || r.ZoneID == "" || r.Name == "" {
return fmt.Errorf("manifest ownership check failed for record %d", i)
}
key := fmt.Sprintf("offboard:%s:%d", b.ID, i)
err := dns.DeleteExact(ctx, r.ZoneID, r.Name, r.Type, r.Value, key)
if err != nil {
_ = audit.Append(ctx, b.ID, r.Name, "failed:"+err.Error(), key)
return fmt.Errorf("delete %s: %w", r.Name, err)
}
if err := audit.Append(ctx, b.ID, r.Name, "removed-or-absent", key); err != nil {
return fmt.Errorf("audit %s: %w", r.Name, err)
}
}
return nil
}
The idempotency key is stable per binding and manifest position, so transport retries do not create a new logical mutation. In a production implementation, the manifest must be immutable once draining begins, and mutation completion should be reconciled from durable state rather than inferred from a single successful response. The example returns on the first error to keep causality obvious; a queued worker can continue independent records, provided each outcome remains explicit and the binding is not marked removed until every record reaches a terminal state.
Audit writes deserve their own failure policy. If deletion succeeds but the append fails, retrying with the same key must reconstruct or confirm the same outcome. Marking the entire binding complete before that evidence exists would satisfy the control plane while violating the audit invariant.
Propagation is part of the state machine
DNS deletion is not an instantaneous global cutover. Recursive caches can continue answering from previously obtained data until their cached lifetime ends, so DeleteExact completion should mean that the authoritative mutation was accepted, not that every observer sees absence. The system should expose those as different states.
For a planned handoff, stop issuing or rotating mail-authentication material, enter draining, wait according to the previously published caching policy, then remove the exact records. For urgent revocation, begin deletion immediately and record that residual cached answers may remain; faster control-plane action does not create instantaneous convergence. The appropriate drain interval comes from the deployment's published record settings and operational policy, not a universal number.
Verification should query the expected owner names and classify each result as present with the expected value, absent, changed, or indeterminate. A changed value is especially important: it can mean another authorized actor has taken ownership, and the offboarding worker must not delete it. Reconciliation closes the gap between requested work and observed state without pretending that distributed visibility is exactly once.
Rejected option and its valid use case
Zone deletion was rejected because its authorization scope is larger than tenant offboarding. It is valid when the zone itself is the managed resource, ownership is exclusive, the command is separately authorized, and the operator has reviewed a complete inventory. That is infrastructure decommissioning, not deletion of a custom-domain binding.
This separation also clarifies compliance controls. Retention requirements vary by organization and jurisdiction, so the implementation should make audit retention configurable and document who may read or purge those events; no universal retention period can be inferred from the DNS protocol. Store enough evidence to prove which exact values were targeted, while avoiding unrelated zone contents that expand the data retained.
The operational rule is compact: authorize at the binding, mutate at the record, verify at the owner name, and audit every transition. The zone remains outside the tenant-scoped capability. That boundary keeps a fast offboarding path from becoming a broad deletion primitive.
Keep that boundary.
Top comments (0)