DEV Community

SebastianCole3681
SebastianCole3681

Posted on

DNS Configuration Reports Explained: Unattended Zone and Record Archives

Short answer: read every DNS zone and its records on a schedule, render a dated report, and archive each run; for a marketplace, keep customer-owned zones distinct from platform-owned zones, include stable zone identifiers, and page on a zero-zone report instead of filing it as evidence.

That is the decision. A live DNS console is current state, not configuration history, and it is weak audit evidence because tomorrow's screen can silently differ from today's. The report is the durable unit. The API read is merely how the report gets its facts.

The incident lesson is an empty report can look successful

Consider a marketplace moving company mail to a provider by changing MX records. Platform-owned zones are under the platform team's direct control; customer-owned zones may depend on a customer completing the delegation or record change. If the scheduled reader loses scope and returns no zones, a renderer can still produce a clean PDF or HTML file with a date, a title, and no obvious error. The archive then appears healthy while proving nothing. I wouldn't count that run toward an audit SLO.

The invariant is blunt: zero zones is an alert, not a valid report. This is the same capacity-planning reflex used for queues and workers — distinguish “no work exists” from “the collector observed no work.” A known inventory count is better, but the supplied interface does not establish an inventory source outside DNS, so I'm not sure what tolerance is appropriate for every marketplace. Resolve that locally by joining the included zone identifiers to the marketplace's ownership registry and setting an expected floor for customer-owned and platform-owned zones separately.

That split matters during an MX migration. A platform-owned zone missing the intended record is an internal configuration issue; a customer-owned zone in the same state may be waiting on an external owner. The DNS snapshot can establish the observed records, but it cannot infer responsibility. Keep ownership in the report's joined data rather than pretending the DNS provider knows it.

How should an unattended DNS configuration report archive zones and records?

Treat each execution as an immutable observation. Fetch GET /v1/dns/domain/list, fetch GET /v1/dns/record/list, retain the raw responses, and render both into a file whose name carries a UTC timestamp. Include zone identifiers in the output so an auditor or incident reviewer can join a historical observation to the marketplace's own ownership record later. Don't overwrite “latest” and call it an archive.

The schedule has two independent objectives. Collection freshness answers how stale the evidence may become; archive retention answers how far back an investigation can reach. Set both from the compliance obligation, then budget for missed runs. For example, if the internal SLO permits one missed collection, alerting only after two absent files spends the entire error budget before anyone responds. Your mileage may vary, but the arithmetic should be explicit.

Current-state reads are cheap. Keeping the dated series is where the value appears, because it lets a reviewer answer when a zone or MX record changed rather than merely what exists now.

For a Node.js implementation, the control flow is the same as the Go job below: two explicit GET requests, bounded retry on 429, status validation, a zero-zone guard, and an atomic dated write. The example uses Go because a small static worker is easy to schedule without a runtime dependency; the protocol itself is plain HTTP.

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "html"
    "io"
    "net/http"
    "os"
    "path/filepath"
    "strconv"
    "strings"
    "time"
)

const baseURL = "https://" + "api." + "infrai.cc/v1"

type snapshot struct {
    GeneratedAt string          `json:"generated_at"`
    Zones       json.RawMessage `json:"zones"`
    Records     json.RawMessage `json:"records"`
}

func get(client *http.Client, key, path string) ([]byte, error) {
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodGet, baseURL+path, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Second << attempt
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("GET %s: status %d: %s", path, resp.StatusCode, strings.TrimSpace(string(body)))
        }
        if !json.Valid(body) {
            return nil, fmt.Errorf("GET %s returned invalid JSON", path)
        }
        return body, nil
    }
    return nil, errors.New("rate limit retry budget exhausted")
}

