Bali, Indonesia
Insights

Cloudflare Flexible SSL Isn't End-to-End, and It Causes WordPress Redirect Loops

October 29, 2026
0:00
You put a site behind Cloudflare and pick Flexible SSL because it works without touching the server. Later you turn on a force-HTTPS option in WordPress, or add a redirect rule to .htaccess, and the browser now shows ERR_TOO_MANY_REDIRECTS. Neither setting is wrong on its own. They disagree about whether the request is secure, and Flexible mode is what makes them disagree. This comes up a lot at the support desk, usually right after someone followed two separate guides that were each correct. The fix is easier to understand once you see what actually travels over the wire.
When a domain is proxied through Cloudflare (the orange cloud in DNS), every visit has two separate connections. The first runs from the visitor's browser to a Cloudflare edge server. The second runs from that edge server to your origin, which is the server that actually hosts WordPress. Flexible mode encrypts only the first connection. The browser does a TLS handshake with Cloudflare using Cloudflare's edge certificate, so the padlock appears. Cloudflare then opens a plain TCP connection to your origin on port 80 and sends the request as ordinary HTTP. Your server never runs a TLS handshake at all. That is why Flexible is not end-to-end encryption. Anything on the path between Cloudflare and your server can read that second leg in cleartext. That includes the datacenter network, upstream transit, or a compromised device along the route. Login forms, wp-admin session cookies, and checkout data all cross that leg unencrypted. The padlock only describes the leg the visitor can see, and customers often take it as proof that the whole path is protected.
Here is one page load on a site that runs Flexible mode and also forces HTTPS at the origin:
  1. The browser requests https://example.com/. TLS ends at the Cloudflare edge.
  2. Cloudflare connects to the origin on port 80 and sends GET / with Host: example.com. It also adds headers describing the original request, including X-Forwarded-Proto: https and CF-Visitor: {"scheme":"https"}.
  3. The web server receives the request on a plain HTTP socket. Apache sets %{HTTPS} to off. PHP sees an empty $_SERVER['HTTPS'] and SERVER_PORT set to 80.
  4. A redirect fires. It might be a RewriteCond %{HTTPS} off rule in .htaccess. It might be WordPress itself: home and siteurl point to https://, the request does not look secure to is_ssl(), so WordPress sends it to the canonical HTTPS address. Either way, the origin returns 301 with Location: https://example.com/.
  5. Cloudflare passes that response back to the browser unchanged.
  6. The browser follows the redirect to https://example.com/, the exact URL it just requested, and the process starts again at step 1.
The browser gives up after a fixed number of hops and shows the redirect error. Every individual response in that chain is correct from the origin's point of view. The origin was asked over HTTP and told the client to use HTTPS. It cannot know that the client already did.
The HTTPS variable in Apache, Nginx, LiteSpeed, and PHP comes from the socket, not from the request content. It answers one question: did this connection arrive over TLS on this server? Under Flexible mode the answer is always no, because Cloudflare never opens a TLS connection to the origin. The forwarded headers do carry the truth, but they are just text. Any client that can reach your origin directly can send X-Forwarded-Proto: https. That is why web servers and WordPress ignore them by default. You will find common workarounds that make the origin trust those headers. One is a few lines in wp-config.php that set $_SERVER['HTTPS'] to on when X-Forwarded-Proto is https. Another is changing the rewrite condition to check %{HTTP:X-Forwarded-Proto} instead of %{HTTPS}. Both stop the loop. Both also leave the second leg unencrypted. If ports 80 and 443 on the origin are open to the whole internet, anyone can send a spoofed header. Treat these as a short-term bridge while you set up a real certificate, not as the fix.
Before you change anything, confirm what is happening. The browser caches 301 redirects, so it is not a reliable witness here. curl shows each hop:
BASH
curl -sIL --max-redirs 5 https://example.com/ | grep -iE '^(HTTP|location|server)'
A Flexible-mode loop looks like this:
TEXT
Sample output format:
HTTP/2 301
server: cloudflare
location: https://example.com/
HTTP/2 301
server: cloudflare
location: https://example.com/
(repeats until curl hits --max-redirs)
The signature is a Location header that matches the URL you just requested. If the location bounces between http:// and https:// instead, or between www and the apex domain, you have a different redirect conflict. Look at Cloudflare redirect rules and the WordPress URL settings instead. Next, talk to the origin directly and skip Cloudflare. This also tells you whether the origin is ready for strict mode. Replace the placeholder IP with your server's real address:
BASH
# 203.0.113.10 is a placeholder for your origin IP
# Plain HTTP to the origin: expect the 301 that causes the loop
curl -sI --resolve example.com:80:203.0.113.10 http://example.com/

# HTTPS to the origin WITHOUT -k: this fails if the certificate is invalid,
# which is roughly what Full (strict) will check
curl -sI --resolve example.com:443:203.0.113.10 https://example.com/

# Inspect the certificate the origin presents for this hostname
openssl s_client -connect 203.0.113.10:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
Here is how to read the results:
  • If the HTTPS test fails with a verification error, the origin is not ready for Full (strict) yet.
  • If the connection is refused, nothing is listening on 443 for that site.
  • If curl and a browser disagree about the origin certificate, the cause is often an incomplete chain. Why curl Rejects an SSL Certificate That Works in Your Browser covers that case.
Cloudflare has two modes that use HTTPS to the origin, and the difference matters. Full connects to the origin on port 443 over TLS, but it does not check the certificate. Self-signed or expired certificates are accepted. That fixes the redirect loop, because the origin now sees HTTPS as on. It also encrypts the second leg. But Cloudflare cannot confirm it is talking to your server and not something sitting in between, so the connection is encrypted but not authenticated. Full (strict) does the same thing and also validates the certificate. The certificate must:
  • be issued by a CA Cloudflare trusts, or be a Cloudflare Origin CA certificate
  • be unexpired
  • cover the hostname being requested
This is the mode that actually earns the padlock. There are two practical choices for the origin certificate. A Cloudflare Origin CA certificate is generated in the Cloudflare dashboard and installed on your server like any other certificate. Cloudflare's proxy trusts it, but browsers do not. Everything works while the record is proxied. If you ever switch the record to DNS-only, visitors get certificate warnings straight away. A publicly trusted certificate, such as Let's Encrypt, works whether the record is proxied or not. You do need to keep renewal working while traffic arrives through Cloudflare. The order matters, because switching the mode first is how sites go from a redirect loop to an error page. Follow these steps in sequence:
  1. Install a valid certificate on the origin vhost. Make sure it covers every hostname you proxy, usually both the apex domain and www.
  2. Run the direct origin test above without -k and confirm it succeeds for each hostname.
  3. Switch the SSL/TLS mode in Cloudflare to Full (strict).
  4. Load the site through Cloudflare with curl or a private browser window, not a window that has cached the old 301.
  5. Remove any X-Forwarded-Proto workaround you added as a bridge. Your normal %{HTTPS} redirect rules can stay, because the origin now sees real TLS connections.
If something goes wrong after the switch, Cloudflare's error codes point you in the right direction:
  • 526 means Cloudflare reached the origin but rejected its certificate. It is expired, self-signed, or the name does not match.
  • 525 means the TLS handshake itself failed. That often means nothing is serving TLS on 443, or the vhost for that hostname is not configured for SSL.
The redirect loop is the visible part of a design problem: the origin is being told one thing by the socket and another by the browser. Flexible mode only hides that gap until a redirect rule exposes it. Put a real certificate on the origin, test it directly, and then switch to Full (strict). That removes the loop, and it means the padlock your visitors see actually covers the whole path to your server.