www.theircompany.com went behind Cloudflare with a custom hostname and a real cert in about a minute. theircompany.com, the bare apex, would not proxy with a custom certificate at all. Proxying a custom-cert apex is an Enterprise SSL-for-SaaS feature, and Cloudflare Pro does not have it.
If you run a SaaS where customers bring their own domain, you will hit this exact wall. Here is why it exists and the small off-origin relay we stood up to get the capability back without paying for Enterprise. We built it from scratch in one 6-hour session and proved it live on a real customer domain, as part of a larger Cloudflare migration moving a multi-thousand-domain customer base behind Cloudflare-for-SaaS.
Why the apex breaks twice
The setup that works for www is Cloudflare-for-SaaS custom hostnames. The customer points www.theirdomain.com at your Cloudflare zone via CNAME, you register the custom hostname, Cloudflare issues and serves the cert, and traffic routes to your origin. Clean.
The apex breaks this in two ways at once. First, you cannot CNAME an apex in standard DNS. The root has to carry other records like the SOA and NS, and a CNAME has to be the only record on a name. Some registrars fake it with CNAME-flattening, but that points apex traffic at Cloudflare, and then you hit the second problem. Cloudflare Pro will not terminate a custom certificate for a customer apex that isn’t your own zone. That capability is SSL for SaaS at the apex, an Enterprise line item.
So on Pro you have a www that is fully behind the edge and an apex with nowhere good to go.
The first answer, and what it cost
We started with the obvious one: issue a Let’s Encrypt cert per apex and serve it from origin. IIS and Certify managed per-domain certs on the Windows origin box. That was literally our architecture going in.
It falls apart the moment you count domains. One LE cert per apex, provisioned by hand or by a per-domain job, across thousands of customer domains, is weeks of work and an ongoing renewal liability. It puts cert state on the origin for every domain you onboard.
The shape that works
The origin does not need to know about individual apex domains at all. You need one small thing in front that can terminate TLS for any apex on demand and bounce it to the www host that is already behind Cloudflare.
The topology:
www.customer.comgoes to a Cloudflare custom hostname (the existing, working path).customer.com(apex) gets an A record pointing at a relay you control, at a fixed IP.- The relay terminates TLS for whatever apex hit it and 301s to
https://www.customer.com.
The apex is only ever a redirect. Nobody is meant to serve content from the bare domain. They are meant to land on www, which Cloudflare already handles. The relay answers TLS for any apex name, then redirects, and the origin stays out of it entirely.
Caddy and on-demand TLS
The hard part of “terminate TLS for any apex” is the cert. You have thousands of apex names, you do not know them all in advance, customers are still being onboarded, and you do not want a provisioning step that has to run before each domain works.
Caddy’s on-demand TLS solves this. Caddy does not provision certs ahead of time. When a real inbound TLS connection arrives for a hostname it has no cert for, it issues one from Let’s Encrypt during the handshake and caches it. There is no per-domain config, no provisioning job and no list of domains to maintain. The first real connection to customer.com mints the cert, and the customer flipping their apex A record to the relay is the trigger. Onboarding a new apex is one DNS change on the customer side and nothing on ours.
We put the relay on a DigitalOcean droplet with a Reserved IP. That fixed address is what every customer’s apex A record points at, so it has to be stable and detachable from any single droplet. If the droplet dies you reattach the Reserved IP to a new one and every apex keeps resolving. The droplet is tiny. It terminates TLS and emits 301s.
The Caddyfile is short. A global on-demand TLS block, plus a catch-all site that redirects the bare apex to its www:
{ on_demand_tls { # ask endpoint that says yes/no for a given host, # so you are not issuing certs for arbitrary names ask http://localhost:9000/check }}
:443 { tls { on_demand } redir https://www.{host}{uri} permanent}The ask endpoint is the one piece you should not skip. On-demand TLS without a guard will attempt issuance for any hostname that resolves to your relay, which is an abuse and rate-limit footgun. The ask endpoint is a small local service that answers whether the requested host is one the relay should issue for, before Caddy talks to Let’s Encrypt. Point it at your real list of customer apexes and issuance stays bounded to domains you actually serve.
What bit us at migration scale
A lifecycle scan that probes every domain for relay TLS health will try to issue LE certs against apexes that have not been flipped to the relay yet. Those probes always fail, and they are slow. When we scanned all current domains, the scan timed out because it was attempting issuance against the ~37 domains whose apex A record still pointed at the old target. The fix is a precondition: only run the relay TLS probe if the apex A record already resolves to the relay IP. With that guard the scan ran 39 migrated domains in about 12 seconds and skipped the 37 that were not ready. Skipping the expensive probe when its precondition is unmet is obvious in retrospect and easy to miss when you only test the happy path.
The other one is Windows-specific and cost us a confused half hour. Running the relay probe from the origin box (Windows) failed with an SSPI error. Windows integrated auth tried to negotiate on an outbound connection to the relay, which is Caddy on Linux and wants nothing to do with SSPI. The scan has to run from an off-box vantage. If your origin is Windows and your relay is Linux, do not run relay health checks from the origin.
Error 1001 looks like DNS
When this class of setup goes wrong on the Cloudflare side, the symptom is Cloudflare error 1001, often reported as a “DNS Resolution Error” on mobile Safari. It looks like DNS because the user sees a DNS-shaped message, and the DNS is usually fine. In an SSL-for-SaaS context, error 1001 means the custom hostname was not registered in your Cloudflare zone. The CNAME points at Cloudflare correctly, but the custom hostname object Cloudflare needs to route the connection was never created. The fix is to register the custom hostname, not to touch DNS. We lost time treating it as a DNS problem the first time a customer hit it.
Before you flip an apex
Check two things before you point a customer’s apex at the relay. Confirm the A record resolves to the relay’s IP and not a stale value left over from before the flip. Confirm the hostname is on the ask endpoint’s approved list before the first TLS connection, because that first connection is what triggers issuance. Get those two right and the cert mints itself on the first real connection.
Related
- The Migration Flip That Took Down a Live Customer (and the Blast Radius We Could Measure in 12 Seconds): the outage that happens when apex redirects aren’t in place before a domain is flipped
- The Cloudflare-for-SaaS + IIS Gotcha Nobody Documents: SNI vs Host Header: the www custom-hostname path this relay redirects toward
- The GoDaddy “No Public API” Myth: Reverse-Engineering the DCC Internal DNS Endpoint: the DNS migration that sets the apex A records pointing at this relay
- Our whole 1,200-domain provider split died instantly: keep a second DoH provider on hand: DNS resilience when thousands of apex records point at one fixed IP
- A three-reviewer naming cascade for the string every domain will CNAME to forever: choosing the CNAME string that the www side of these same domains points at