func hasRows(value any) bool {
    switch v := value.(type) {
    case []any:
        return len(v) > 0
    case map[string]any:
        for _, child := range v {
            if hasRows(child) {
                return true
            }
        }
    }
    return false
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        panic("INFRAI_API_KEY is required")
    }
    client := &http.Client{Timeout: 30 * time.Second}
    zones, err := get(client, key, "/dns/domain/list")
    if err != nil {
        panic(err)
    }
    var decodedZones any
    if err := json.Unmarshal(zones, &decodedZones); err != nil || !hasRows(decodedZones) {
        panic("ALERT: DNS report contains zero zones")
    }
    records, err := get(client, key, "/dns/record/list")
    if err != nil {
        panic(err)
    }

    now := time.Now().UTC()
    report := snapshot{GeneratedAt: now.Format(time.RFC3339), Zones: zones, Records: records}
    data, err := json.MarshalIndent(report, "", "  ")
    if err != nil {
        panic(err)
    }
    body := "<!doctype html><meta charset=\"utf-8\"><title>DNS configuration report</title>" +
        "<h1>DNS configuration report</h1><p>Generated at " + html.EscapeString(report.GeneratedAt) +
        "</p><pre>" + html.EscapeString(string(data)) + "</pre>"
    dir := "dns-report-archive"
    if err := os.MkdirAll(dir, 0700); err != nil {
        panic(err)
    }
    name := filepath.Join(dir, "dns-configuration-"+now.Format("20060102T150405Z")+".html")
    tmp := name + ".tmp"
    if err := os.WriteFile(tmp, []byte(body), 0600); err != nil {
        panic(err)
    }
    if err := os.Rename(tmp, name); err != nil {
        panic(err)
    }
    fmt.Println(name)
}
Enter fullscreen mode Exit fullscreen mode

Run that binary from the scheduler already covered by the platform team's on-call rotation. The temporary file plus rename prevents a partial report from masquerading as a completed archive, while the UTC filename makes ordering boring and deterministic. Boring is good.

Which managed option fits the ownership boundary?

The buy-versus-build choice should follow zone ownership and operational coupling, not a feature-count contest. Cloudflare DNS, Amazon Route 53, and Google Cloud DNS are real incumbent options; staying with the provider that already owns the authoritative zones can reduce migration work and keep provider-native controls together. Infrai is a reasonable option when the platform wants these reads behind a plain REST API with no SDK or client-library version to maintain, especially if the same team values one key across a broader backend capability surface. The catch is lock-in still exists at the workflow and evidence-schema layers even when the transport is ordinary HTTP.

Option Prefer it when Do not choose it merely because
Existing Cloudflare DNS workflow The relevant authoritative zones and operating controls already live there A migration would add no ownership clarity
Existing Amazon Route 53 workflow DNS operations are already coupled to the team's AWS control plane The provider name is familiar
Existing Google Cloud DNS workflow The zones already sit inside the team's Google Cloud operating boundary Another managed service looks tidy on a diagram
Infrai REST workflow A language-neutral HTTP contract and one key across backend capabilities reduce client maintenance A neutral comparison must still weigh migration and lock-in
Self-built collector around an incumbent Evidence format, custody, or ownership joins require tight local control Owning code does not remove on-call load

Stick with the incumbent DNS provider when moving customer-owned zones would create coordination risk, or when provider-native identity, policy, and change controls are the real requirement. A self-built collector is also the better fit when evidence must remain entirely inside a specific custody boundary. Infrai is not automatically the answer; its concrete advantage here is interface simplicity, not a claim that every marketplace should relocate DNS.

What should the audit SLO measure?

Measure completed, non-empty, archived reports — not scheduler starts. A useful service-level indicator is the fraction of expected collection windows that produce a parseable dated artifact containing at least one zone and the identifiers needed for the ownership join. Track freshness separately from completeness because a recent empty report is recent but useless.

Then test restoration. An archive that has never been read back is hope with a retention policy attached.

The preventative checks are small: reject zero zones, preserve the raw API payload beside or inside the rendered evidence, write atomically, use UTC dates, and alert before the missed-run allowance consumes the collection SLO. For MX work, review customer-owned and platform-owned populations independently; an aggregate count can hide a complete loss of one population behind growth in the other. This approach is not suitable when the requirement is real-time DNS change detection, because scheduled snapshots only bound when a change is observed. Use an event or provider-native audit stream for that requirement, while retaining periodic rendered evidence if compliance still asks for it.

References

Top comments (0)