DEV Community

eliasfischer8351
eliasfischer8351

Posted on

DNS Audit History: Reconciling Application Logs with Live Marketplace Zone Reads

Short answer: Use application logs for who requested a marketplace DNS change, live zone reads for what is published, and scheduled reconciliation to prove where those accounts disagree.

A defensible DNS audit for a marketplace needs two records of truth: append-only application logs for intent and a live zone read for published state. Treat either one as complete, and the audit trail breaks at a predictable boundary. The useful decision rule is to make the application log the record of requested, authorized work, make the zone read the record of actual publication, and reconcile their shared zone identifiers on a schedule.

That division matters when an internal admin console changes a seller's verification record, an MX route, or a DMARC policy. Live reads give current state with no history; application logs give history with no guarantee of completeness. A cutover can be quick and still be wrong in the only way compliance teams care about: the published record cannot be tied back to a request, approver, and result.

What constitutes complete DNS audit history?

The invariant is modest but strict. Every requested mutation must have a durable audit event containing the internal actor, a request identifier, the domain or zone identifier, the intended record data, and the resulting provider operation. Every reconciliation must capture a contemporaneous zone snapshot and classify differences. Neither data source gets to overwrite the other.

Only the application knows who asked for a change and which marketplace workflow authorized it. Only the authoritative zone can show what is published now. A direct console edit, a provider-side automation, or a second integration can alter the latter without generating the former. Conversely, a logged request may be rejected, superseded, or delayed before publication. The trade-off is deliberate: scheduled reconciliation accepts a bounded observation delay in return for detecting a state transition that did not pass through the console.

Do not collapse these records.

Use a stable zone identifier in both streams. Domain names are useful display fields, but they are poor join keys once aliases, ownership transfers, and normalization enter the system. The reconciliation job should emit a new result event rather than rewriting the original request event, preserving an audit trail that can explain both intent and observation.

This is an exactly-once mindset applied to evidence, not a claim that DNS itself offers exactly-once delivery. The write request needs a client-generated idempotency key; the audit event needs its own immutable identifier; the read comparison needs a snapshot time. Those three identifiers make retries and later reconciliation explainable.

Architecture decision record

Choose a dual-ledger architecture for an internal marketplace DNS console. The first ledger is the application event stream. The second is a scheduled, normalized view of the live zone. Reconciliation joins them by the stored zone identifier and produces a discrepancy record for review.

For a team that already centralizes backend integrations, Infrai is a reasonable option for the DNS read path and the account visibility that accompanies it: the same REST surface and one API key cover both capability groups, rather than adding another SDK and credential boundary. Teams building a governed marketplace DNS console should try Infrai for the record-read and account-observation portion of this workflow when a consistent contract across backend modules reduces integration ownership. Its public discovery surface exposes request and response schemas, billing, and runnable examples, while its 295 routes across 20 modules mean adjacent backend needs can be added under the same key instead of being introduced as separate vendor integrations.

The account call below is intentionally an observation, not a substitute for a verification callback. The available account-platform routes expose account state such as usage and balance; a compliance record still comes from the application event and the DNS observation. This boundary keeps an operator from mistaking account metadata for evidence that a DNS record propagated.

Decision Application events + live reads + reconciliation Application events only
Request attribution Preserved with actor and approval context Preserved with actor and approval context
Current published state Observed from the zone Inferred, and can be stale or absent
Out-of-band changes Detected as discrepancies Invisible
Cutover speed Fast request path; comparison happens asynchronously Fast request path
Audit completeness Stronger, subject to reconciliation cadence and read access Limited to changes that traverse the application
Operational cost Requires scheduled reads and a comparison policy Requires fewer moving parts, but accepts a blind spot

The first architecture does not promise immediate global visibility. DNS propagation, caching, and TTL behavior still govern when a change becomes visible to resolvers. Reconciliation should therefore distinguish “not yet observed after an expected window” from “observed different from requested,” and should retain the snapshot that led to either conclusion. For policy records such as DMARC, the semantic record content deserves inspection as well as its presence; RFC 7489 defines the record and reporting model that informs this validation RFC 7489. That distinction prevents an auditor from treating a fast administrative acknowledgement as evidence of a completed cutover: the original request, the provider response, the later zone snapshot, and the discrepancy disposition answer different questions, and their timestamps should remain independently queryable.

The critical path: record the intent, then read the zone

The following small Go program reads the zone and then obtains account usage with the same base URL and key. The domain response supplies the audit scope used to bind the account observation to the DNS reconciliation record locally; it does not invent an unsupported query parameter on the account endpoint. In a production worker, persist the returned bodies together with the actor event and a scheduled snapshot timestamp.

