---
title: "An Off-Origin Caddy Relay for Bare-Domain SSL (Because Cloudflare Pro Won't Proxy Your Apex)"
canonical: https://dxdev.com/blog/off-origin-caddy-relay-bare-domain-ssl-cloudflare-pro/
datePublished: 2026-05-28
---
Cloudflare Pro proxied every subdomain my SaaS threw at it and silently refused the apex. If you run a SaaS where customers bring their own domain, you will hit this exact wall: `www.theircompany.com` goes behind Cloudflare with a custom hostname and a real cert in about a minute, and `theircompany.com` (the bare apex, no `www`) just will not proxy with a custom certificate. That is not a config you missed. Proxying a custom-cert apex is an Enterprise SSL-for-SaaS feature, and Pro does not have it.

Here is the wall, why it exists, and the small off-origin relay I stood up to get the capability back without paying for Enterprise. I 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.

## The wall

The setup that works fine for `www` is Cloudflare-for-SaaS custom hostnames. Your 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 now you are back to 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, which is an Enterprise line item.

So on Pro you have a `www` that is fully behind the edge and an apex that has nowhere good to go. The naive answer is "issue a Let's Encrypt cert per apex and serve it from origin." That was literally the architecture I started with, with IIS and Certify managing per-domain certs on the Windows origin box. 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 does not scale, and it puts cert state on the origin for every domain you onboard.

## The shape that actually works

You do not need the origin 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.com` to Cloudflare custom hostname (the existing, working path).
- `customer.com` (apex) A record to a relay you control, at a fixed IP.
- The relay terminates TLS for whatever apex hit it and 301s to `https://www.customer.com`.

That is the whole trick. The apex is never an endpoint, it is 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's only job is "answer TLS for any apex name, then redirect." The origin stays out of it entirely.

## Why Caddy, specifically 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 exactly this. With on-demand TLS, Caddy does not provision certs ahead of time. When a real inbound TLS connection arrives for a hostname it does not yet have a cert for, it issues one from Let's Encrypt during the handshake and caches it. No per-domain config, no provisioning job, no list of domains to maintain. The first real connection to `customer.com` is what mints the cert. The customer flipping their apex A record to the relay is itself the trigger.

That is the property that makes this scale. The provisioning step does not get faster, it disappears. Onboarding a new apex is one DNS change on the customer side and nothing on yours.

I put the relay on a DigitalOcean droplet with a Reserved IP. The Reserved IP matters: 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 itself is tiny. It terminates TLS and emits 301s, that is all it does.

The Caddyfile is short. The core of it is 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 "is this a host I should issue for?" before Caddy talks to Let's Encrypt. Point it at your real list of customer apexes. That bounds issuance to domains you actually serve.

## What bit me

Two things from running this at migration scale, both worth knowing before you build the relay into a fleet:

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, and those probes always fail and are slow. When I scanned all current domains, the scan was timing 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 silently skipped the 37 that were not ready. "Skip the expensive probe when the precondition is not met" is obvious in retrospect and easy to miss when you only test the happy path.

The other one is Windows-specific and cost me a confused half hour. Running that relay probe from the origin box (Windows) failed with an SSPI error, because 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 your relay health checks from the origin.

## The error you will misdiagnose

While I am here: when this whole class of setup goes wrong on the Cloudflare side, the symptom you will see 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. Error 1001 in an SSL-for-SaaS context 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. I lost time treating it as a DNS problem the first time a customer hit it.

## The takeaway

Before you point a customer's apex at the relay, resolve its A record and compare it with the relay IP. Then confirm the hostname is on the ask endpoint's approved list before the first TLS connection.

Before you point a customer's apex at the relay, check two things. Confirm the A record actually resolves to the relay's IP, not a stale value left over from before the flip, and confirm the hostname is on the ask endpoint's approved list so Caddy will actually issue for it instead of silently refusing. 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 I Could Measure in 12 Seconds)](/migration-flip-live-outage-measure-blast-radius): 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](/cloudflare-saas-iis-sni-host-mismatch-gotcha): the www custom-hostname path this relay redirects toward
- [The GoDaddy "No Public API" Myth: Reverse-Engineering the DCC Internal DNS Endpoint](/godaddy-no-public-api-reverse-engineer-dcc-dns): the DNS migration that sets the apex A records pointing at this relay
- [My whole 1,200-domain provider split died instantly: keep a second DoH provider on hand](/keep-a-second-doh-provider-dns-google-fallback): DNS resilience when thousands of apex records point at one fixed IP
- [A three-reviewer naming cascade for a string 1,308 domains will CNAME to forever](/naming-cascade-for-a-string-1308-domains-cname-to-forever): choosing the CNAME string that the www side of these same domains points at
