wiki-vault/Sources/Dev/2026-06-29-contributor-wp-admin-lockout-debug-cloudflare-cache-bypass-cooki.md

6.4 KiB

created path project tags type
2026-06-29 Sources/Dev cloudflare-cache-strategy
pbs
cloudflare
caching
wordpress
wordfence
ultimate-member
redis
auth
session-notes

Outcome

A content contributor (Italy-based, username sheridanbf731) was repeatedly locked out of /wp-admin/ on production. What presented as an "IP block / login" problem turned out to be three stacked, unrelated issues that had to be peeled apart in sequence. All three were resolved; contributor confirmed able to log in, move between front-end and admin, and log out without being blocked.

Root causes, in the order they were found and fixed:

  1. Editor role bounced from wp-admin — Ultimate Member's um_block_wpadmin_by_user_role (hooked to init @ priority 99) was enforcing a stale "Can access wp-admin? No" value. The per-role setting had been flipped to Yes and the DB + UM debug panel both showed Yes, but the function was reading a stale Redis-cached role object. wp cache flush cleared it and the bounce stopped. (The role setting was correct three steps before the fix actually took — the cache was the blocker.)

  2. reauth=1 session-invalidation loop + Wordfence lockout cascade — the real recurring problem. A Cloudflare cache rule ("Bypass cache for admin") only bypassed cache for /wp-admin and /wp-login.php paths. Logged-in users browsing front-end pages (homepage, recipes — where UM/role redirects send them) were served cached, logged-out pages. WordPress saw the cookie/cache mismatch, threw reauth=1, and background admin-ajax.php requests fired with the confused session state. Wordfence logged those AJAX requests as failed logins → two of them tripped the brute-force lockout. Fixed by adding http.cookie contains "wordpress_logged_in_" to the cache-bypass rule, so Cloudflare never serves cache to any logged-in user on any path.

  3. Two broken conditions in the same Cloudflare rule — the n8n and gitea bypass conditions used URI Path contains "https://n8n...", which can never match (a URI path never contains a hostname). Corrected syntax is http.host eq "n8n.plantbasedsoutherner.com". Travis chose to leave the originals in place for now rather than edit them tonight; the Host-based correction is captured here as a follow-on.

Topics Covered

  • Wordfence block-page diagnosis vs. manual block list (the IP was never manually blocked; it was a failed-login lockout)
  • Cloudflare IPs in logs: all visitor traffic appears to Wordfence as Cloudflare edge IPs (172.69.x.x, 172.70.x.x), rotating and geolocated to wherever the edge node is (Milan, Tallinn). Wordfence counts failed logins per IP, so a shared Cloudflare IP pools everyone's failures onto one counter, making lockouts fire "faster than the threshold."
  • Ultimate Member two-layer access control: per-role "Can access wp-admin?" toggle vs. role priority (UM uses the highest-priority role's settings when a user has multiple). Contributor had a single role, so priority wasn't the issue here.
  • Redirect-source diagnosis: x-redirect-by: WordPress response header indicates a core wp_redirect() call (theme/snippet/mu-plugin) rather than a self-labeling plugin.
  • wp eval hook-dump technique to list every callback on init / admin_init / template_redirect with file+line for closures — this is what surfaced um_block_wpadmin_by_user_role by name.
  • Confirmed clean: all WPCode snippets, stock Astra theme + functions.php, mu-plugins (only Elementor Safe Mode), and grep -rni "setcookie" across themes/mu-plugins (empty — confirms cookie-busting code was replaced by the n8n publish-webhook approach).
  • The Cloudflare "Verify you are human" challenge on /wp-login.php (Turnstile/managed challenge, __cf_chl_f_tk token) is normal, benign bot protection — risk-scored per session, so trusted browsers skip it and fresh/private sessions get it. Not part of the bug.

Key Learnings

  • When a role/permission change "doesn't take" but the setting clearly reads correct, flush the object cache (Redis) before assuming the setting is wrong. Object cache + role meta is a classic stale-read trap. This alone would have saved many steps.
  • Cache-bypass for logged-in WordPress users must be keyed on the wordpress_logged_in_ cookie, not on URL path. Path-only bypass leaves front-end pages cacheable, which poisons logged-in sessions and produces reauth=1 loops. This is the single most important fix from the session.
  • Wordfence behind Cloudflare must be told how to read real client IPs (Settings → "How does Wordfence get IPs?" → CF-Connecting-IP / X-Forwarded-For). Until set, all per-IP logic (lockouts, blocks, allowlists) operates on shared Cloudflare edge IPs and behaves erratically. Do not allowlist or block 172.69.x.x / 172.70.x.x ranges — those are Cloudflare, not visitors. This remained un-done at session end and is the top follow-on.
  • Cloudflare rule field discipline: URI Path is the path only; matching a subdomain requires the Host field (http.host eq ...).
  • Stacked symptoms can share one root: the "rotating IPs," "faster than 3 attempts," and "blocked on logout" complaints were all downstream of the same Cloudflare-cache + shared-IP situation.

Follow-ons

  • Set Wordfence "How does Wordfence get IPs?" to the CF-Connecting-IP (or X-Forwarded-For) option so Wordfence sees real client IPs. Highest-value remaining cleanup; safe to do any day. Until then, per-IP blocks/allowlists are unreliable.
  • Fix the n8n/gitea cache-bypass conditions in the "Bypass cache for admin" rule — switch from the non-functional URI Path contains "https://..." to http.host eq "n8n.plantbasedsoutherner.com" / http.host eq "gitea.plantbasedsoutherner.com". Test on staging first; verify n8n + gitea still load and that cache still bypasses correctly for them.
  • Rotate the hardcoded n8n webhook secret found in the publish_post WPCode snippet (HMAC secret was exposed in chat) and move it to wp-config.php or an env var instead of inline in the snippet.
  • Consider replicating the corrected Cloudflare cache rule on staging and validating the full logged-in cycle there before treating the production fix as final.
  • Optional: if the /wp-login.php human-check becomes friction for the three known contributors, tune via a Cloudflare WAF skip rule for known users — low priority, currently working as intended.