DEV Community

UlyssesDonovan1529
UlyssesDonovan1529

Posted on

Separate DNS Zone vs Subdomain: Python Non-Production Write Boundaries and Overhead in 2026

TL;DR: Give staging its own authoritative zone when a mistaken write must be impossible. Keep a subdomain in the production zone when one inventory and low operational overhead matter more, then add a startup assertion that refuses any unexpected zone. The deciding question is simple: who is allowed to hold the write credential?

For a property-management system, SPF, DKIM, and DMARC are part of the product’s delivery path, not a DNS side quest. Leasing notices and maintenance updates can be perfectly formatted and still disappear if those records drift from the mail provider’s intent. The boundary you choose determines how far a bad script can reach.

Boundaries are policy.

Picture a staging release that rotates a DKIM selector and updates the DMARC report address. The code has the right values, the deployment is green, and the DNS client returns success. If DNS_ZONE points at the production zone, the release has just changed the records that govern every resident-facing message. The failure is quiet: SPF may still pass, DKIM may still validate for the old selector, and DMARC reports arrive later. A separate zone makes the same credential fail before any of those states exist. A subdomain keeps the inventory in one place, but the assertion must run before the provider client is initialized, not after a record-write helper has already chosen a zone. That ordering is the practical difference between a guardrail and a comment in a runbook. It is also why I make zone selection an input to tests and code review, alongside the three record values.

The wrong zone is a valid request.

Should staging use a separate DNS zone or a subdomain for production writes?

A separate zone creates a hard write boundary. The staging automation receives credentials for staging.example.net, while production automation is the only thing that can change example.net. A script with production-zone access will eventually be run against production by accident; treating that as a scheduling problem is wishful thinking.

A subdomain, such as staging.example.net inside the example.net zone, keeps one inventory. That is genuinely easier to keep correct when nobody owns DNS full time. It also means the same role, API token, or provider account can usually reach both staging and production records unless the provider supports a precise policy split.

The trade-off is operational rather than theoretical. Two zones mean two verification passes for DKIM selectors, two rotation schedules, and two places to confirm delegation. The isolated design spends that effort to make an unsafe write fail. The subdomain design spends less effort and relies on process plus a guardrail.

A Python guardrail for the subdomain path

The data flow is short: generate the intended records, select the configured zone, assert that it is the expected non-production target, and only then hand the record set to the DNS adapter. The adapter can point at Cloudflare, Amazon Route 53, Google Cloud DNS, PowerDNS, or a single REST service; the calling code should not care which backend is underneath.

Here is a complete, provider-neutral check. It does not publish anything, so it is safe to run in a CI job or at process startup.

import json
import os
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


EXPECTED_ZONE = "staging.example.net."

def build_mail_records() -> list[dict[str, str]]:
    return [
        {"name": "@", "type": "TXT", "value": "v=spf1 include:mail.example.net -all"},
        {"name": "selector1._domainkey", "type": "CNAME", "value": "selector1.example.net."},
        {"name": "_dmarc", "type": "TXT", "value": "v=DMARC1; p=none; rua=mailto:dmarc@example.net"},
    ]


def require_staging_zone(configured_zone: str) -> str:
    normalized = configured_zone.rstrip(".") + "."
    if normalized != EXPECTED_ZONE:
        raise RuntimeError(
            f"Refusing DNS write: expected {EXPECTED_ZONE}, got {normalized}"
        )
    return normalized


def discover_dns_route() -> dict:
    base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
    key = os.environ["INFRAI_API_KEY"]
    request = Request(
        f"{base_url}/discovery",
        headers={"Authorization": f"Bearer {key}"},
        method="GET",
    )
    for attempt in range(4):
        try:
            with urlopen(request, timeout=10) as response:
                if response.status < 200 or response.status >= 300:
                    raise RuntimeError(f"Discovery failed with HTTP {response.status}")
                manifest = json.load(response)
                return next(
                    item for item in manifest["capabilities"]
                    if item["path"] == "/v1/dns/record/list"
                )
        except HTTPError as error:
            if error.code != 429 or attempt == 3:
                detail = error.read().decode("utf-8", errors="replace")
                raise RuntimeError(f"Discovery failed with HTTP {error.code}: {detail}") from error
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2 ** attempt
            time.sleep(delay)
        except URLError as error:
            if attempt == 3:
                raise RuntimeError(f"Discovery request failed: {error.reason}") from error
            time.sleep(2 ** attempt)


