D
P
0
← All articles Baca dalam Bahasa Indonesia

Caching & CDN: Deployed, but Nothing Changed

Login Returns `302` to /account/ but the Browser Stays Logged Out: Cloudflare Flexible SSL Makes `is_ssl()` False at the Origin

· · 5 min read
Login Returns `302` to /account/ but the Browser Stays Logged Out: Cloudflare Flexible SSL Makes `is_ssl()` False at the Origin

The most exhausting bugs are not the loud ones. They are the ones where the server reports success and the result is still nothing. On a client membership site, the login form worked perfectly according to every log I had. According to the browser, nothing had ever happened.

The symptom: the server says success, the browser asks who you are

The flow was ordinary. The login form POSTs to admin-post.php, the handler verifies credentials, then redirects to /account/.

Here is what the network tab showed:

POST /wp-admin/admin-post.php   302
Location: /account/
cf-cache-status: DYNAMIC

All of it correct. A 302, not a 200 carrying an error message. The right redirect target. And cf-cache-status: DYNAMIC confirmed the response was not some stale page served from a Cloudflare edge, which killed the "it is just cache" theory before it could start.

Then the browser followed the redirect to /account/, and /account/ threw it straight back to the login page. Loading /wp-admin/ by hand did the same. Credentials valid, session created server side, yet as far as the next request was concerned I was still a stranger.

The auth cookie never stuck.

First trap: two illnesses wearing one symptom

Before I get to the real cause, there is an hour I lost by assuming every failure came from a single source.

That site runs a hand rolled login throttle, and the throttle had already locked out our tester machine's IP after dozens of repeated login attempts during debugging. So some attempts failed because the cookie never stuck, and other attempts failed because we were genuinely blocked. Two different causes, one identical outcome: back to the login page.

While those two causes stayed mixed together, no experiment could be read. I would change one thing, see it improve, then watch it regress for no reason, when the only real variable was whether the throttle happened to be active.

What broke the deadlock was not a clever idea. It was boring discipline: disable the throttle temporarily, then test from a genuinely clean network. After that the symptom became consistent, and a consistent bug is the only kind worth debugging.

The root cause: the origin had no idea the visitor arrived over HTTPS

Once the symptom held still, I checked the one thing I had been treating as obviously true. What does PHP actually see about the request protocol?

add_action('init', function () {
    error_log(sprintf(
        'is_ssl=%s | XFP=%s | HTTPS=%s',
        var_export(is_ssl(), true),
        $_SERVER['HTTP_X_FORWARDED_PROTO'] ?? 'none',
        $_SERVER['HTTPS'] ?? 'unset'
    ));
});

The output made every piece fall into place at once:

is_ssl=false | XFP=https | HTTPS=unset

Visitors reach the site over HTTPS. Cloudflare terminates that encrypted connection and forwards the request to the origin as plain HTTP. That is Flexible SSL. From the visitor's side the padlock is green, but from the origin's side the request looks like ordinary HTTP traffic. $_SERVER['HTTPS'] is never set, so is_ssl() returns false.

And is_ssl() is not a trivial helper in WordPress. It feeds into how authentication cookies get set, including whether they are marked as safe for an HTTPS context. Once the origin misjudges the protocol, cookie decisions are made for a world other than the one the browser is actually in. The result is a cookie that does not match the context asking for it, and every subsequent request is treated as logged out again.

That is why everything looked successful. Password verification really did pass. The redirect really was sent. The only step that never happened was the very last one, and that step appears in no log at all.

The fix: give the origin the truth about the protocol

Cloudflare still sends the original information in the X-Forwarded-Proto header. The origin just needs to be taught to read it, and that lesson has to happen before WordPress makes any decision of its own.

Put this in wp-config.php, above the line that requires wp-settings.php:

if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
    && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}
 
/* That's all, stop editing! */
require_once ABSPATH . 'wp-settings.php';

Placement matters. Drop this into a theme's functions.php or a plugin and some decisions are already made before your code ever runs. It belongs at the very top of the bootstrap, while nothing has read is_ssl() yet.

Then check the log again. is_ssl=true, the cookie sticks, and /account/ stops throwing people back out.

Worth being honest about what this is. It makes PHP see the correct protocol, but the traffic between Cloudflare and the origin is still plain HTTP. Once the origin can serve HTTPS on its own with a valid certificate, the SSL mode is the thing that should be raised, and this shim drops back to being nothing more than a safety net.

One step not to skip: bypass cache for authenticated paths

There is one more thing to lock down before calling this done. Pages whose content differs per user must never be served from the edge cache. If /account/ or the admin area slips into cache even briefly, one visitor can be handed another visitor's page, and that is a far more expensive class of bug than a failing login.

So create a Cache Rule with a bypass action for authenticated paths, and do not lean on the assumption that "WordPress is dynamic, it will not be cached anyway". Prove it from the headers:

curl -sI https://your-site.com/account/ | grep -i "cf-cache-status\|cache-control"

What you want to see there is BYPASS or DYNAMIC, never HIT.

What I took away