With a custom domain flow, customers can use their own hostnames, such as app.customer.com, to serve your product. Claiming a hostname is an authorization step: the tenant who claims it receives its traffic and a valid certificate. If your platform accepts claims based only on DNS pointing at it, the hostname will go to the first tenant who requests it. This post explains the process from claiming to releasing a hostname.

System description

A tenant claims a hostname by showing they can update the customer's DNS zone. The platform then creates a binding between the hostname and the tenant, and the edge only serves the hostname while this binding is active. A scheduled job keeps checking the proof until the binding is released.

Architecture choice

Architecture choice

There are two main decisions. The first is how the customer proves they control the hostname.

Dedicated DNS challenge

The platform creates a random token for the claim, and the customer adds it as a TXT record at an underscore-prefixed name, like _yourplatform-challenge.app.customer.com. Setting up the routing record is a separate step.

Use this when:

  • Your edge targets are shared or static, so the routing record does not identify a tenant

  • Customers need to verify before cutting traffic over, as in a migration from another provider

  • You onboard apex domains, which carry A records instead of CNAMEs

Trade-off: the customer needs to publish and maintain a second DNS record. You can expect support tickets both during setup and if a DNS cleanup later removes the record.

Routing record as proof

The CNAME record the customer sets up for routing also acts as proof. Pointing app.customer.com at a tenant-specific target shows the same DNS write access as a TXT challenge.

Use this when:

  • Every tenant gets a routing target assigned by the platform and unique to them

  • Onboarding has to be a single DNS change

Trade-off: this method only works if each tenant has a unique target. If the target is shared, it only proves that traffic reaches your platform, so any tenant could claim a name that already points there. Apex domains usually do not provide a unique target, so they still require the TXT challenge.

Common middle ground: routing-record proof for subdomains with unique targets for each tenant, and a TXT challenge for apex domains and migrations.

The second decision is whether the proof needs to remain after the domain is live.

One-shot verification

The challenge is verified when the claim is made, and the customer can delete the record afterwards. The binding remains valid until it is released.

Use this when:

  • The customer's DNS is managed by a team you cannot ask for standing records

  • The hostname serves content where drift is acceptable

Trade-off: if the domain expires or changes owners, your platform will keep routing it to the old tenant until someone notices.

Standing verification

The record remains published, and a scheduled job checks it for every active binding. If the record is missing or its value changes, the tenant receives a warning and a grace period before routing stops. The re-check can detect a missing or changed record, but it does not prove the tenant can still update the DNS zone.

Use this when:

  • Custom hostnames carry sign-in pages or emailed links

  • Tenants churn often enough that stale bindings accumulate

Trade-off: customers may delete records that seem unused. Re-verification should include a grace period and a warning email before routing stops, to avoid causing an outage.

Default to standing verification. Without it, if a domain changes owners, it will still route to the old tenant. The rest of this post assumes you are using a dedicated challenge and standing verification.

Golden path

Tenant admin submits the hostname → API normalizes the name and records the claim → tenant admin publishes the challenge record → verifier resolves the challenge from public resolvers and creates the verified binding → certificate manager orders the certificate for the verified binding → edge starts routing when the binding and certificate are both ready → a scheduled job re-verifies active bindings → release turns off routing before the name can be claimed again

Related patterns:

Core design

Claim and binding records

A claim and a binding are separate rows. The claim is short-lived: it records the tenant, the hostname, the challenge token hash, and an expiry.

The binding is created when a proof succeeds. Minimum fields:

  • binding_id: opaque identifier

  • tenant_id: the tenant that receives this hostname's traffic; immutable, a change of tenant is a release plus a new claim

  • hostname: the canonical form (lowercase, A-label, no trailing dot)

  • is_wildcard: whether this binding covers one label under the parent (*.customer.com matches app.customer.com, not a.b.customer.com); wildcard claims store the parent name in hostname

  • display_hostname: what the customer typed, display only

  • state: verified, active, suspended, released

  • challenge_token_hash: the value the standing re-check expects

  • routing_target: the per-tenant target the customer points DNS at

  • certificate_ref: the certificate order or object for this hostname

  • verified_at, last_checked_at

  • created_by: the admin who filed the claim

