Fixing Cloudflare 525 Errors When Pointing a Domain to Fly.io
My Fly.io app fronted by Cloudflare kept returning Error 525 (SSL handshake failed). Here is the breakdown.
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 (grey 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.
Mental model:
Browser --TLS--> Cloudflare --TLS--> Fly.io
^
Needs valid cert at origin
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:
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:
- DNS returned just the Fly.io AAAA for apex + www
- Fly.io marked the domain verified and showed issued certs
- Cloudflare SSL mode was Full (Strict) with proxy on
Everything else — manual probing, deep TLS inspection — was 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"
A 525 error means a Cloudflare-to-origin TLS failure, and DNS cleanliness gates cert issuance. Let Fly.io handle the issuance, because fewer DNS records mean fewer surprises.
Proxy pointed at an origin without a valid cert because of messy DNS. Clean DNS, correct AAAA only, wait for Fly.io issuance, Full (Strict). If you see 525: check DNS, then Fly.io cert status, then Cloudflare mode. Stop when all three are green.