.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.
What Flexible mode does on the wire
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.
Following one request through the loop
- The browser requests
https://example.com/. TLS ends at the Cloudflare edge. - Cloudflare connects to the origin on port 80 and sends
GET /withHost: example.com. It also adds headers describing the original request, includingX-Forwarded-Proto: httpsandCF-Visitor: {"scheme":"https"}. - The web server receives the request on a plain HTTP socket. Apache sets
%{HTTPS}tooff. PHP sees an empty$_SERVER['HTTPS']andSERVER_PORTset to80. - A redirect fires. It might be a
RewriteCond %{HTTPS} offrule in.htaccess. It might be WordPress itself:homeandsiteurlpoint tohttps://, the request does not look secure tois_ssl(), so WordPress sends it to the canonical HTTPS address. Either way, the origin returns301withLocation: https://example.com/. - Cloudflare passes that response back to the browser unchanged.
- The browser follows the redirect to
https://example.com/, the exact URL it just requested, and the process starts again at step 1.
Why the origin can't tell the visitor used HTTPS
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.
Confirming the loop with curl
curl shows each hop:
BASH
curl -sIL --max-redirs 5 https://example.com/ | grep -iE '^(HTTP|location|server)'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)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- 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.
Moving to Full (strict) without breaking the site
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
- Install a valid certificate on the origin vhost. Make sure it covers every hostname you proxy, usually both the apex domain and
www. - Run the direct origin test above without
-kand confirm it succeeds for each hostname. - Switch the SSL/TLS mode in Cloudflare to Full (strict).
- Load the site through Cloudflare with curl or a private browser window, not a window that has cached the old 301.
- Remove any
X-Forwarded-Protoworkaround you added as a bridge. Your normal%{HTTPS}redirect rules can stay, because the origin now sees real TLS connections.
526means Cloudflare reached the origin but rejected its certificate. It is expired, self-signed, or the name does not match.525means 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.