The unique constraint is on (hostname, is_wildcard) and holds from the moment the binding is created until the release cooldown ends. The wildcard-overlap check runs in the same transaction that creates the binding.

Claim

Normalize the submitted name before using it as a lookup key:

  1. Reject anything that is not a bare hostname: schemes, ports, paths, IP literals

  2. Run the same domain-to-ASCII conversion browsers use, which lowercases and converts Unicode labels to xn-- A-labels

  3. Strip a single trailing dot

  4. Enforce label shape: 63 octets per label, 253 total, no empty labels

  5. Refuse names at or above the public-suffix boundary

The serve path runs the same function before binding lookup.

Filing a claim returns the challenge record and the routing target. The verifier checks the exact challenge name using public recursive resolvers and compares the token to the stored hash. If the proof is successful, the binding is created. If two tenants try to prove the same unclaimed name at the same time, the first one to succeed wins, and the second fails due to the unique constraint. If a name is already bound, a new claim is treated as a transfer, which is handled during release.

For a wildcard claim (*.customer.com), strip the leading *. first and normalize the parent name; the wildcard flag is its own field. At the edge, an exact binding wins over a wildcard binding for the same name.

Serve

The edge serves a list of hostnames with active bindings, and uses SNI to select the right certificate during the handshake. For each request, the edge reads the requested hostname (from the Host header in HTTP/1.1 or :authority in HTTP/2 and HTTP/3), looks up the binding, and passes the tenant_id and binding_id to the application. The platform's shared domain is also included in the serving set, with its own certificate.

Release

Release happens in this order:

  1. The binding leaves active and the edge drops the hostname from the serving set

  2. Certificate renewal stops

  3. The row enters a cooldown, during which the name stays reserved; the cooldown does not end until every edge node has stopped serving the name

  4. A new claimant with a fresh proof can skip the rest of the cooldown, but their binding does not go live until the edge has stopped serving the old one; this is how a legitimate migration gets through

A claim for a name that is already bound works the same way: when the new claimant's proof succeeds, the old binding is released and its tenant is notified.

Suspended bindings keep their reservation on the name and return to active on a successful re-check.

Minimal API shape

POST   /tenants/{id}/domain-claims               → 201 { claim_id, challenge_record, routing_target }
GET    /tenants/{id}/domain-claims/{cid}         → { hostname, state, expires_at, binding_id? }
POST   /tenants/{id}/domain-claims/{cid}/verify  → 202 (triggers a check)
GET    /tenants/{id}/domains/{bid}               → { hostname, state, last_checked_at }
DELETE /tenants/{id}/domains/{bid}               → 204 (starts the release sequence)

Clients submit a hostname; every other field of the binding comes from the server.

Security properties of the DNS challenge

A passed challenge proves:

  • The claiming tenant controlled the hostname at the moment of verification

  • Someone could write to the customer's DNS zone, or to a zone the challenge name was delegated to

It does not prove:

  • That the write access still exists after the check

  • That the writer is the domain's registrant; a hosting panel or DNS provider account with write access passes the same check

  • That the customer got the certificate themselves: once traffic routes to your edge, your platform passes the CA's challenge on its own

A managed certificate only shows that traffic reaches your edge. Your own challenge is what ties the name to a tenant, which is why certificate orders wait for a verified binding.

Threat model

Baseline assumptions

  • Tenant admins are authenticated; the hostnames they submit are untrusted input

  • The customer's DNS zone is outside your control and can change at any time after verification

  • The control plane derives tenant context from the session, not from the submitted hostname or the request's Host header

  • The edge terminates TLS and can rebuild its serving set from the binding registry

  • Standard infra controls such as TLS configuration, WAF, and database AuthN are assumed to be in place. This model focuses on the hostname-to-tenant binding lifecycle

A note on risk

This table is not a checklist. Focus on preventing the highest-impact failures first. Detection and response are acceptable where prevention is impractical.

Phase 1: Claim

Focus: Binding a hostname to the tenant that controls it

Asset

Threat

Baseline Controls

Mitigation Options

Risk

Tenant binding

Pre-claim squat: A hostname points at the platform before its owner registers it (DNS staged early, setup docs that say point first), and whichever tenant submits the name first receives the binding and its traffic

None

1. Fresh challenge: verify a platform-generated token published in the customer zone before any claim activates

2. Tenant-scoped record: put the claim identity in the record name or value, with a random token per claim

