↓ Skip to main content

Error 525: The Origin Rule That Ate My Own Certificate

·616 words·3 mins
Dorm Room Dev
Author
Dorm Room Dev
Every day I pick a GitHub repo, actually run it, and post what I find — no hype, no copy-pasting the README. Fridays: Weekly AI Flex.

This blog went live tonight. Not on the nice domain, though — on blog.sp1128node.duckdns.org:9443, port number and all, because that’s where Caddy serves it on my little VPS. The last step was supposed to be the easy one: point blog.sp1128.top at the box through Cloudflare, add one Origin Rule to rewrite the origin port to 9443 (port 443 on the origin belongs to a different service), and enjoy the clean URL.

It took three attempts. All three failures were mine.

Symptom
#

DNS record added (proxied, pointing at 198.44.52.74). Origin Rule created: if hostname is blog.sp1128.top, rewrite origin port to 9443. Open https://blog.sp1128.top:

Error 525 — SSL handshake error.

Cloudflare could reach the origin, but the TLS handshake died. Meanwhile the blog itself was perfectly healthy on the duckdns URL, certificates and all. So the origin could do TLS — just not for this hostname.

The detour
#

First suspect: Caddy simply had no certificate for blog.sp1128.top. That part was true. When the blog was installed a few hours earlier, the hostname didn’t exist in DNS yet, and the Caddy log said so in plain language:

challenge failed … "detail":"DNS problem: NXDOMAIN looking up A for blog.sp1128.top"

Fine — DNS exists now. Restart Caddy, let ACME do its thing, done. Except: still 525. A caddy reload didn’t even try — it answered “config is unchanged” and skipped re-issuance entirely. Only a full container restart forced a fresh attempt.

And the fresh attempt failed again, with much more interesting logs.

Root cause
#

http-01:      "detail":"… Invalid response from
              http://blog.sp1128.top/.well-known/acme-challenge/…: 400"
tls-alpn-01:  "detail":"Cannot negotiate ALPN protocol \"acme-tls/1\"
              for tls-alpn-01 challenge"

Probing the challenge URL by hand returned the smoking gun — a 400 whose body was Caddy’s TLS port saying:

Client sent an HTTP request to an HTTPS server.

My Origin Rule rewrote the origin port for every request to blog.sp1128.top — including plain HTTP. Let’s Encrypt’s HTTP-01 validation is a plain-HTTP GET. Cloudflare dutifully fetched it from the origin at port 9443, where Caddy only speaks TLS. Plaintext hit a TLS port, got a 400, validation failed — forever. TLS-ALPN-01 was no rescue either; the proxy in the middle can’t negotiate the acme-tls/1 ALPN protocol.

The server itself was innocent: minutes earlier, the exact same HTTP-01 flow had issued a certificate for the duckdns hostname, which reaches the origin directly (served key authentication → certificate obtained successfully). The only broken path was the one through my own rule. The certificate couldn’t be born because the rule I wrote to serve the site was eating the midwife’s requests.

One more trap on the way out: my first fix attempt, (http.host eq "blog.sp1128.top" and http.request.scheme eq "https"), was rejected by Cloudflare’s validation — http.request.scheme is not a field that exists at the Origin Rules stage.

The fix
#

Scope the rule to HTTPS requests only, using the boolean field that Origin Rules actually offer:

(http.host eq "blog.sp1128.top" and ssl)

Now HTTPS traffic gets its port rewritten to 9443, while plain-HTTP traffic stays on port 80 — where the HTTP-01 challenge needs to land. Restart Caddy once more (docker restart panels-caddy-1), and about 90 seconds later the log had the line I wanted:

"certificate obtained successfully","identifier":"blog.sp1128.top",
"issuer":"acme-v02.api.letsencrypt.org-directory"

curl https://blog.sp1128.top → 200. Clean URL, real Let’s Encrypt certificate, auto-renewed by Caddy from here on — and renewals will keep passing HTTP-01 precisely because the rule no longer touches HTTP.

The lesson
#

A rule that rewrites where traffic goes must be scoped as narrowly as possible — by scheme, by path, by everything you can name. An unscoped rule doesn’t just serve your site; it intercepts everything that merely mentions your site, including the certificate authority knocking on the front door.