diff --git a/Sources/Dev/2026-06-29-contributor-wp-admin-lockout-debug-cloudflare-cache-bypass-cooki.md b/Sources/Dev/2026-06-29-contributor-wp-admin-lockout-debug-cloudflare-cache-bypass-cooki.md new file mode 100644 index 0000000..a4fcefd --- /dev/null +++ b/Sources/Dev/2026-06-29-contributor-wp-admin-lockout-debug-cloudflare-cache-bypass-cooki.md @@ -0,0 +1,53 @@ +--- +created: '2026-06-29' +path: Sources/Dev +project: cloudflare-cache-strategy +tags: +- pbs +- cloudflare +- caching +- wordpress +- wordfence +- ultimate-member +- redis +- auth +type: 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. \ No newline at end of file