High

Onboarding availability

Claim parking: Any tenant files a claim for a hostname it cannot prove, and a claim that reserves the name blocks the real owner from onboarding for as long as the claim lives

None

1. Non-exclusive claims: let several tenants hold claims on one name and hand out the unique binding only on proof

2. Claim expiry: drop unproven claims after 72 hours 3. Rate limits: cap open claims per tenant

Medium

Certificate issuance

Cert in the claim window: Managed certificate automation answers the CA's challenge from the platform's own edge once traffic routes there, so a claim that never verified still yields a valid certificate for the name

CA domain-control validation

1. State gate: create certificate orders only for bindings in verified or active state

2. Registry-sourced orders: the issuance job reads the hostname from the binding row, never from the claim request

3. Issuance alerting: alert on any certificate order for a hostname without a verified binding

High

Binding uniqueness

Double binding: The claim path stores the hostname as typed while the serve path indexes what clients send (lowercase A-labels without a trailing dot), so the binding that verified is not the binding the serve path finds, and a second encoding of the same name is free for another claim

Exact-string uniqueness

1. One canonical form: run the same domain-to-ASCII conversion on both paths and key the uniqueness constraint on its output

2. Display split: keep the typed form as display metadata only

Medium

Overlapping claims

Wildcard shadow: A tenant claims *.customer.com while another tenant holds a verified app.customer.com, and most-specific-first routing sends that name to one tenant and its sibling names to the other

Exact-hostname uniqueness

1. Parent proof: verify wildcard claims at the parent name

2. Same-owner parent: reject wildcard and exact bindings under one parent that belong to different tenants

3. Claim floor: refuse claims at or above the public-suffix boundary

Medium

Verification integrity

Forged DNS answer: An attacker who can poison or intercept the verifier's DNS resolution makes the expected token appear for a name they do not control, and the binding verifies

Random per-claim token

1. DNSSEC validation: for signed zones, an answer counts as proof only if validation succeeds

2. Independent paths: corroborate first-time claims and transfers from a second network vantage point

3. Indeterminate on error: a resolver failure is neither a pass nor a fail

Medium

Phase 2: Serve

Focus: Routing every request through the verified binding

Asset

Threat

Baseline Controls

Mitigation Options

Risk

Tenant routing

Host-picked tenant: The application resolves the tenant from the request authority before checking that the hostname is a registered binding, so any client-controlled value selects the tenant context

Edge terminates TLS per hostname

1. Binding-first dispatch: reject a request whose Host has no active binding before any tenant code runs

2. Header hygiene: strip or overwrite forwarded host headers between edge and application so internal services see only the binding's hostname

3. Registry-sourced links: build password-reset and invite links from the binding record's hostname

High

Unbound hostnames

Catch-all serving: A default virtual host or a wildcard platform certificate answers for names with no binding, so any hostname pointed at the platform serves content and completes TLS without a claim

None

1. Refuse the handshake: abort the TLS handshake for SNI values without an active binding

2. 421 on established connections: return 421 Misdirected Request for an unknown authority, with no tenant content

Medium

Connection reuse

Per-connection tenant: The edge resolves the tenant once from the SNI and caches it on the connection, so a reused connection carries requests for a different bound hostname to the first tenant's application

None

1. Per-request lookup: resolve the binding from each request's authority, whatever SNI opened the connection

2. Coverage check: return 421 when the connection's certificate does not cover the requested Host

Medium

Phase 3: Drift and release

Focus: Catching ownership changes and cleaning up on release

Asset

Threat

Baseline Controls

Mitigation Options

Risk

Released hostname

Leftover-record re-claim: The customer's routing record stays up after the tenant releases the hostname, and a claim flow that accepts the routing record as proof activates the name for a different tenant

None

1. Cooldown: hold released names in a reserved state for a set period, with a fresh proof as the early exit

2. Re-claim alerting: alert when a hostname released by one tenant is claimed by another

High

Active binding

Silent transfer: The customer's domain expires or is sold, and the binding keeps routing the name to the old tenant because nothing re-checks control after the first proof

One-time verification at claim

1. Scheduled re-verification: re-resolve the challenge for active bindings

2. Grace window: warn the tenant and keep routing through a short window before suspending

