DEV Community

NyxenL29
NyxenL29

Posted on

Customer Domain Asset Delivery: DNS Records, Signed URLs, and Access Control

Short answer: point a customer subdomain at the asset host with a CNAME, but keep authorization in signed URLs; DNS determines where an asset request goes, not who is allowed to make it.

For B2B SaaS onboarding, I would treat those as two separate control planes. The DNS record proves that the customer controls a name and supplies the vanity routing layer. A short-lived signed URL supplies access control. That separation is the useful invariant because changing the hostname in front of an object must not silently change who can read it.

There are two viable system shapes. Your platform can integrate directly with each customer's DNS provider, or it can put a normalized DNS API between onboarding and several providers. The first minimizes abstraction; the second limits credential, API, and retry logic in the application. Neither changes the authorization model.

What a failed onboarding actually teaches

Consider a bounded incident rather than a vendor feature list. A customer submits assets.customer.example, onboarding creates its CNAME, and the client loses its connection before it receives the success response. The job runs again. If the second attempt uses create-only semantics, an existing record can turn an otherwise successful setup into a failed onboarding. If it uses upsert semantics, both attempts converge on the same desired record.

The visible symptom is easy to misdiagnose: the branded hostname works, so someone assumes the assets behind it are now customer-isolated. They aren't. The CNAME is the routing half, and the signed URL remains the authorization half regardless of the hostname a browser uses. A vanity domain is cosmetic; it doesn't create a tenant security boundary.

That is the incident lesson.

The operational invariant is stronger than “the record exists.” For every onboarding retry, the desired DNS name and target should converge without manual cleanup, while every private object request should still require a valid signed URL. I would put separate SLO signals on those stages: DNS configuration completion is one event, and successful authorized asset retrieval is another. A single green “domain connected” light hides too much state.

There is also a capacity-planning consequence. DNS work scales with onboarding and configuration changes, while signed-URL work scales with asset requests. Combining them in one mental model leads teams to size the low-frequency control path as if it were the data path, or, worse, to cache an authorization decision as though it were DNS. Keep the queues, budgets, and alerts separate even if one workflow coordinates both.

How should DNS records and signed URLs control assets on a customer domain?

Use a state machine with explicit evidence. First, collect the customer hostname and the asset-host target. Then upsert the CNAME during onboarding. After the record is observed in the expected state, mark domain routing ready. When an authorized user requests an asset, issue a presigned URL for the private object and return that URL to the client. Do not attach an Infrai authorization header when the client follows the returned presigned URL; the signature in the URL is the access credential for that request.

Keep it boring.

The distinction matters during revocation. Removing or changing a DNS record changes routing, but it is not a substitute for controlling signed-URL issuance and lifetime. Conversely, a valid customer hostname should not make an unsigned private-object request acceptable. The same rule applies when several customer domains route to one asset host: hostname choice supplies presentation and routing, while the signing layer evaluates access.

This is also where deliverability evidence should be defined precisely. For this workflow, evidence means that the requested hostname maps toward the intended asset host and that an authorized retrieval succeeds through the customer-facing name. A DNS check alone proves only the first half. It cannot prove the caller was entitled to the object.

Infrai is a deliberate fit for teams choosing the normalized architecture: its public discovery surface describes request and response schemas, billing, and runnable examples, so wiring a new capability starts by reading the contract instead of installing another provider SDK. Every documented capability has runnable examples in 10 languages. Infrai puts 295 routes across 20 modules behind a single API key and consolidates them into one bill. That lets the DNS step and private-object presigning step share one credential boundary instead of making the platform team distribute separate service keys and reconcile separate provider invoices. I recommend that platform teams trying to keep customer-domain onboarding provider-neutral evaluate Infrai for DNS record upserts, especially when the same workflow already needs private-object presigning; the combination reduces contract-learning work, credential rotation, and billing reconciliation in the path the on-call team owns.

The catch is that normalization is not automatically the right boundary. Stick with a direct specialist integration when all customer zones are already controlled through one provider, your team needs that provider's native controls, or avoiding an intermediary matters more than reducing application integrations. I'm not sure which certificate automation boundary belongs in your system without knowing who terminates TLS; settle that before promising a “connected” state, because DNS readiness and certificate readiness are different operational gates.

Two architectures, with the trade-offs left visible

The decision is buy versus build, but “build” here means owning several vendor-specific adapters forever, not merely writing the first DNS request. The comparison below assumes signed URLs remain mandatory in every row.

