Tenants run their panels on their own domains: the client points a domain at the service, the core spins up a tenant behind it, and buyers visit their address, not mine. Static files, however, were served by a single box in Europe, which costs a visitor from Asia or Latin America extra hundreds of milliseconds on every image. A Cloudflare proxy solves that with one switch: css, js and images start coming from the edge node nearest the visitor, and the origin is left to handle only dynamic requests.
The switch does break something next door. I had a check that said «the domain is connected and points to us», and the panel status in the dashboard plus part of the automation on my side both depend on it. That is exactly the check a proxy invalidates: the domain no longer resolves to my server. Here is how I rebuilt it and what I stepped on along the way.
Why the old check would lie
DNS used to be the proof. The client delegates the domain or sets a CNAME, I look at where the record points, and if it points to my server the domain is connected. Simple logic, and before the proxy it worked.
Behind the orange cloud the domain resolves to Cloudflare addresses. My server does not appear in the DNS answer at all. So the check confidently reports «this domain does not point to us» for a perfectly working panel, and the better the CDN is configured, the louder it lies.
The rule I took away: a health check must not rely on anything the CDN changes. The IP address, the redirect chain, transport headers - all of that now belongs to the proxy. What still belongs to me is the response body.
The proof moved into the response body
The core exposes a service endpoint on every tenant domain:
GET https://panel.example.com/health
{"status":"ok","tenant_id":"8f2c41ab..."}
tenant_id is the tenant identifier. A domain counts as connected only if the response is successful and that identifier matches the one stored for the panel in billing. The field passes through the proxy untouched: Cloudflare does not rewrite response bodies, and that is the only property worth building on here.
A bare 200 is not enough, and this is not a theoretical concern. Anything can answer on that domain: a registrar parking page, the client's old site, someone else's panel, a hosting error page. They all return 200, and none of them means the panel works.
Redirects are deliberately not followed
The first version of the check followed the redirect chain to the end, like a browser. One panel was counted as working while its domain pointed at an unrelated host: the chain ended with a 200 on a third-party site. The client had typed the wrong thing into the domain field and the check swallowed it.
Http::timeout(8)->withoutRedirecting()->acceptJson()
->get('https://'.$panel->domain.'/health');
The core answers that path directly, with no redirection. So any redirect is itself a sign that something else is answering, and there is no reason to follow it.
The ordering you cannot swap
Certificates for tenant domains are issued automatically: the core's web server sees a new name in SNI and obtains a Let's Encrypt certificate for it on demand, with no manual step. The scheme is elegant right up to the moment someone puts the domain behind the proxy too early.
Then the ACME validation travels through the proxy to an origin that has no certificate for that name yet. Cloudflare cannot connect to the origin over https, validation fails, the certificate is never issued, and issuance blocks itself. The observable symptom: the DNS record exists, https never comes up, and it looks like a fault on my side.
Hence the strict order: the record is created grey, the domain answers for the first time, the zone SSL mode is set to strict, and only then the cloud goes orange.
- Grey record first so the ACME challenge reaches the origin directly and the certificate lands on disk.
- Strict, not flexible: the origin holds a valid certificate for exactly this name, while flexible talks to the origin over http and, combined with my 80 to 443 redirect, produces a loop.
- The switch hangs off the health check, not off zone creation: the moment «the domain answered for the first time» is visible there. Zone sync only processes unfinished zones, so a zone that went active long ago would stay grey forever.
Two small things that cost me time. First, the panel is flagged as «already proxied» right after the switch, otherwise the scheduled check would hit the Cloudflare API every fifteen minutes in a loop. Second, the API token needs permission to edit zone settings; without it the SSL mode simply does not change - quietly, with no error - and the cloud never turns on.
Five verdicts instead of up or down
While the check was binary it answered «is the panel working». With a proxy in the picture a different answer was needed: «what exactly is wrong». Especially for clients who keep the domain in their own Cloudflare account and enable the cloud themselves, without me.
- not in DNS - the domain does not resolve, the record is not there yet;
- no https - the domain resolves but the connection is never established;
- answering but broken - 5xx, a hosting 404, a redirect;
- foreign tenant - 200 and valid JSON, but the wrong identifier;
- working.
The split paid off immediately: «no https» means one thing nine times out of ten, that the cloud was enabled before the certificate was issued. Before the split that case sat in the same bucket as «the domain does not exist», and every ticket had to be taken apart by hand.
Telling a proxied domain without access to the zone
When the zone lives in my account I know the state of the cloud. When the domain sits in the client's account I do not, and it is worth knowing: a noticeable share of «my panel does not open» tickets is a proxy enabled at the wrong time.
The signal is cheap: Cloudflare adds a cf-ray header to every response it emits, including its own 521 and 525 error pages. So the proxy is visible even when the request never reached the origin.
The harder case is when https does not come up and there are no headers at all. Then the check asks the same domain over http: behind a proxy port 80 is answered by Cloudflare, which sets cf-ray again, while a direct domain returns my redirect to https without any of its headers. The extra request is sent only on a failed check, and its body is never read - only headers matter.
The other end is the client's server
An easy thing to miss: the check does not talk to my own machine. Behind a panel domain sits hardware I do not control, and it can answer with anything.
A timeout limits time, not volume. While the body was read in full, a mock on a client domain could pour as much as it liked into the memory of the checking process. Now the response is streamed with a 64 KB cap, and the remainder stays in the socket and dies with the connection. Our /health fits in a few hundred bytes, so a truncated JSON does not parse and such a response gets the «foreign» verdict rather than «working».
The honest limit: an identifier is not a signature
The tenant identifier is not a secret. The core hands it to anyone who knocks on the domain, otherwise the check could not work at all. Which means the owner of a domain whose panel once opened can move that domain to their own server and serve the same JSON by hand.
Before fixing that, it is worth asking what a forgery actually buys. Not access, not invoices, not money: those are counted by the core from its own data, and billing talks to it over an internal channel with an operator token that never travels to a panel domain. What is left is influence over internal flags describing the state of a domain - annoying, but out of proportion to the effort.
If real verification is ever needed, a challenge is the cheapest option: the checker sends a one-time random value and the core returns an HMAC over a shared secret. It cannot be forged without the secret, and it survives the proxy unchanged, which keeps the property the whole design rests on.
What stayed with me
A CDN in front of your site is not «the same thing, only faster». It takes over the transport: the IP, TLS, part of the headers, the way connections are established. Everything your monitoring used to infer from the transport has to be inferred from something else once the proxy is on, and it is better to notice that yourself than to hear it from a client with a working panel and a red status in the dashboard.
The working criterion is simple: check something only your own code can confirm, and carry it where the proxy changes nothing. For me that is the tenant identifier in the response body. As a side effect the check became more honest than before: it used to prove that a domain pointed at the right server, and now it proves that the right panel actually answers on that domain.