Skip to main content
Cybersecurity & Hardening

CrowdSec for WordPress: Bouncing Bad IPs at the App Layer

How I wire CrowdSec's WordPress bouncer to the LAPI on the same server, what bouncing level to pick, and the failure modes I've watched it catch in production.

Published Updated 8 min read

The first time I rolled CrowdSec out across a fleet of WordPress sites, I turned on the firewall bouncer, saw the auth log go quiet, and assumed I was done. A week later one of those sites still got hit by a credential-stuffing attack that crawled wp-login at a polite pace from a few hundred residential IPs. The firewall bouncer never flagged a single one because the rate per IP was too low. CrowdSec WordPress integration, the plugin that actually sees the WordPress request, is what closes that gap.

This post walks through wiring the WordPress bouncer to a CrowdSec agent already running on the same server. It assumes you’ve installed the agent and the firewall bouncer following the CrowdSec installation and server protection walkthrough. If you haven’t, start there. The WordPress plugin without an agent on the box is a dashboard with nothing to draw on.

I’ve kept this short on purpose. The plugin is genuinely a five-minute install once the agent is in place. Most of the value here is in the small set of decisions you make during that five minutes, the failure modes I’ve seen in production, and the things I’d skip.

Why a WordPress bouncer when you already have a firewall bouncer

The two bouncers see different traffic, and that’s the point.

The firewall bouncer (running through iptables or nftables) drops packets at the kernel before they reach Nginx. It’s fast, cheap, and handles the bulk of the noise: SSH brute-force, port scans, generic web exploit probes. Once an IP is flagged by the agent, the firewall stops it cold.

The WordPress bouncer runs inside PHP, on every request that makes it past the firewall. It checks the visiting IP against the LAPI’s decision list and either lets the request through, shows a captcha, or returns a block page. The reason you want it is that some scenarios only fire once a request has hit a specific WordPress endpoint, things like wp-login brute-force, xmlrpc abuse, REST API enumeration. The agent can flag an IP based on what WordPress logged, and the WordPress bouncer enforces that decision in PHP before the slow login code path runs.

You also get the mid-attack scenario the firewall bouncer can’t catch: a botnet hitting wp-login at a low rate per IP from thousands of IPs. The agent’s WordPress collection aggregates across IPs, flags the pool, and the bouncer enforces the decision on the next request.

Installing the CrowdSec WordPress plugin

The plugin is in the official WordPress marketplace under the name “CrowdSec.” Search, install, activate, the usual flow. No surprises here.

CrowdSec WordPress plugin installed and configured in the WordPress admin showing the bouncer settings

The WordPress admin after the plugin is installed. The plugin’s settings page is where the LAPI URL, API key, and bouncing level get configured.

Before you open the plugin’s settings, you need to register the bouncer with the local CrowdSec agent and get an API key. SSH into the server (where CrowdSec is already running) and run:

sudo cscli bouncers add wordpress-bouncer

CrowdSec prints the API key once. Copy it now. There is no way to read this key back later; if you lose it, you delete the bouncer with cscli bouncers delete wordpress-bouncer and add it again to generate a new one. I’ve watched more than one engineer on a shared call paste the key into Slack, lose the message, and have to re-create the bouncer. Save it to your password manager the second it appears.

Then restart the CrowdSec service so the new bouncer registration is picked up:

sudo systemctl restart crowdsec

Now back to the WordPress admin. Open the plugin’s settings page and fill in three fields:

  • LAPI URL: http://localhost:8080
  • Bouncer API key: the key cscli printed above
  • Bouncing level: Flex

That’s it for the connection. Save the settings and the plugin will immediately start querying LAPI on every request.

Warning: The LAPI URL must point to localhost. Do not bind LAPI to a public interface and do not point the WordPress plugin at a public hostname unless you’ve put the LAPI behind TLS and authentication. Exposing port 8080 to the internet is the single most common CrowdSec misconfiguration I run into during audits, and it gives anyone on the internet a way to query your decision list and the metadata that comes with it.

Picking the right bouncing level

The plugin offers three levels: Disabled, Flex, and Normal.

  • Disabled is exactly what it sounds like, useful as a debugging step but pointless as a steady state.
  • Flex shows a captcha to flagged IPs and blocks repeat offenders. This is the right default.
  • Normal blocks every flagged IP outright. Use when you’re under active attack or when false positives are an acceptable cost (a staging site, an internal tool).

