Incident Overview
During today’s routine inspection, a WordPress site was found to be completely inaccessible—the browser displayed ERR_TOO_MANY_REDIRECTS. Investigation revealed a classic 308 redirect loop caused by Cloudflare’s Flexible SSL mode. After fixing the nginx-level redirection, WordPress itself generated a new 301 redirect loop. Subsequent auditing of all Cloudflare-proxied sites uncovered the same vulnerability across multiple sites.
Root Cause Chain
The full redirect loop consists of two interdependent layers—both must be fixed to resolve the issue completely:
Layer 1: nginx 308 Loop
User → Cloudflare (HTTPS) → Origin server (HTTP, Flexible mode) → nginx sees $scheme=http → issues 308 redirect to HTTPS → Cloudflare re-requests origin via HTTP → loop
This issue has been thoroughly analyzed by @fedora-devops in “Cloudflare Pitfalls: The 308–522–429 Trifecta”.
Layer 2: WordPress 301 Loop (Often Overlooked)
Even after fixing the nginx layer, the redirect loop persists—only changing from 308 to 301—and the response headers now include x-redirect-by: WordPress.
Cause: Under Cloudflare’s Flexible SSL mode, PHP receives HTTP requests, so $_SERVER['HTTPS'] is empty. WordPress functions wp_redirect_admin_locations() and redirect_canonical() detect a mismatch between the current protocol (http) and the configured siteurl (https://), triggering a 301 redirect to the HTTPS version—but Cloudflare continues to request the origin over HTTP, restarting the loop.
Remediation Steps
nginx Layer: Use the Client’s Actual Protocol
Replace blind if ($scheme = http) redirects with logic that respects the actual client protocol conveyed via Cloudflare headers:
# Cloudflare forwards the true client protocol in X-Forwarded-Proto
set $real_proto $scheme;
if ($http_x_forwarded_proto) {
set $real_proto $http_x_forwarded_proto;
}
if ($real_proto = http) {
return 308 https://$host$request_uri;
}
This approach is more intuitive than string-concatenation alternatives: $real_proto explicitly represents “the protocol actually used by the client”, making the logic transparent and maintainable.
WordPress Layer: Trust Proxy Headers in wp-config.php
Add the following snippet at the top of wp-config.php, right after <?php:
/* Cloudflare Proxy: Trust X-Forwarded-Proto */
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
This ensures WordPress’s is_ssl() function correctly returns true, preventing spurious protocol-mismatch redirects.
Site-Wide Audit
After resolving the affected site, we audited all Cloudflare-proxied sites:
# Identify vhosts with HTTP→HTTPS redirects but no Cloudflare-aware logic
for f in /path/to/vhost/*.conf; do
has_redirect=$(grep -c 'scheme.*http.*return\|scheme = http' "$f")
has_cf_aware=$(grep -c 'real_proto\|x_forwarded_proto\|do_redirect' "$f")
if [ "$has_redirect" -gt 0 ] && [ "$has_cf_aware" -eq 0 ]; then
echo "RISK: $(basename "$f")"
fi
done
Results revealed:
- Two additional sites with identical nginx misconfigurations (they were unaffected only because their Cloudflare SSL mode happened to be Full, not Flexible).
- Four WordPress sites missing the
X-Forwarded-Prototrust configuration inwp-config.php.
All were proactively remediated.
Lessons Learned
- Cloudflare-proxied sites require dual-layer protection: Both nginx and the application layer (WordPress/PHP) must correctly interpret the client’s protocol. Fixing only one layer is insufficient.
- Don’t wait for failure to audit: When one site fails, immediately audit all similar infrastructure. Infrastructure issues are rarely isolated—they tend to affect multiple systems.
- Cloudflare Flexible SSL is a root cause of many issues: If your origin server has a valid SSL certificate, use Full or Full (Strict) mode instead—eliminating HTTP-origin requests entirely.
- Automated health checks are critical: Without proactive monitoring, such outages may persist undetected for extended periods.
Quick Diagnostic Checklist
| Symptom | Check |
|---|---|
308 redirect loop + server: openresty/nginx |
nginx layer: if ($scheme = http) without Cloudflare-aware logic |
301 redirect loop + x-redirect-by: WordPress |
WordPress layer: missing X-Forwarded-Proto trust in wp-config.php |
| Site works when accessed directly (bypassing Cloudflare), but loops when proxied | Cloudflare SSL mode is Flexible, and origin server enforces HTTPS redirects |
We hope this two-layer resolution guide helps others facing the same issue.