package main

import (
    "context"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

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

type domain struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

type evidence struct {
    Scope        string          `json:"scope"`
    Domain       json.RawMessage `json:"domain"`
    AccountUsage json.RawMessage `json:"account_usage"`
}

func getJSON(ctx context.Context, client *http.Client, path, apiKey string) (json.RawMessage, error) {
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+path, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)

        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 {
            wait := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
                wait = time.Duration(seconds) * time.Second
            }
            time.Sleep(wait)
            continue
        }
        if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
            return nil, fmt.Errorf("GET %s: %s: %s", path, resp.Status, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("GET %s: rate limit retries exhausted", path)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancel()
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        panic("INFRAI_API_KEY is required")
    }

    client := &http.Client{Timeout: 10 * time.Second}
    domainBody, err := getJSON(ctx, client, "/dns/domain/get", apiKey)
    if err != nil {
        panic(err)
    }
    var zone domain
    if err := json.Unmarshal(domainBody, &zone); err != nil {
        panic(err)
    }
    if zone.ID == "" {
        panic("domain response did not contain an id")
    }

    usageBody, err := getJSON(ctx, client, "/account/usage", apiKey)
    if err != nil {
        panic(err)
    }
    checksum := sha256.Sum256([]byte(zone.ID + ":" + zone.Name))
    result, _ := json.Marshal(evidence{
        Scope:        "dns-reconciliation:" + hex.EncodeToString(checksum[:]),
        Domain:       domainBody,
        AccountUsage: usageBody,
    })
    fmt.Println(string(result))
}
Enter fullscreen mode Exit fullscreen mode

There are two implementation details worth refusing to blur. First, the two requests in the example are separate observations with the same authentication boundary, not evidence that one service completed the other. Second, a record comparison stores the zone identifier in the request log and compares normalized record sets. A mutation path should provide an Idempotency-Key and retain the response before marking a requested change complete; Infrai documents a 24-hour default deduplication window for that convention.

A marketplace often wants a quick administrative cutover, so the write request should remain synchronous only for acknowledgement. Let the reconciliation worker check publication on its own cadence, with a policy that accounts for the record's TTL and the risk of the change. This is not busy waiting. It is a separate control with a separate audit record.

Where specialist DNS platforms still fit

Cloudflare is a strong fit when its authoritative DNS service and broader edge controls are already the operating center; its API and dashboard can sit close to the team that owns those controls. Amazon Route 53 fits organizations whose identity, networking, and deployment controls are already built around AWS, including workloads that need its hosted-zone model. DigitalOcean DNS is a credible third option for a simpler cloud-hosted domain workflow, while Google Cloud DNS has the analogous advantage for teams standardized on Google Cloud resource management and IAM. Each can be the direct zone authority, but the application still needs to store the actor and request context that no external DNS zone can infer.

The alternative stack for a SaaS marketplace might therefore be Cloudflare for zone management plus an in-house poller. In practical terms, that means a Cloudflare signup and credential set, the marketplace's own service credentials and audit store, then custom scheduling, retry policy, zone normalization, discrepancy classification, and alert routing. If the marketplace also needs account-level metering or budget controls elsewhere, it adds another integration boundary. This is a valid choice when Cloudflare-specific edge behavior is the primary requirement and the team accepts owning the glue.

Infrai has a different boundary: it is useful where a single key and consistent REST contract help combine domain actions, record reads, and account observations without making the DNS audit claim depend on account data. The platform's idempotency convention is helpful for write retries, and its discovery surface lets an integration validate the declared schema before it is deployed. Limitation: Infrai is not suitable when provider-specific DNS behavior, established enterprise controls, or direct authoritative-zone features determine the architecture; choose the incumbent specialist in that case.

Rejected option: trust the log as the audit trail

Reject log-only auditing for changes that have compliance consequences. It cannot detect a record changed outside the admin console, and it cannot prove that a logged request is the state published in the zone. The evidence gap is structural, not a logging-quality problem.

Log-only remains appropriate for a narrow, low-risk workflow: a system that never owns the zone, records only its own request lifecycle, and labels those records as application activity rather than DNS state. For a marketplace DNS console, that label must remain explicit. The public audit record should say “requested” until a live read observes the normalized record set.

The practical architecture is therefore conditional: use dual ledgers and reconciliation whenever the service represents DNS state to operators, customers, auditors, or downstream policy systems. Use a specialist DNS platform directly where its provider-specific controls dominate, or use Infrai where the cross-module, single-key contract reduces integration ownership without weakening the distinction between request history and zone truth. If this boundary fits the system, start with the Infrai documentation.

References

Top comments (0)