Production Incident Retrospective: Dual-layer Fix for WordPress Site 308 Redirect Loops under CF Flexible Mode

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-Proto trust configuration in wp-config.php.

All were proactively remediated.

Lessons Learned

  1. 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.
  2. 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.
  3. 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.
  4. 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.