I run Flex on every customer-facing WordPress site. The captcha layer means a legitimate user behind a CGNAT IP that another customer of the ISP misbehaved on isn’t permanently locked out, they solve a hCaptcha and continue. The block-everything mode is what I switch to during an incident, then back to Flex once the dust settles.

What the plugin actually does on every request

Worth understanding so you know what you’re paying for performance-wise.

On each WordPress request, the plugin grabs the client IP, checks its in-memory cache for a decision, and either acts on the cached decision or fires a request to LAPI on http://localhost:8080 to get a fresh one. The cache TTL is configurable; the default is short enough that newly flagged IPs are blocked within seconds and long enough that the plugin doesn’t hammer LAPI on a busy site.

If the IP has a Block decision, the plugin renders the block page and exits before WordPress’s own routing kicks in. No database queries, no plugin loading, no themes. The blocked request never gets near the slow parts of WordPress. If the IP has a Captcha decision, the plugin renders the captcha page until the user solves it, then sets a cookie and lets the request through. If there’s no decision, the plugin gets out of the way and WordPress runs as normal.

The cost on a clean request is microseconds for the cache lookup. On a cache miss it’s a localhost HTTP call, which on a typical server adds a millisecond or two. On a real attack, the plugin saves you orders of magnitude of CPU because it’s terminating malicious requests before WordPress core boots.

Verifying the integration is actually working

Don’t skip this step. I’ve seen plugins installed, configured, and never tested, where the API key was wrong and the plugin was silently failing open. The check takes 30 seconds.

From a different network (a phone on cellular, a friend’s laptop, anywhere the IP isn’t your office), trigger a fake “I’m a bot” decision from the agent and confirm WordPress now blocks you:

sudo cscli decisions add --ip <test-ip> --duration 5m --reason "manual test"

Replace <test-ip> with an IP you control on a different network. Visit the WordPress site from that IP. You should see the plugin’s block page within a couple of seconds (give the plugin’s cache TTL time to refresh). When you’ve confirmed the block, remove the decision:

sudo cscli decisions delete --ip <test-ip>

If the block page doesn’t appear, the most likely culprits are: API key copied wrong, LAPI URL pointing somewhere other than localhost, or the plugin’s cache hasn’t refreshed yet. Double-check those three before assuming a deeper problem.

What this protects you from, and what it doesn’t

The plugin plus the firewall bouncer plus the agent’s WordPress collection covers a meaningful chunk of the attack surface I see hitting customer sites:

  • wp-login brute-force from single IPs and from low-rate distributed campaigns
  • xmlrpc.php abuse (still a thing in 2026, somehow)
  • WordPress REST API user enumeration
  • Bots scanning for known-vulnerable plugin paths
  • Generic web shells trying common upload paths

What it doesn’t cover, and what you still need other layers for:

  • Compromised credentials. If a real user’s password leaks, the bouncer doesn’t see anything wrong with the login. 2FA is the answer here, ideally with a tool like 2FAuth or pushed through Authentik.
  • Vulnerable plugin code paths. CrowdSec’s collection catches some known exploits, not all of them. Patch your plugins, monitor your CVE feed, and treat the bouncer as a layer not a solution.
  • Server-level access (SSH, panel access). That’s the Linux server security baseline and the firewall bouncer’s job, not the WordPress plugin’s.
  • Locked-out admin recovery. If you do get locked out by an aggressive bouncing rule, the WordPress admin account recovery walkthrough covers the database-level reset.

For the bigger picture of how all these layers fit together on a WordPress host, the comprehensive WordPress security guide is the long-form companion to this post. And the Nginx security hardening post covers the headers and request rules I run in front of every WordPress site.

Closing the loop

A working CrowdSec stack on WordPress is three pieces: the agent watching the logs, a firewall bouncer dropping packets at the kernel, and the WordPress plugin enforcing decisions inside PHP. Each layer is cheap to run and each one catches things the others miss.

The WordPress plugin specifically is what turns CrowdSec from a server-protection tool into something that knows what’s happening at the application layer. If you’ve installed the agent and the firewall bouncer and stopped there, you’ve done two-thirds of the job. Spend the five minutes adding the WordPress bouncer, set it to Flex, run the verification test, and move on. The auth log will be quiet, the wp-login attempts will be quiet, and you’ll free up an evening you used to spend chasing brute-force noise.

Watch on YouTube

Video walkthrough

Prefer the screen-recording version of this guide? Watch it on YouTube — opens in a new tab so the player only loads when you ask for it.

Frequently Asked Questions

Want this handled, not just understood?

Reading the playbook is one thing. Running it on production at 2am is another. If you'd rather have me run it for you, the door is open.

Apply for Access