Route 53 and ACM: Custom Domains That Must Prove Delegation Before They Go Live

Smarter lets an account serve its chatbots from a domain of its own, and it uses Amazon Route 53 and AWS Certificate Manager (ACM) to check that the account really controls that domain before anything is served on it. This post covers how that check works, what it protects against, what it costs to run, and whether the same idea belongs in your own multi-tenant platform.

A Smarter CustomDomain is a resource like any other, a short YAML manifest with a domainName, applied with the CLI or the API. Applying it costs nothing and proves nothing. Anyone can type llmclients.example.com into a manifest. What matters is what the platform does next, and in Smarter the answer is: nothing that faces the public, until the domain’s owner has done something only the domain’s owner can do.

The claim and the proof

A custom domain is a claim that says “this name is mine, serve my chatbots on it.” A platform that accepts the claim at face value will create DNS records, request certificates and route traffic for names its tenants don’t own. At best that wastes resources. At worst it lets one tenant put a chatbot, with a browser padlock, behind a name that someone else’s users trust.

Smarter turns the claim into something checkable by asking for delegation rather than for a pasted record. When an account deploys a CustomDomain, the platform creates a Route 53 hosted zone for it and tells the owner its name servers. The owner adds those NS records at their own DNS provider. From then on, Smarter answers for that domain, and only because the domain’s owner said so in the one place that the rest of the internet listens to.

The deploy step, in register_custom_domain.py, creates the zone and the certificate request, then hands off to verification rather than waiting.

# create a DNS zone for the custom domain
zone, _ = infrastructure.dns.get_or_create_zone(domain_name)

# create a certificate for the custom domain
certificate_id, _ = infrastructure.certificates.get_or_create_certificate(domain_name)

# create the DNS records that validate the certificate. The provider can only read them once the
# customer has delegated the domain's NS records to the DNS zone, which may take a day,
# so rather than wait here, verify_custom_domain checks the NS records, the certificate and
# https, and checks again later until they work.
infrastructure.certificates.create_validation_records(certificate_id)

Two checks, in order

A CustomDomain is Verified only when two things are true, and verify_custom_domain.py checks them in this order.

  1. The domain’s public NS records include one of the name servers of its Route 53 zone.
  2. The domain’s ACM certificate has the status ISSUED.

The first check asks the public DNS, not Route 53, because the question is what the world sees.

dns_ns_records = {normalize_name(rdata.to_text()) for rdata in dns.resolver.query(domain_name, "NS")}
...
name_servers = infrastructure.dns.get_name_servers(hosted_zone_id)
for i, name_server in enumerate(name_servers, start=1):
    ...
    if normalize_name(name_server) in dns_ns_records:
        return True
return False

The second check is where the design earns its keep. The certificate is requested with DNS validation, from acm.py.

response = self.acm.request_certificate(
    DomainName=domain_name,
    ValidationMethod="DNS",
    SubjectAlternativeNames=[f"*.{domain_name}"],
)

ACM issues it only after it finds the validation record that Smarter wrote into the new hosted zone. And ACM can only find that record through public DNS, which reaches the hosted zone only if the delegation is real. So an issued certificate is a second, independent witness. Amazon’s own validation, not Smarter’s resolver, confirms that the zone answers for the domain. The NS check alone could be fooled by a stale cache or a resolver with a different view. The two together are hard to fake without actually owning the domain.

Flow of a Smarter custom domain from claim to proof. An account applies a CustomDomain manifest, Smarter creates a Route 53 hosted zone, an ACM certificate request with DNS validation and its validation records, and the owner adds the zone's NS records at their DNS provider. Two independent checks follow in order. First, the domain's public NS records include one of the zone's name servers, and then ACM reports the certificate ISSUED. Only when both pass is the domain Verified, custom_host returns a value, and the deploy task creates an A record, a single-host Ingress and a cert-manager certificate.

The order matters for the person waiting, too. Whichever step is incomplete is written to the domain’s verification_message, so the owner sees “add these NS records” before they ever see “certificate pending”, and doesn’t chase the wrong problem.

if not _ns_records_verified(hosted_zone_id, domain_name, task_id):
    ns_records = infrastructure.dns.get_name_servers(hosted_zone_id)
    return (
        f"The NS records of {domain_name} are not delegated to Smarter yet. "
        f"Add these NS records to your root domain's DNS settings: {', '.join(ns_records)}"
    )
certificate_status = _certificate_status(domain_name)
if certificate_status != CERTIFICATE_ISSUED:
    return f"The TLS certificate of {domain_name} is not issued yet. Its status is {certificate_status}."
return None

What verification unlocks

The gate is one property on the LLMClient model, in llmclient.py. A chatbot has no custom host at all until its domain is Verified.

@property
def custom_host(self) -> Optional[str]:
    if self.custom_domain and self.custom_domain.is_verified:
        domain = f"{self.rfc1034_compliant_name}.{self.custom_domain.domain_name}"
        SmarterValidator.validate_domain(domain)
        return domain
    return None

Everything downstream reads that property. The chatbot’s public hostname and URL fall back to its default host while custom_host is None. The deploy task in deploy_custom_api.py does nothing without it.

# custom_host is None unless the llmclient has a verified custom domain.
custom_host = llmclient.custom_host
if not llmclient.deployed or not custom_domain or not custom_host:
    ...
    return None

