My Fly.io app worked differently depending on one Cloudflare toggle. With the proxy on, it returned Error 525 (SSL handshake failed). With the proxy off, the browser showed a certificate mismatch. The problem was stale DNS preventing Fly.io from issuing the right origin certificate.
Two TLS Hops, One Failure Point
When Cloudflare proxying is enabled (orange cloud), two TLS connections exist: browser to Cloudflare edge, then Cloudflare edge to Fly.io origin. Fly.io must present a valid certificate for the custom domain. If the origin cert is missing or wrong, Cloudflare cannot complete the handshake. You get 525.
The symptom: 525 SSL handshake failed only when Cloudflare proxy was enabled. With proxy off (gray cloud / DNS only), the site showed certificate mismatch errors instead.
Warning Root cause: DNS records at Cloudflare were inconsistent (stale A/AAAA or CNAME remnants). Fly.io manages cert issuance itself through ACME (Let’s Encrypt). Dirty DNS prevented ACME validation, which meant no valid origin cert, which meant Cloudflare’s handshake failed.
If Cloudflare cannot finish that second TLS hop, you get 525.
The Fix
Clean your DNS. Remove every unrelated or legacy record for the apex and www. Delete old A, AAAA, and CNAME records not belonging to the Fly.io deployment. ACME validation and routing depend on unambiguous records.
Add correct AAAA records. Fly.io provides an IPv6 that routes to the app:
| Name | Type | Content | Proxy status |
|---|---|---|---|
example.com | AAAA | <fly-io-ipv6-address> | Proxied |
www.example.com | AAAA | <fly-io-ipv6-address> | Proxied |
No A record needed if Fly.io only gave IPv6. Cloudflare terminates at edge (dual stack) then connects over IPv6 downstream.
Preserve ACME records. Leave existing _acme-challenge CNAMEs intact. These allow Let’s Encrypt to validate via DNS-01.
Wait for propagation. TTL and resolver caches need a few minutes. Verify with dig from several public resolvers.
Important Let Fly.io re-issue certificates: Once DNS points correctly, Fly.io’s dashboard shows domain status as Verified with RSA + ECDSA issued. Set Cloudflare SSL Mode to Full (Strict), not Flexible. Strict ensures Cloudflare validates the origin cert chain instead of ignoring it.
Three Things to Check
I only needed to confirm three things:
| Layer | Healthy state | Failure signal |
|---|---|---|
| DNS | Only the Fly.io records remain for apex and www | Stale A, AAAA, or CNAME records point elsewhere |
| Fly.io | Domain is verified and RSA + ECDSA certificates are issued | Certificate remains pending or the hostname mismatches |
| Cloudflare | Proxy is on and SSL mode is Full (Strict) | Flexible mode hides the origin problem |
Manual probing and deep TLS inspection were noise once those were correct.
Why custom cert upload failed: Fly.io issues and renews its own certs via ACME. Cloudflare Origin Certificates only handle Cloudflare-to-origin trust and are not public-PKI certs. Uploading was never the path.
Common pitfalls: Stale A/AAAA/CNAME records lingering. Mixing previous host IPs with Fly.io AAAA. Using “Flexible” SSL (masks the real problem). Trying to import certificates instead of letting automation run.
Minimal checklist:
- Add domain in Fly.io
- Publish ONLY the Fly.io AAAA (and A if given) in Cloudflare; proxy on
- Leave
_acme-challengeCNAMEs untouched - Set SSL/TLS = Full (Strict)
- Wait a few minutes; confirm Fly.io shows “Verified / Issued”
Once those three checks were green, the 525 disappeared. Fly.io could issue the certificate and Cloudflare could validate it in Full (Strict) mode.
TLS proxy mistakes show up whenever you put Cloudflare in front of an origin — stack context in static blog on Cloudflare.
One quick signal
Did this earn your time?
Thanks. That gives me something concrete to check.



