Networking & DNS Bug Report

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.

Daniel Tran Daniel Tran @fun_dev · February 06, 2025 · 8 min read · 6 Views
Banner: Cloudflare Flexible SSL and an origin HTTPS redirect creating an endless redirect 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.com to www.example.com; the location was 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-Proto or the CF-Visitor header. 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 www falling 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_protocol set 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

Add a label

#nginx #https #ssl #cloudflare #redirects
Written by
Daniel Tran @fun_dev

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.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.