Cloudflare ERR_TOO_MANY_REDIRECTS with Flexible SSL and an HTTPS-forcing origin
Minutes after we proxied our marketing site through Cloudflare, every visitor hit ERR_TOO_MANY_REDIRECTS. Cloudflare's Flexible mode spoke plain HTTP to an origin that redirected all HTTP to HTTPS. Full (strict) with an origin certificate ended the loop.
We moved DNS for our marketing site to Cloudflare on a Tuesday afternoon to get caching and DDoS protection before a product launch. The orange cloud went on at 14:20. By 14:25 Slack was full of screenshots of the same browser page:
This page isn't working
www.example.com redirected you too many times.
Try deleting your cookies.
ERR_TOO_MANY_REDIRECTS
Firefox said "The page isn't redirecting properly". The API on a different hostname, which we had not proxied yet, kept working.
Reproducing it
curl showed the loop clearly. Every response was a 301 to the exact same URL:
curl -sIL --max-redirs 5 https://www.example.com/ | grep -iE '^(HTTP|location|server|cf-ray)'
HTTP/2 301
location: https://www.example.com/
server: cloudflare
cf-ray: 90c1a2b3c4d5e6f7-SIN
HTTP/2 301
location: https://www.example.com/
server: cloudflare
cf-ray: 90c1a2b3d6e7f809-SIN
...
curl: (47) Maximum (5) redirects followed
The 301 was redirecting HTTPS to the same HTTPS URL. That only makes sense if whoever generated the redirect thought the request was plain HTTP.
What we ruled out
- A Cloudflare Page Rule or Redirect Rule. The zone had no rules yet.
- Cookies or HSTS cache in the browser. The same loop happened in curl with no cookies.
- The app's own canonical-host redirect. That one redirects
example.comtowww.example.com; thelocationwas identical to the request URL, so it was not involved.
Then we bypassed Cloudflare and asked the origin directly, pretending to be Cloudflare's plain-HTTP connection:
curl -sI http://www.example.com/ --resolve www.example.com:80:198.51.100.40 | grep -iE '^(HTTP|location)'
# HTTP/1.1 301 Moved Permanently
# Location: https://www.example.com/
Root cause
The zone's SSL/TLS encryption mode was Flexible, which was the setting this zone had been created with years earlier and never touched. In Flexible mode, browsers connect to Cloudflare over HTTPS, but Cloudflare connects to the origin over plain HTTP on port 80. Our origin Nginx had a standard port-80 server block that redirects everything to HTTPS. So: browser asks for HTTPS, Cloudflare asks the origin over HTTP, origin answers "go to HTTPS", Cloudflare passes that 301 to the browser, the browser asks for HTTPS again, and the cycle repeats until the browser gives up.
What is happening under the hood
Cloudflare is a reverse proxy, so there are two separate connections: visitor to edge, and edge to origin. The encryption mode only controls the second one:
| Mode | Edge to origin | Certificate check at origin |
|---|---|---|
| Off | HTTP | None |
| Flexible | HTTP, even if the visitor used HTTPS | None |
| Full | HTTPS | Any certificate, even self-signed or expired |
| Full (strict) | HTTPS | Must be valid: publicly trusted or a Cloudflare Origin CA certificate, with matching hostname |
Any origin that decides "is this request secure?" by looking at its own connection will always see
HTTP under Flexible mode. Nginx return 301 https://... on port 80, Apache RewriteCond %{HTTPS} off,
WordPress with FORCE_SSL_ADMIN, Laravel or Express middleware that forces HTTPS based on the request
scheme: all of them loop.
Environment
| Component | Version / value |
|---|---|
| Origin OS | Ubuntu 24.04 LTS |
| Origin web server | Nginx 1.24, Node.js 22 app (Express 4.21) behind it |
| Origin TLS before | Let's Encrypt certificate, port 80 redirects to 443 |
| Cloudflare plan | Pro, proxied A record for www |
| Cloudflare SSL/TLS mode | Flexible (zone default from 2019) |
Minimal reproduction
# Origin: redirect all HTTP to HTTPS
server {
listen 80;
server_name www.example.com;
return 301 https://$host$request_uri;
}
Combine that with Cloudflare Flexible mode and a proxied DNS record, and every request loops.
Who is affected
Every site that forces HTTPS at the origin and sits behind Cloudflare in Flexible mode. It is especially common when an existing HTTPS site is moved behind Cloudflare, because the origin already has a redirect. Sites that are HTTP-only at the origin do not loop, but they are not secure either.
The fix
We switched to Full (strict) so Cloudflare talks HTTPS to the origin and validates the certificate.
1. Make sure the origin has a valid certificate. We already had a Let's Encrypt certificate, but
renewing it via HTTP-01 behind Cloudflare is fiddly, so we replaced it with a Cloudflare Origin CA
certificate (SSL/TLS, Origin Server, Create Certificate), valid for example.com and *.example.com.
Origin CA certificates are trusted only by Cloudflare, which is fine because only Cloudflare connects
to the origin.
server {
listen 443 ssl http2;
server_name www.example.com;
ssl_certificate /etc/nginx/tls/cf-origin-example.com.pem;
ssl_certificate_key /etc/nginx/tls/cf-origin-example.com.key;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
2. Switch the zone to Full (strict) in SSL/TLS, Overview. Order matters: install the certificate first, then change the mode, or Cloudflare returns 526 (invalid SSL certificate) until the origin is ready.
3. Move the HTTP to HTTPS redirect to the edge. We turned on Always Use HTTPS in Cloudflare and kept the origin port 80 redirect only as a fallback. Since the edge now upgrades visitors before they reach the origin, the origin redirect no longer triggers for proxied traffic.
4. Make the app trust the right header. Express was building absolute URLs as http:// because it
saw the connection from Nginx as plain HTTP. Nginx now passes X-Forwarded-Proto, and Express is told
to trust exactly one proxy hop:
// server.js
const app = express();
app.set('trust proxy', 'loopback'); // only trust X-Forwarded-* from Nginx on localhost
Alternatives and trade-offs
- Keep Flexible and remove the origin redirect. Stops the loop, but traffic between Cloudflare and the origin travels unencrypted across the internet. We did not consider that acceptable.
- Full (not strict). Encrypted but does not authenticate the origin, so a network attacker could impersonate it. Use only as a short transitional step.
- Origin redirect based on
X-Forwarded-Protoor theCF-Visitorheader. Works under Flexible, but those headers can be spoofed by anyone who reaches the origin directly, so only rely on them when the origin accepts traffic from Cloudflare IPs only.
How to test the fix
# Through Cloudflare: one 200, no loop
curl -sIL https://www.example.com/ | grep -iE '^(HTTP|location)'
# Plain HTTP through Cloudflare: one 301 from the edge, then 200
curl -sIL http://www.example.com/ | grep -iE '^(HTTP|location)'
# Origin directly over HTTPS with the Origin CA cert (not publicly trusted, so pass the CA)
curl -sI https://www.example.com/ --resolve www.example.com:443:198.51.100.40 \
--cacert cloudflare-origin-ca-rsa-root.pem
Confirming in production
- Cloudflare Analytics showed 3xx responses for
wwwfalling from 62 percent of all responses to under 2 percent (legitimate redirects only) within five minutes of the change. - The edge served 200 for
/and our five most visited pages from four locations in a synthetic check. - Origin access logs showed every request arriving on port 443 with
$ssl_protocolset to TLSv1.3. - No 525 or 526 errors in the following week.
Regression guard
- The zone settings live in Terraform with the Cloudflare provider, so the SSL mode is code-reviewed and cannot silently drift back:
resource "cloudflare_zone_settings_override" "example" {
zone_id = var.zone_id
settings {
ssl = "strict"
always_use_https = "on"
min_tls_version = "1.2"
}
}
- A synthetic monitor fails if the homepage needs more than one redirect hop.
- A calendar reminder 30 days before the Origin CA certificate expires, plus a monitor on the origin certificate's expiry date.
Security implications
Flexible mode looks like HTTPS to visitors, with a padlock in the address bar, while the origin leg is
plain HTTP. Credentials, cookies and form data cross the internet unencrypted between Cloudflare and
the origin. Moving to Full (strict) closes that gap. We also restricted the origin firewall to
Cloudflare's published IP ranges and enabled Authenticated Origin Pulls, so nobody can bypass
Cloudflare and talk to the origin directly or spoof X-Forwarded-Proto.
Prevention checklist
- Check the zone's SSL/TLS mode before turning on the proxy for an existing site.
- Use Full (strict) with a valid origin certificate; never leave production on Flexible.
- Do HTTP to HTTPS redirects at the edge, and make origin redirects proxy-aware.
- Lock the origin down to Cloudflare IP ranges.
- Manage zone settings as code.
References
- Cloudflare: encryption modes — https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/
- Cloudflare: ERR_TOO_MANY_REDIRECTS troubleshooting — https://developers.cloudflare.com/ssl/troubleshooting/too-many-redirects/
- Cloudflare: Origin CA certificates — https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/
- Cloudflare: Always Use HTTPS — https://developers.cloudflare.com/ssl/edge-certificates/additional-options/always-use-https/
- Express: behind proxies — https://expressjs.com/en/guide/behind-proxies.html
Add a label
Embed this post
<iframe src="https://hibug.me/cloudflare-too-many-redirects-flexible-ssl" width="700" height="420" frameborder="0" loading="lazy"></iframe>
I have spent most of my career on the side of software that pages people at night: payment integrations, web platforms with real traffic, CI/CD pipelines, Kubernetes clusters and the databases underneath them. Somewhere along the way I started keeping a notebook of every bug that cost more than an hour — the exact error, what we ruled out, the real root cause and how we proved the fix. Hibug is where those notes live. Every post follows the same path: Investigate, Explain, Fix, Verify — with the environment spelled out, the fix tested, and the security angle checked.
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.