I spent four hours on May 30 chasing a Cloudflare-for-SaaS setup that returned no errors and behaved wrong anyway. No 5xx, no cert warning, no broken padlock in the browser. The TLS handshake completed cleanly. The page just came back as the wrong site, or with subtly wrong routing, depending on which customer domain I tested. The whole failure lived in a place neither the Cloudflare docs nor the IIS docs think to connect: SNI and the HTTP Host header are two different values, and Cloudflare-for-SaaS makes them disagree on purpose.

If you front a Windows/IIS app with Cloudflare-for-SaaS so each customer can bring their own domain, this is the gotcha waiting for you. It is dangerous precisely because nothing throws.

What Cloudflare-for-SaaS actually sends

Cloudflare-for-SaaS (the custom-hostnames product) lets clientcompany.com resolve to Cloudflare’s edge, and Cloudflare terminates TLS for that hostname using a cert it manages. The traffic from Cloudflare back to your origin is where the trap is set.

When Cloudflare connects to your origin over the fallback origin certificate, the SNI on that origin-facing handshake is the custom hostname. So your origin sees a TLS ClientHello with SNI=clientcompany.com. That feels right. You assume the rest of the request matches.

It does not have to. SNI is a TLS-layer hint sent during the handshake, before any HTTP exists. The HTTP Host header is an application-layer value sent inside the request after TLS is up. They are set by different parts of the stack, and on Cloudflare-for-SaaS they can carry different values: the SNI follows the custom hostname while the Host header follows whatever Cloudflare’s origin rules, host-header-override, or your DNS target resolves to.

Why IIS does not catch it

IIS routes by Host header. Site bindings match on hostname:port, and Host-header matching is how IIS decides which site serves a request. SNI matters to IIS only for picking a certificate when you have SNI-enabled HTTPS bindings. The certificate selection and the site selection are answering two different questions.

So you get a handshake where:

  • IIS selects a cert using the SNI (clientcompany.com), succeeds, handshake completes.
  • IIS then routes the decrypted request using the Host header, which is not clientcompany.com, and lands it on whatever binding matches that header instead.

Cert selection passes. Routing goes somewhere else. No layer in the chain has both halves in view at the same time, so no layer raises an alarm.

The Full vs Full (strict) detail that hides it

Here is the part that turns a loud failure into a silent one. Your Cloudflare zone’s SSL mode controls how strict the origin-facing validation is.

On Full (strict), Cloudflare validates the origin certificate against the hostname, so a cert/hostname mismatch tends to surface as a 526 (invalid SSL certificate). You get an error. Errors are findable.

On Full, Cloudflare encrypts to the origin but does not strictly validate that the origin cert matches the hostname. The mismatch I described passes. The connection is encrypted, the handshake completes, and the request is delivered to the wrong IIS binding with no signal that anything is off. You are now in a mixed state: TLS is healthy, the byte stream is intact, and the application behavior is wrong. That is the worst kind of broken, because every dashboard says it is fine.

If your zone is on Full and your symptom is “it works but serves the wrong thing for some custom hostnames,” stop looking at Cloudflare’s analytics. Nothing there will tell you, because from Cloudflare’s perspective the request succeeded.

The fix lives in the IIS binding, not in Cloudflare

The instinct is to fix this in Cloudflare: change the SSL mode, add an origin rule, tweak the custom hostname config. That is the wrong layer. Cloudflare is doing exactly what it advertises. The disagreement is real and it is the product working as designed.

The fix is to make IIS route correctly for the Host header it actually receives. Get the bindings to match the traffic Cloudflare delivers for each custom domain, instead of assuming the SNI in the handshake is the value IIS will route on. The binding is where Host-header routing is decided, so the binding is where the correction belongs.

The general move: stop trusting that “the handshake used the right hostname” means “the request routed to the right site.” Verify the Host header your origin receives, explicitly, and configure the IIS binding against that observed value rather than the SNI.

The other blocker that gates all of this: the origin firewall

Before any of the SNI-versus-Host debugging matters, there is a prerequisite that is not in the Cloudflare-for-SaaS docs. Cloudflare reaches your origin from its own egress IP ranges. If your origin sits behind a managed firewall, that firewall has to allow inbound traffic from those ranges, or the edge can never reach you at all.

For us this was its own vendor ticket and a critical-path blocker: until that firewall rule existed, nothing downstream could work. The symptom looks nothing like a firewall problem from the Cloudflare side. You see connection failures or timeouts that are easy to misread as DNS or cert issues.

The order of operations: open the origin firewall to Cloudflare’s egress ranges first, confirm the edge can reach the origin, then debug routing. Do it in the other order and you will burn hours on a TLS mystery that is actually a closed port.