Cloudflare Troubleshooting Advanced

Wildcard Subdomains and Automatic Cloudflare SSL for Multi-Tenant Apps

Give every tenant its own subdomain with automatic DNS and SSL — and the 526 → 404 → 403 debugging trail behind it, including an SSR fetch that quietly hairpins out through your CDN.

Tested with

Traefik
v3
Sveltekit
2.x

Before you start

  • A domain already active on Cloudflare with Universal SSL enabled
  • A reverse proxy you control at the origin (Traefik here)
  • An application that resolves its tenant from the Host header
On this page

Draft — screenshots pending. The SCREENSHOT: captions below mark where UI captures go; see the figure manifest at the end. Every hostname and IP here is a placeholder.

On a multi-tenant commerce platform, every new store should just work at its own address — acme.example.com, demo.example.com, test1-f1m86.example.com — with DNS assigned automatically and HTTPS provisioned automatically, no per-store setup.

That is a one-line promise with a surprising amount of plumbing behind it. What follows is the whole thing, including a bug hunt that went 526 → 404 → 403 before every subdomain rendered. If you run SvelteKit — or any SSR framework — behind a CDN, the 403 at the end is the part worth your attention. It is easy to ship and hard to see.

The goal

One wildcard, set up once, covering every current and future tenant:

  • DNS: *.example.com → your edge, so no per-store DNS record is ever created.
  • SSL: a wildcard certificate covering every subdomain, auto-renewing.
  • Routing: any *.example.com request reaches the one multi-tenant app, which resolves the tenant from the Host header.

When those three hold, provisioning a new store is a database row and the URL is live immediately.

A wildcard setup is really three wildcards

The most useful mental model here: “wildcard SSL” is not one thing. There are three independent layers, and each has to be a wildcard or new subdomains break.

LayerWhat it isFailure symptom
DNS*.example.com A/CNAME → your edge, proxiedNXDOMAIN, does not resolve
Edge TLSThe certificate the browser sees — Universal SSL covers example.com and *.example.comBrowser certificate warning
Origin TLS + routingThe certificate Cloudflare sees when it talks to your server, and a router that accepts any subdomain526 (bad origin cert) or 404 (no route)

The browser-facing layers are the easy 90%. Universal SSL already covers one level of wildcard for free, so test1-f1m86.example.com is handled at the edge with no work at all. The origin is where the surprises live.

Symptom 1 — 526: the origin certificate

A brand-new store subdomain returned a Cloudflare 526:

bash
curl -sS -I https://test1-f1m86.example.com/
plaintext
HTTP/1.1 526
server: cloudflare
cf-cache-status: DYNAMIC

SCREENSHOT: terminal showing the HTTP/1.1 526 response headers

526 means one specific thing. Cloudflare is in Full (strict) SSL mode, it connected to your origin over TLS, and the certificate the origin presented could not be validated. Not DNS. Not the edge. The origin certificate.

The tell that this was wildcard-specific: sibling subdomains configured by hand all worked, but anything un-provisioned failed identically.

bash
for h in test1-f1m86 zzznotreal-9q7x2 shop store; do
  curl -sS -o /dev/null -w "$h -> %{http_code}\n" "https://$h.example.com/"
done
plaintext
test1-f1m86      -> 526
zzznotreal-9q7x2 -> 526   # never existed
shop             -> 526
store            -> 526

Every subdomain that had not been individually set up got the origin’s default self-signed certificate, which Full (strict) rightly rejects.

The fix is a wildcard origin certificate that Cloudflare trusts. Cloudflare’s Origin CA issues exactly that: a long-lived certificate for example.com plus *.example.com, trusted by Cloudflare’s edge in Full (strict) mode. Generate it under SSL/TLS → Origin Server → Create Certificate, install it on the origin reverse proxy as the default certificate, and every subdomain — present and future — is covered in one shot.

SCREENSHOT: Cloudflare “Create Origin Certificate” form with hostnames example.com and *.example.com

Result: 526404. Progress. A 404 means the request now reaches the origin, so TLS is happy.

Symptom 2 — 404: wildcard routing at the origin

The 404 was the reverse proxy’s default “no router matched” page. The certificate let Cloudflare in, but the origin had no route for an arbitrary subdomain.

Most panels — this one runs Traefik underneath — let you add domains per app. The trap: typing *.example.com into a plain domain field generates a Traefik Host(`*.example.com`) rule, and Traefik treats that asterisk literally, so it matches nothing. A real wildcard needs HostRegexp, which is usually behind a dedicated “wildcard domain” toggle.

