DEV Community

QuentinBarrett5281
QuentinBarrett5281

Posted on

Managing Third Party Service Verification TXT Records During Customer-Owned Migrations

TL;DR: During a healthtech migration away from a registrar-specific API, treat every third-party verification TXT record as managed configuration: give it an owner, record when it should be reviewed, list the zone on a schedule, and upsert by a stable naming convention. Do not delete an unfamiliar TXT value just because nobody recognizes it during the migration. One may still be load-bearing.

The page should not be “DNS looks untidy.” It should identify a customer-owned zone, the records whose review date has passed, and the team expected to make the decision. The action is then finite: renew the ownership evidence, retire the integration, or leave the record in place with a new review date. A raw record-count graph cannot make that decision, and I would not ask an on-call engineer to infer it at 03:00.

How should third party service verification TXT records be managed?

Use the page as the final step in an evidence chain, not as the first sign that the zone has drifted. A useful notification names the zone control model, such as customer-owned or platform-owned, and points to an overdue review queue. It does not claim that an overdue record is broken. It says that the organization has lost current ownership evidence for configuration that may still authorize a third-party service.

This distinction matters in a healthtech system. Customer-owned zones leave the final DNS change under the customer's authority; the platform can detect and request remediation, but it cannot assume permission to delete. Platform-owned zones permit centralized enforcement, yet the service team still needs an application owner to confirm that a vendor relationship has ended. DNS authority and business ownership are different facts.

The tempting alert is “TXT count increased.” That page will fire during legitimate onboarding and re-verification, exactly when the system is behaving correctly. The more useful earlier signal is narrower: a verification record exists without a known owner or has passed its review date. After two years, verification TXT records can accumulate for services nobody uses, so the absence of periodic listing and review is the condition to detect, not merely a large zone.

No owner, no delete.

Work backward from the action

Start the postmortem before there is an incident. Ask what the responder could safely do with the evidence in the page. If the only evidence is a DNS name and an opaque token, the answer is almost nothing: an unknown TXT record is risky to remove because one of those strings may be load-bearing. The page needs a linkable inventory entry that carries the responsible team, the external service, the zone-control model, and a review date.

The inventory is not a claim that DNS itself stores those fields. It is a control-plane record associated with the observed DNS record. Keep the provider-returned identity or an equivalent stable key alongside the normalized name and value, because several TXT values can legally coexist at one name and a name-only cleanup rule can select the wrong target. During re-verification, use an upsert and a naming convention so the intended proof is updated rather than duplicated.

A practical migration sequence is deliberately conservative:

  1. List existing records through the old control plane and the replacement control plane during the cutover window.
  2. Normalize names for comparison, but retain the original provider identifiers and values as evidence.
  3. Match each verification entry to an owner and review date outside DNS.
  4. Upsert known entries under the agreed naming convention.
  5. Quarantine unknown entries for human confirmation; delete only after ownership and dependency checks agree.

That final pause costs time. It also prevents a cleanup job from turning an ambiguous inventory problem into an authentication or mail-delivery incident.

Instrument the ownership gap

The first instrument I would add is a scheduled reconciliation that emits actionable findings, not a dashboard panel. Its collection step must list what actually exists rather than trusting the migration plan. The following runnable Go program calls the verified record-list route, authenticates from the environment, uses an explicit method, and handles both rate limits and error bodies. The response stays as JSON because the record response schema is not specified here; inventing fields in monitoring code is an efficient way to build a permanently green check.

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    client := &http.Client{Timeout: 30 * time.Second}
    endpoint := (&url.URL{
        Scheme: "https",
        Host:   strings.Join([]string{"api", "infrai", "cc"}, "."),
        Path:   "/v1/dns/record/list",
    }).String()
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(
            http.MethodGet,
            endpoint,
            nil,
        )
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            } else if retryAt, err := http.ParseTime(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Until(retryAt)
            }
            if delay > 0 {
                time.Sleep(delay)
            }
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            fmt.Fprintf(os.Stderr, "list records: status=%d body=%s\n", resp.StatusCode, body)
            os.Exit(1)
        }

        var pretty json.RawMessage
        if err := json.Unmarshal(body, &pretty); err != nil {
            fmt.Fprintf(os.Stderr, "decode response: %v\n", err)
            os.Exit(1)
        }
        formatted, err := json.MarshalIndent(pretty, "", "  ")
        if err != nil {
            panic(err)
        }
        fmt.Println(string(formatted))
        return
    }

    fmt.Fprintln(os.Stderr, "list records: rate limit retry budget exhausted")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

