Custom domains and automatic TLS, done properly
At some point a customer asks to use their own domain. It sounds like a DNS record and a certificate, and the demo version genuinely is. The production version has a security property that's easy to miss and expensive to miss: without it, one tenant can claim a hostname belonging to another and start receiving their traffic.
I built this for PandaStack apps and a review pass caught fourteen real bugs before it shipped, domain squatting among them. This is the design, the failure modes, and the ordering that makes them impossible rather than unlikely.
First, the wildcard certificate constraint
Before custom domains, you have a per-app hostname on your own domain covered by a wildcard certificate. That works, with one rule people design around too late: a wildcard covers exactly one label.
`*.example.com` matches `app-123.example.com`. It does not match `app-123.apps.example.com`. Every extra dot needs its own wildcard, and multi-level wildcards don't exist. So if your URL scheme has an infix — `<id>.apps.` or `<port>-<id>.preview.` — you either issue more certificates or restructure the hostname.
We serve apps at the apex of the suffix, `<app-id>.<suffix>`, specifically so a single-level wildcard covers them. That's not elegance, it's the certificate deciding the URL scheme. Learn it before building the router rather than after.
The flow, in the order that matters
Five steps. The ordering is the security design.
- The tenant claims a hostname. Record it as pending. It must not be routable yet.
- The tenant proves control — a TXT record at a name you specify, or a CNAME to your endpoint. The proof value is unique per claim and unguessable.
- You verify it by resolving DNS yourself, server-side. Only now does the hostname become active for that tenant.
- You obtain a certificate for it, typically via ACME with an HTTP or DNS challenge, and store it for the router to serve.
- Your edge terminates TLS, reads the SNI hostname from the handshake, looks up which app owns it, and proxies. Renewal runs on a timer well ahead of expiry.
Verification, and the mistakes in it
TXT-record verification is the standard approach: you generate a random token, the tenant publishes it at a known name under their domain, you resolve and compare. Four details separate a correct implementation from a plausible one.
- Resolve server-side, from your infrastructure. Anything the client reports about DNS is a claim, not evidence.
- Make the token unguessable and single-use. A predictable token means anyone can pre-publish it.
- Re-verify periodically. Domains change hands, and a hostname verified two years ago may now belong to someone else — one of the ways subdomain takeover happens.
- Cap the number of pending claims per tenant, and expire them. Unbounded pending claims are a memory-exhaustion vector and a way to squat the namespace, which is precisely the pair of bugs review caught in ours.
Tenant adds:
_pandastack-challenge.app.acme.com. TXT "ps-verify=9f2c...unique-per-claim"
app.acme.com. CNAME <app-id>.pandastack.app.
Platform, server-side:
resolve TXT _pandastack-challenge.app.acme.com
constant-time compare against the stored token for THIS claim
on match -> mark verified, create the routing entry, request a cert
no match -> stay pending; NEVER create a routing entry
Periodically:
re-resolve; if the record is gone, mark the domain unverified
and remove the route -- domains change ownersCertificates at multi-tenant scale
One certificate per customer domain, issued automatically and renewed automatically. Conceptually simple; operationally it's the part that pages you at 3am if you get it wrong.
- Rate limits are real. Public ACME providers cap issuance per domain and per account per week. A retry loop on a failing domain will consume your budget and block issuance for unrelated customers. Back off exponentially and stop retrying a domain that has failed repeatedly.
- Renew early. Thirty days before expiry, not three. Renewal fails for boring reasons — the customer changed DNS, their firewall blocks the challenge — and you want weeks of runway to notice and email them.
- Store certificates where every edge node can read them, and make sure a node that restarts loads current material rather than something stale from disk.
- Handle the handshake miss deliberately. A TLS connection for an unknown hostname must fail cleanly, not serve another tenant's certificate — which is both a mismatch error for the user and an information leak about who else you host.
- Monitor expiry independently of the renewal system. If renewal is broken, renewal will not tell you. An external check that alerts on any served certificate within fourteen days of expiry costs nothing and has saved every team that has one.
SNI routing: hostname to backend
At the edge, the request arrives, TLS begins, and the client's SNI extension names the hostname before any HTTP is exchanged. Your edge selects the certificate for that hostname, completes the handshake, then looks up which backend owns the hostname and proxies to it.
Two properties matter. The lookup must be fast and cached, because it's on every connection. And it must be updated atomically on deploys, since the backend behind a hostname changes every time a blue-green flip happens — the mapping updates in one step, and a request either goes to the old machine or the new one, never to a half-updated entry.
The same mechanism works beyond HTTP. We route managed Postgres connections by SNI too: `<id>.db.pandastack.ai` terminates TLS at a proxy that maps the hostname to a database VM. Same idea, different protocol.
The interaction with sleeping apps
If your platform sleeps idle apps, custom domains interact with that in a way that will surprise you. A custom domain is public, so it attracts the full internet background radiation: scanners, crawlers, certificate-authority validation probes, and whatever is looking for WordPress admin panels this month.
If any of that resets the idle timer, apps on custom domains never sleep. We shipped exactly that. The initial suspicion fell on the domain machinery, which was innocent — the cause was automated traffic keeping the timer warm, and the fix was a classifier distinguishing traffic that keeps an app awake from traffic that may wake a sleeping one. Those are separate decisions: a scanner should do neither, or you've given the internet a button that boots your infrastructure.
The checklist
If you're building this, these are the properties I'd hold a review to.
- A hostname is never routable before verification succeeds.
- Verification tokens are random, per-claim, and checked server-side.
- Pending claims are capped per tenant and expire.
- Verification is re-checked periodically, and losing it removes the route.
- Certificate issuance backs off and stops retrying persistently failing domains.
- Renewal starts weeks before expiry, with independent external expiry monitoring.
- An unknown SNI hostname fails cleanly rather than serving someone else's certificate.
- The hostname-to-backend mapping updates atomically on deploy.
- Automated traffic to a custom domain does not silently defeat idle sleeping.
Nine bullets, several days of work, and one of them is the difference between a feature and a vulnerability disclosure. The first one is that bullet.
Frequently asked questions
How do custom domains work for a multi-tenant app?
The tenant claims a hostname, proves they control it — typically by publishing a unique TXT record, alongside a CNAME pointing at your endpoint — and only after your infrastructure verifies that record server-side does the hostname become routable and get a certificate issued for it. At request time your edge terminates TLS, reads the hostname from the SNI extension of the handshake, selects that hostname's certificate, looks up which backend owns it, and proxies the request. Renewal runs on a timer well ahead of expiry.
What is the domain-squatting bug in custom domain implementations?
It happens when a claimed but not-yet-verified hostname is inserted into the routing table. If claiming implies routing, then any tenant can claim a hostname belonging to someone else — say a competitor's marketing subdomain — and start receiving traffic for it the moment that domain's DNS points at your platform. The rule that prevents it is absolute: a claim must never create a routing entry, and successful server-side verification must be the only thing that does. It is worth writing as an explicit invariant in review, because the code path that adds a route is easy to reach from the wrong place.
Why doesn't my wildcard certificate cover my subdomain?
Because a wildcard covers exactly one DNS label. A certificate for *.example.com matches app-123.example.com but not app-123.apps.example.com — every additional dot needs its own wildcard, and multi-level wildcards do not exist. This constraint should decide your URL scheme rather than being discovered after you build the router. Serving per-app hostnames at the apex of the suffix, as a single label, keeps one wildcard sufficient; adding a category infix such as .apps. or .preview. means issuing and managing more certificates.
How should automatic certificate renewal be monitored?
Independently of the renewal system itself, because if renewal is broken it will not be the thing that tells you. Run an external check that connects to your served hostnames, reads the presented certificate, and alerts on anything within about fourteen days of expiry. Start renewal roughly thirty days out rather than three, since renewals fail for mundane reasons like a customer changing DNS or a firewall blocking the challenge, and you want weeks of runway to notice and contact them. Also respect ACME rate limits: back off exponentially and stop retrying domains that have failed repeatedly, or one broken domain will consume the issuance budget that unrelated customers need.
Do custom domains stop an app from scaling to zero?
They can, and it is a subtle failure. A custom domain is a public hostname, so it attracts the full background noise of the internet — security scanners, crawlers, certificate-authority validation probes, and bots hunting for admin panels. If any of that resets the idle timer, apps on custom domains never sleep and the hosting bill looks exactly like always-on hosting. The fix is classifying traffic on two separate axes: whether a request keeps an already-awake app awake, and whether it is allowed to wake a sleeping one. Automated traffic should do neither, or anyone on the internet can force your machines to boot.
49ms p50 cold start. Fork, snapshot, and scale to zero.