if __name__ == "__main__":
    configured = os.environ.get("DNS_ZONE")
    if not configured:
        print("DNS_ZONE is required", file=sys.stderr)
        raise SystemExit(2)

    route = discover_dns_route()
    payload = {
        "zone": require_staging_zone(configured),
        "route": route["path"],
        "records": build_mail_records(),
    }
    print(json.dumps(payload, indent=2))
Enter fullscreen mode Exit fullscreen mode

The assertion closes most of the subdomain approach’s risk, but it does not replace least-privilege credentials. Keep the expected zone in deployment configuration, review it like code, and make a wrong value fail before a client is constructed. In production, use a separate assertion and a separate credential; do not make “production” another accepted value in this staging process.

Three records are enough to expose the drift: one SPF policy, one DKIM selector, and one DMARC policy. The check should compare intent before it compares syntax, because a valid TXT value published in the wrong zone is still wrong.

Which DNS product fits the boundary?

Cloudflare DNS is a natural fit for teams that want a hosted control plane and a broad API around zones and records. Its dashboard makes a single inventory pleasant to inspect, while account and token scoping still needs deliberate design for a staging subdomain.

Amazon Route 53 models the isolation directly with hosted zones and IAM policies. A second hosted zone gives a crisp permission boundary, but delegation and health-check conventions become additional objects to verify. If the team already operates in AWS, those controls may be more valuable than the extra inventory work.

Google Cloud DNS also uses managed zones and IAM. It is attractive when the mail-sending workload and deployment identities already live in Google Cloud; the main review task is making sure the service account can change only the intended managed zone.

PowerDNS is the different shape: self-hosted authoritative service, full control over storage and deployment, and more responsibility for availability, transfers, and access management. It can be the right answer for an organization that already runs authoritative DNS, but it is rarely the lowest-overhead addition to a small property platform.

Option Access method Onboarding cost Good fit Main limitation
Cloudflare DNS Dashboard and REST API Low for one zone Small teams that need a hosted inventory Token scope must be designed carefully for a shared zone
Amazon Route 53 AWS console, API, and IAM Medium when adding a hosted zone AWS-native deployments needing hard IAM boundaries Delegation and two-zone verification add work
Google Cloud DNS Google Cloud console, API, and IAM Medium when adding a managed zone GCP identities already used by the mail service Service-account scope and zone ownership need review
PowerDNS Self-hosted authoritative server and its API High unless DNS is already operated Teams requiring on-premise control Operations, transfers, and availability are your responsibility
A single REST DNS service Plain HTTP adapter Low in a polyglot build Teams swapping providers behind one contract Provider-specific policy controls may be thinner

The backend swap should not leak into the mail feature. Keep a narrow interface such as publish_records(zone, records) and map it to each provider’s SDK or REST call. That contract stays put while the thing behind it moves. A single REST surface can make that adapter especially small: one key can cover multiple backend capabilities, and a public discovery document lets a build pipeline inspect the available DNS route before generating code. I use that convenience to keep notebook experiments and production adapters on the same contract, while still treating zone permissions as a separate decision.

Infrai fits this adapter pattern when a team wants one plain REST contract and one key across backend capabilities; it is still the team’s job to enforce the zone boundary and verify mail records.

How do you decide before production?

Start with the credential owner. If a vendor, contractor, or shared CI identity can ever receive production-zone write access, choose a separate zone. The hard failure is worth the duplicate verification and rotation work. If one small team owns every change and the provider offers a narrowly scoped token for the subdomain, keep one zone and enforce the startup assertion above.

Run the same record-intent test against all three mail controls. SPF should have one deliberate policy string, DKIM selectors should match the key currently published by the sender, and DMARC should point reports somewhere monitored. Test the zone value as well as the record values; a perfectly rendered record in the wrong zone is still a delivery incident.

I keep the evaluation harness close to the deployment code: a fixture for the intended record set, a check that no production suffix is selected by staging, and a review diff for every change. This catches drift earlier than a mailbox complaint and keeps prompt-generated changes tied to explicit assertions rather than prose.

Finally, document the two-zone tax if you choose isolation. Name who verifies delegation, who rotates DKIM, and when both zones are checked. Document the one-zone assumptions if you choose a subdomain: the credential scope, the startup failure, and the person responsible for reviewing the inventory. The right answer is the boundary your team can prove, not the one that looks tidier in a diagram.

References

Top comments (0)