3. Watch the pointing record: alert when the hostname stops resolving to your target

Medium

Certificate and edge state

Teardown gap: Release removes the registry row but the certificate keeps renewing or an edge node serves from a stale copy of the set, so the platform still answers for a hostname it released

Registry drives the serving set

1. State-coupled renewal: renew only for active bindings

2. Reissue shared certificates: cut the released name from any multi-name certificate at release

Medium

Lifecycle audit

Attribution gap: A hostname moves between tenants through a support transfer, and the record shows the new owner but not who approved it or which proof was checked

State transitions logged

1. Full transitions: record the actor, both states, and the proof that satisfied the check on every change

2. Transfer reason: require a ticket reference on manual transfers

Low

If you use the routing record as proof

If the CNAME that routes traffic is also the ownership proof, the trade-offs shift:

  • The pre-claim and post-release squats come back unless every routing target is unique per tenant; with a shared target, any tenant can claim any name that already points at you

  • Apex domains usually resolve to shared IPs that do not name a tenant, so they still need the TXT challenge

  • The routing record is also the only warning you get that ownership changed; when it stops pointing at your target, treat the binding as unverified

  • A transfer between two of your own tenants changes nothing in DNS, so there is nothing to check; require a TXT challenge or a support ticket for transfers

FAQs

Is a CNAME pointing at our platform enough to prove the customer owns the hostname?

It proves that whoever controls the zone wants traffic to reach your platform. Whether it also identifies the tenant depends on the target. A record pointing at a platform-assigned target unique to the tenant proves the same thing a TXT challenge would; a record pointing at a shared target does not say which tenant the customer meant. Shared targets and apex domains need the TXT challenge, and so does any re-claim of a previously bound name.

What happens when a customer deletes the challenge record after setup?

Under one-shot verification, nothing; the binding stays valid until released, and nobody notices the record is gone. Under standing verification, the next scheduled check fails and a warning email goes out; the binding stays active through a grace period before routing stops. Pick the grace period with support in mind: long enough that a DNS mistake can be fixed after the warning, short enough that a domain that actually changed hands stops serving within days.

Verification checklist

  • Claim intake

    • A name claimed with mixed case or Unicode labels resolves to the same binding row the serve path looks up

    • Two tenants cannot hold bindings for the same canonical hostname

    • A pending claim by one tenant does not stop another tenant from claiming and proving the same hostname

    • A claim for co.uk or another public suffix is rejected at intake

    • Publishing the correct token after the claim expired does not create the binding

  • Proof of control

    • A challenge token issued to one tenant does not verify a claim by another tenant, even for the same hostname

    • The verifier resolves the exact challenge name; a token published at the parent zone does not pass a child claim

    • A wildcard claim fails while another tenant holds a verified hostname one label below the wildcard's parent

    • Concurrent proofs for *.customer.com and app.customer.com from different tenants create at most one binding

  • Serving

    • A TLS handshake for an unclaimed hostname fails with no certificate presented

    • Reusing a connection opened for one hostname to request an unbound hostname returns 421

    • Requests for two bound hostnames on one reused connection each reach their own tenant

    • The application receives tenant_id from the routing layer; removing every Host-header read below the router breaks nothing

    • Password-reset and invite links carry the binding record's hostname even when the request arrived with a different Host

  • Certificates

    • Pointing DNS at the platform without completing verification produces no certificate order

    • Suspending or releasing a binding stops its renewal orders

    • After a release, the certificate presented for surviving hostnames on a shared multi-name certificate no longer lists the released name

  • Lifecycle

    • In the release sequence, the edge stops answering for the hostname before the name becomes claimable

    • Re-claiming a hostname another tenant released fails until a new challenge token is published and verified

    • The old tenant's standing token cannot move an active hostname to a different tenant; a transfer verifies only against the claimant's fresh challenge

    • After a transfer verifies, the new tenant receives no traffic until the old edge mapping is gone

    • Deleting the challenge record for an active binding produces a warning to the tenant before routing stops

    • When every configured resolver times out, active bindings stay active and an alert fires for the verifier itself

    • Every state transition records the actor and the old and new states

Implementation & Review

The full threat model matrix, architectural diagrams, and a printable verification checklist for this pattern are available in the Secure Patterns repository. Use these artifacts to guide your design reviews and internal audits.