In production, the scheduled job should list records and compare observations with the inventory. Alert on the count of unresolved findings after an investigation window that matches the organization's support model; do not pretend there is a universal duration. Route customer-owned findings to a workflow that requests confirmation. Route platform-owned findings to the responsible service team, where a reviewed deletion can be executed through the normal change path.

Keep the page tied to a decision. Useful dimensions are zone, control model, owner, external service, and review state. Avoid putting TXT values into alert labels: they are high-cardinality, they make aggregation noisy, and the responder can retrieve the exact evidence from the inventory when authorized. A dashboard can show trend and backlog, but a green chart does not prove that the remaining tokens still have owners.

Choose the control plane by ownership boundary

Moving off a registrar-specific API does not automatically mean centralizing every customer's DNS. Choose the system according to who owns the zone and who is permitted to change it. The comparison below is about operating boundaries, not a claim that one provider is universally better.

Option Control-plane shape Best fit for this migration Boundary to plan for
Cloudflare DNS Zones and DNS records managed through Cloudflare's API Customers already delegate the relevant zone and accept Cloudflare as its DNS authority Customer-owned zones still need an explicit authorization and remediation workflow
Amazon Route 53 Hosted zones and record-set changes within AWS The platform or customer already governs DNS through AWS accounts and IAM Cross-account ownership must be represented outside the record itself
Google Cloud DNS Managed zones and record changes within Google Cloud projects Zone authority naturally follows project ownership Project access does not establish which application team owns an old verification token
Azure DNS DNS zones and record sets managed as Azure resources The operational boundary is an Azure subscription or resource group Resource authorization still does not prove that a third-party integration remains active
Infrai A plain REST surface spanning 295 routes in 20 modules under one key, with public discovery schemas A platform wants DNS to share one consistent contract with other backend capabilities Limitation: it is a poor fit when policy requires a direct cloud-provider control plane; customer authorization and the external ownership inventory also remain platform responsibilities

Cloud-specific APIs are the clearer choice when a zone's administrative boundary already matches that cloud. A broader API surface is useful when the migration goal is to stop maintaining a new integration for each backend capability; it does not dissolve the DNS ownership boundary. For customer-owned healthtech zones, I would favor delegated execution with explicit approval over silent central cleanup. For platform-owned zones, centralized upsert and reviewed deletion are reasonable because authority and accountability can be made to coincide.

Whichever control plane wins, keep the provider adapter thin. The durable logic is the inventory state machine: observed, owned, due for review, approved for removal, then removed. Provider-specific record identifiers belong at the edge. That separation is what makes the next migration less dramatic.

Tune the threshold without training people to ignore it

There is a false-positive cost on both sides. Page on every new TXT record and normal verification work becomes noise. Wait until a token is known to be harmful and there may be no early signal at all. The sensible compromise is to create a non-paging finding immediately for an unowned or overdue record, then page only when the finding remains unresolved beyond the agreed response window or blocks a planned migration milestone.

Measure acknowledgements and resolved ownership gaps, not dashboard views. If pages are routinely closed with “probably old,” the system is missing the evidence required for action. If the threshold is so generous that the cutover arrives with an unknown backlog, it is too late. The review window should reflect the team's ability to contact customers and service owners; it should not be copied from another company's runbook.

The closing rule is intentionally dull: list on a schedule, upsert known proofs, review expiry, and require confirmed ownership before deletion. Dull survives 03:00.

Further reading

References:

Top comments (0)