Once it has one, it writes an A record for <llmclient>.<domain> into the customer’s delegated zone, as a copy of the platform API domain’s A record, and applies a Kubernetes Ingress for exactly that host, which asks cert-manager for a certificate of its own. No A record, no Ingress and no certificate request exists for a custom host until both checks have passed. I like that the rule lives in one property rather than in a series of if statements spread across tasks, because then there is no code path that can forget it.

Waiting without blocking

Delegation is a human step at someone else’s DNS provider, and it can take minutes or a day. The task doesn’t sleep in a Celery worker while that happens. Each run checks once, and if a step is incomplete, it schedules itself again.

if attempt + 1 < max_attempts:
    if custom_domain and not custom_domain.is_verified:
        custom_domain.set_verification_status(Status.VERIFYING, blocker)
    # check again later, without blocking this worker while DNS changes.
    verify_custom_domain.apply_async(
        kwargs={
            "hosted_zone_id": hosted_zone_id,
            "sleep_interval": sleep_interval,
            "max_attempts": max_attempts,
            "attempt": attempt + 1,
            "task_id": task_id,
        },
        countdown=sleep_interval,
    )
    return None

The defaults are every 30 minutes for 24 hours, 48 checks in all. Success or failure, the account that owns the domain gets an email that says which, and why. After the last attempt the domain is Failed, with the blocking step recorded, and the owner can deploy it again once they’ve fixed their DNS.

Flowchart of the verify_custom_domain task. Each attempt checks whether the NS records are delegated, then whether the certificate is ISSUED. If both pass, the domain is Verified and the owner is emailed. If either fails, the blocking step is written to verification_message, the status is set to Verifying and the task reschedules itself with a 30-minute countdown. After attempt 48, 24 hours by default, the domain is Failed and the owner is emailed.

How large a hole does this close?

On a 1-10 scale I’d put this at a 7 for a platform that serves tenant content on tenant-supplied names. The attack it prevents is quiet rather than dramatic. A tenant names a domain they don’t control and gets the platform to build hosting for it. Without delegation, public DNS still points the name elsewhere, so the immediate damage is limited. But a platform that skips the check has already done the expensive and trust-bearing parts (a certificate request, an Ingress, routing rules) for a name it has no right to, and it is one misconfigured record away from serving it. Proof of control before provisioning is the same rule that certificate authorities and every large SaaS custom-domain feature follow, for the same reason.

It is not a 10 because it protects the platform’s provisioning, not the domain itself. Once a domain is delegated, whoever controls the account controls what is served on it, which is the intended result.

What it costs to run

The money is small. Each custom domain gets a Route 53 hosted zone, which AWS bills at fifty cents a month for the first 25 zones, plus query charges. ACM public certificates are free. The verification task is a few DNS lookups and API calls every half hour for at most a day.

The administrative burden is mostly zero, with two exceptions worth knowing.

  • Deleting a CustomDomain deletes the Smarter resource but not its hosted zone. That is the safer default, because a zone deleted while the customer’s registrar still delegates to its name servers is a classic dangling-delegation risk. But it means an operator removes old zones by hand, after confirming the delegation is gone.
  • Verification runs when a domain is deployed, or when an operator runs the verify_custom_domain management command. Nothing re-checks a Verified domain on a schedule. If a customer later moves their NS records elsewhere, Smarter’s zone simply stops answering for the domain, which fails safe, but the console still shows Verified until someone checks again.

UX and stability

For the domain owner, the cost is one DNS change and some patience. The flow is honest about where it is. The web console’s Custom Domains page shows a Verifying, Verified or Failed badge whose tooltip carries the blocking step, and the email says what to do next. The NS records to add are in the describe output, so there is nothing to look up.

For the platform, the design adds no load on the request path. Verification runs on the infrastructure worker queue, never inside a web request, and a failed check is a status, not an exception. A domain that never verifies costs a hosted zone and a few dozen failed lookups, and nothing else.

Should you build it?

Yes, if your platform serves anything on names that tenants supply. Asking for NS delegation is more work for the tenant than asking for a TXT record, but it buys you something a TXT record doesn’t. From that point on, you can create the DNS records you need without asking the customer again, including the records your certificate authority needs to validate. Pair it with a certificate that can only be issued through that zone, and the issued certificate becomes your proof.

If your tenants can’t or won’t delegate a subdomain, a TXT ownership record followed by a CNAME is the common alternative. The principle is the same either way. Prove control first, then provision.

Takeaways

  • Treat a custom domain as a claim, and provision nothing public until it is proven.
  • Ask the public DNS, not your own DNS service, whether the delegation exists.
  • Use a DNS-validated certificate as a second witness, since it can only be issued if the delegation is real.
  • Put the rule in one place, such as a property that returns nothing until verification passes, so that no code path can skip it.
  • Check asynchronously and re-schedule, rather than sleeping in a worker, and tell the owner which step is blocking.
  • Keep hosted zones after a domain is removed, and clean them up only once the delegation is gone.

Closing thoughts

The CustomDomain check is a small amount of code wrapped around a simple idea. Let the domain’s owner prove ownership in the place that everybody else trusts, and let a certificate authority confirm it. Everything after that, the A record, the Ingress, the chatbot’s URL, waits on one property that says whether the proof is in.