Fixing Cloudflare 525 Errors When Pointing a Domain to Fly.io
This is a breakdown of why my Fly.io app fronted by Cloudflare was returning Error 525 (SSL handshake failed) and how it was fixed.
Understanding the Problem
When Cloudflare proxying is enabled (orange cloud), you effectively have dual TLS: Browser → Cloudflare Edge terminates client TLS first, then Cloudflare → Fly.io Origin establishes a second TLS connection. Fly.io must present a valid certificate for the custom domain. If the origin doesn't present a valid cert for the hostname, Cloudflare can't complete the handshake → 525.
The symptom was 525 SSL handshake failed seen only when Cloudflare proxy was enabled. With proxy off (grey cloud / DNS only) the site either failed differently or showed certificate mismatch depending on timing.
Root cause: DNS records at Cloudflare were inconsistent (stale A/AAAA or CNAME remnants). Attempts to upload a custom certificate to Fly.io weren't supported - Fly.io expects to manage cert issuance itself. While DNS didn't cleanly point to Fly.io's IPv6, Fly.io couldn't finish ACME (Let's Encrypt) validation → no valid origin cert → Cloudflare handshake failed → 525.
Mental model:
Browser --TLS--> Cloudflare --TLS--> Fly.io
^
Needs valid cert at origin
If Cloudflare can't finish that second TLS hop, you get 525.
The Solution
DNS hygiene: Remove every unrelated/legacy record for the apex and www that could conflict. Delete old A, AAAA, CNAME records not belonging to the Fly.io deployment. Leave only what Fly.io + ACME needed. Why: ACME HTTP-01 or DNS-01 and routing depend on unambiguous records.
Add correct AAAA records: Fly.io provided an IPv6 that routes to the app. Add proxied records:
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 will still proxy fine. Why IPv6 only works: Cloudflare terminates at edge (dual stack) then connects over IPv6 downstream.
Preserve ACME records: Existing _acme-challenge CNAMEs (added earlier by Fly.io automation) were left intact. These allow Let's Encrypt to validate via DNS-01.
Wait for propagation: TTL + resolver caches required a short wait (a few minutes). Verify via dig from several public resolvers.
Let Fly.io re-issue certificates: Once DNS pointed correctly, Fly.io's dashboard showed domain status as Verified with RSA + ECDSA issued. Cloudflare SSL Mode set to Full (Strict) (or at least Full). Not Flexible. Why Strict matters: Ensures Cloudflare validates the origin cert chain instead of ignoring it or being lax.
Troubleshooting Tips
What actually mattered: 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 are only meant for Cloudflare↔origin trust and aren't 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, over-debugging before checking DNS + Fly.io status + Cloudflare mode.
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"
What matters: 525 = Cloudflare↔origin TLS failure. DNS cleanliness gates cert issuance. Let Fly.io handle issuance; don't upload certs. Fewer DNS records = fewer surprises.
Broken: Proxy pointed at an origin without a valid cert due to messy DNS. Fix: Clean DNS → correct AAAA only → wait for Fly.io issuance → Full (Strict). Result: Stable dual TLS with automated renewals. If you see 525: DNS → Fly.io cert status → Cloudflare mode. Stop when all three are green.