toml
# What you want Traefik to generate (v3):
rule = "HostRegexp(`^.+\\.example\\.com$`)"   # matches any subdomain

# What a plain domain field gives you:
rule = "Host(`*.example.com`)"                # matches the literal "*." — never fires

SCREENSHOT: hosting panel “Add Domain” dialog with the wildcard toggle enabled and the target port shown

Point the wildcard route at the single multi-tenant app and any subdomain reaches it:

plaintext
test1-f1m86       -> 500   # reaching the app now
randomstore-ab12x -> 500
hello-world-99    -> 500

526 (SSL) and 404 (routing) are both gone. The 500 is a new and better problem: the app is running and answering, it just cannot render these particular stores yet.

Symptom 3 — 403: the SSR hairpin

The app returned:

json
{ "message": "HTTP error 403: Forbidden", "status": 500 }

This is the interesting one, because every obvious explanation was wrong.

  • “The store does not exist.” No — the API resolved it fine. The public-details endpoint returned a real store with 200.
  • “A WAF or bot rule is blocking it.” It was deterministic, five times out of five, so not rate limiting.
  • “It is Cloudflare at the edge.” Bypassing Cloudflare entirely, hitting the origin directly with the Host header set, still returned 500. Not the edge.

The decisive clue was a correlation. Subdomains on DNS-only records (grey-cloud, straight to origin) worked. Subdomains on the proxied wildcard (orange-cloud) failed — for the same underlying store.

The root cause

The storefront resolves the tenant during SSR with a relative fetch:

ts
// hooks.server.ts — resolve the store for this request
const store = await storeService.getStoreByIdOrDomain({ domain });
// under the hood: event.fetch('/api/stores/public-details?domain=…')

That /api/... is relative, so SvelteKit’s event.fetch resolves it against the current request’s origin — the public hostname. In production the /api prefix is proxied at the edge, not inside the app. So the server, in the middle of rendering a page, makes an outbound HTTPS call to its own public domain.

For a proxied subdomain that call leaves the container, goes out to Cloudflare and comes back — and Cloudflare blocks it, because it arrives from a datacenter IP (the server’s own egress) and trips bot protection. Hence 403.

For DNS-only domains the same relative fetch resolves straight to the origin, never touches Cloudflare, and works. That is the entire correlation.

We proved it from inside the container, using the same fetch the SSR path uses:

bash
# via the public hostname — out through the CDN and back
node -e 'fetch("https://test1-f1m86.example.com/api/stores/public-details?domain=test1-f1m86.example.com").then(r=>console.log("via public/CDN:", r.status))'

# straight to the backend on the internal network
node -e 'fetch("http://10.0.0.5:7000/api/stores/public-details?domain=test1-f1m86.example.com").then(r=>console.log("direct:", r.status))'
plaintext
via public/CDN: 403
direct: 200

SCREENSHOT: container console showing “via public/CDN: 403” and “direct: 200”

That is the whole bug in two lines: the SSR sub-request hairpins through the CDN and gets blocked.

The fix

Two layers. Use both.

1. Immediate, at the infrastructure level. Allow the origin’s egress IP through the CDN: Security → WAF → IP Access Rules, add an Allow for the server’s IP. The hairpin passes. No code, no redeploy.

SCREENSHOT: Cloudflare IP Access Rules list with the origin IP set to Allow

2. Proper, in code. Stop hairpinning at all. SvelteKit’s handleFetch hook rewrites server-side fetches so /api calls go straight to the backend and never leave the network.

ts
// hooks.server.ts
import { env } from '$env/dynamic/public';
import type { HandleFetch } from '@sveltejs/kit';

export const handleFetch: HandleFetch = async ({ request, fetch }) => {
  const apiBase = env.PUBLIC_API_URL; // e.g. http://10.0.0.5:7000
  if (apiBase) {
    const url = new URL(request.url);
    if (url.pathname.startsWith('/api')) {
      const target = new URL(apiBase);
      url.protocol = target.protocol;
      url.hostname = target.hostname;
      url.port = target.port;
      return fetch(new Request(url, request)); // direct to backend
    }
  }
  return fetch(request);
};

Browser fetches are untouched — they never run through handleFetch — so there is no mixed-content problem. Only server-side calls are redirected inward. That makes the fix independent of any egress-IP allowlist and removes a pointless CDN round trip on every render.

While in there, we also turned “no store maps to this domain” from a raw 500 into a proper 404:

ts
// hooks.server.ts — a genuine 404 means "store not found"; re-throw real errors
try {
  storeDetails = await storeService.getStoreByIdOrDomain({ domain });
} catch (e) {
  if (!/not found|404/i.test(e?.message || '')) throw e; // real API failure → 500
}
if (storeDetails?.id) event.locals.storeDetails = storeDetails;
else event.locals.storeNotFound = true;
ts
// +layout.server.ts
if (event.locals?.storeNotFound) throw error(404, 'Store not found');

Bonus: the deploy that succeeded and changed nothing

One last trap, because it cost real time. After pushing the fix the deploy went green — the build log showed a fresh vite build and a new image — yet the running app served the old code. Every deploy produced a new image but kept running the same container.

That is the Docker Swarm classic: the service references an image by the :latest tag, and updating a service with an unchanged tag does not re-pull. The build succeeds; the container never swaps.

Do not trust “deploy succeeded” — verify the running code. Exec into the container and grep for a string you just added:

bash
# inside the running container
grep -c storeNotFound src/hooks.server.ts
plaintext
0     # still the old code, despite a green deploy

The fix is to force the service to recreate its container from the new image — a stop and start, or pin the image by digest instead of by tag.

Takeaways

  • “Wildcard SSL” is three wildcards: DNS, edge certificate, and origin certificate plus route. Debug them independently; the HTTP status tells you which layer. 526 is the origin certificate, 404 is the origin route, and anything after that is the app.
  • Cloudflare Origin CA gives you a free, long-lived wildcard certificate for the origin — the clean way to satisfy Full (strict) for every subdomain at once.
  • Host() versus HostRegexp(): a literal *. in a Traefik host rule matches nothing. Wildcard routing needs a regex matcher.
  • SSR plus a CDN means watching your relative fetches. A relative /api fetch during server rendering resolves to your public origin and can hairpin out through your CDN, where your own server looks like a bot. Use handleFetch, or the equivalent in your framework, to send server-side calls straight to the backend.
  • Verify the running code, not the deploy status. A green build is not a swapped container. Same-tag images do not re-pull.

The end state: a wildcard that provisions DNS, SSL and routing for every tenant automatically, and an SSR path that talks to the backend directly instead of taking the scenic route through the CDN.

Figure manifest

Screenshots to capture, then wire in place of the SCREENSHOT: placeholders above. Sanitise each one — crop or redact real IPs, container IDs, account emails and dashboard URLs before publishing.

  1. 526-headers — terminal: the HTTP/1.1 526 response.
  2. origin-ca — Cloudflare Create Origin Certificate, hostnames example.com and *.example.com.
  3. wildcard-domain — hosting panel Add-Domain dialog with the wildcard toggle on.
  4. container-hairpin — container console: via public/CDN: 403 next to direct: 200.
  5. ip-access-rule — Cloudflare IP Access Rules with the origin IP set to Allow.

Frequently asked questions

Why does a brand-new subdomain return 526 when its siblings work?

526 means Cloudflare is in Full (strict) mode and could not validate the certificate your origin presented. Subdomains configured by hand have their own certificate; an un-provisioned one falls back to the origin's default self-signed certificate, which Full (strict) correctly rejects. A wildcard Origin CA certificate installed as the default fixes every current and future subdomain at once.

Why does a Traefik rule containing `*.example.com` never match?

Traefik's `Host` matcher compares literal hostnames, so the asterisk is treated as an ordinary character and the rule can never fire. Wildcard routing needs the `HostRegexp` matcher with a real regular expression.

Why does server-side rendering get 403 while the browser gets 200?

A relative fetch during SSR resolves against the current request's origin, which is the public hostname. The server therefore calls its own public domain, the request leaves the machine, reaches the CDN from a datacenter IP and is blocked as automated traffic. Rewriting server-side fetches to the internal backend address removes the round trip entirely.

Sources

  1. Cloudflare Origin CA certificates Cloudflare Primary source Accessed 21 Jul 2026
  2. Traefik routers and rule matchers Traefik Labs Primary source Accessed 21 Jul 2026
  3. SvelteKit server hooks — handleFetch Svelte Primary source Accessed 21 Jul 2026
  4. Cloudflare IP Access Rules Cloudflare Primary source Accessed 21 Jul 2026
6 min read
  • Cloudflare advanced

    How to Run Cloudflare Tunnel in Front of Traefik

    Expose Docker services through Cloudflare with no inbound ports open — cloudflared plus Traefik, the forwarded-header setting that trips people up, and how to verify the firewall is closed.

    4 min
  • Cloudflare beginner

    How to Point a Cloudflare Domain at Your VPS

    Create the A record, pick the right proxy status, set the SSL mode to Full (strict) and lock the origin so only Cloudflare can reach it — plus what errors 521, 522, 525 and 526 actually mean.

    4 min