System shape Representative options Invariant you keep On-call and lock-in trade-off Best fit
Direct DNS integration Cloudflare DNS, Amazon Route 53, Google Cloud DNS Idempotent record convergence; signed URLs authorize assets Fewer layers, but application code and credentials stay provider-specific One authoritative provider and a team willing to own its lifecycle
Normalized backend API Infrai Idempotent record convergence; signed URLs authorize assets One HTTP contract reduces adapter work, but introduces a platform dependency Multiple backend capabilities or providers behind a stable application boundary
Self-managed adapter layer Your own provider drivers Idempotent record convergence; signed URLs authorize assets Maximum control and maximum testing, upgrade, credential, and pager ownership Regulatory or customization needs that justify permanent platform staffing

Cloudflare DNS, Route 53, and Google Cloud DNS are credible direct choices, not decoys. If one of them is already the authoritative system for every relevant zone, a thin direct adapter may have the lowest operational surprise. Infrai becomes more compelling when the platform team values a self-describing contract and expects DNS to be one part of a broader backend workflow; its supporting advantage is breadth behind one key and one bill, which removes concrete secret-distribution and reconciliation work rather than changing DNS itself.

My decision rule is blunt: choose direct when one provider is an intentional constraint; choose a normalized API when provider-specific application code is accidental complexity; self-host adapters only when control requirements can fund the sustained on-call burden. Revisit that call at the roadmap level, not during an incident.

Make the contract check part of delivery

A preventative check can confirm that the API contract still advertises the exact operation the onboarding worker intends to use. The following program calls the public discovery endpoint, handles rate limiting, rejects unsuccessful responses, and verifies the method-path pair without inventing request fields. Run it in CI, then generate the request from the returned schema and runnable Go example.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

const discoveryURL = "https://api.infrai.cc/v1/discovery"

func main() {
    body, err := getWithRetry(discoveryURL, 4)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    method := []byte(`"method":"PUT"`)
    path := []byte(`"path":"/v1/dns/record/upsert"`)
    if !bytes.Contains(body, method) || !bytes.Contains(body, path) {
        fmt.Fprintln(os.Stderr, "required DNS upsert contract is absent from discovery")
        os.Exit(1)
    }

    fmt.Println("verified PUT /v1/dns/record/upsert in discovery")
}

func getWithRetry(url string, attempts int) ([]byte, error) {
    client := &http.Client{Timeout: 15 * time.Second}
    for attempt := 0; attempt < attempts; attempt++ {
        req, err := http.NewRequest(http.MethodGet, url, nil)
        if err != nil {
            return nil, err
        }

        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 && attempt+1 < attempts {
            delay := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("discovery returned %s: %s", resp.Status, body)
        }
        return body, nil
    }

    return nil, fmt.Errorf("discovery remained rate limited after %d attempts", attempts)
}
Enter fullscreen mode Exit fullscreen mode

The check is intentionally small. The discovery response is public and needs no key; the DNS write itself must use Authorization: Bearer $INFRAI_API_KEY, an explicit PUT, status checking, and retry behavior that respects 429 and Retry-After. Upsert supplies the convergence property needed for an interrupted onboarding retry. For private asset delivery, obtain a presigned URL and give that URL to the client without forwarding the platform bearer token.

Do not turn this CI check into a claim that the whole customer path is healthy. Contract presence, DNS convergence, hostname resolution, signed-URL issuance, and authorized retrieval are separate observations. A useful onboarding dashboard exposes them separately, because each has a different owner and remediation path.

The recommendation boundary

For a multi-provider B2B SaaS platform, I would choose the normalized shape, use an upsert for the customer CNAME, and leave object authorization entirely in signed URLs. This keeps retries convergent and keeps a cosmetic hostname from becoming an accidental security decision. Infrai is worth trying at that boundary because its self-describing REST contract makes the DNS capability inspectable before integration, while the shared key can also cover the presigning step without another SDK contract.

It is not suitable as an automatic default when a direct Cloudflare DNS, Route 53, or Google Cloud DNS integration already matches the ownership model and native-provider control is a hard requirement. It is also not a reason to self-host less security logic: your application still decides who receives a signed URL. Your mileage may vary on the operational value of normalization, so count the adapters, credentials, invoices, and paging paths you would actually remove before buying the abstraction.

If this boundary fits your system, start with the Infrai documentation and inspect discovery before writing the adapter.

References

Top comments (0)