On December 3rd 2025 a critical React Server Components flaw nicknamed React2Shell went public: unauthenticated remote code execution in a framework a huge slice of the web's frontends depend on. CrowdSec shipped vpatch-CVE-2025-55182 to the hub within hours of the disclosure, and their writeup of that response is worth your time. Anyone running the AppSec component was one cscli hub upgrade away from blocking exploitation attempts before most teams had finished reading the advisory.
That's the case for the CrowdSec WAF in one story. Log-based scenarios, the classic CrowdSec mode, see an attack after the web server has already answered. The AppSec component moves inspection inline: nginx forwards each request to a local CrowdSec endpoint before passing it to your app, and a malicious request dies as a 403 without your application ever parsing it.
This guide wires the whole thing on Ubuntu 24.04 with nginx, using the two-layer rule setup I run in production. If you're weighing this against a network IDS, my Suricata vs CrowdSec comparison covers where each one earns its place.
Prerequisites and the nginx bouncer
You need a running Security Engine on the 1.7 line; the CrowdSec install walkthrough gets you there on Debian or Ubuntu in about ten minutes. Then add the bouncer:
sudo apt install crowdsec-nginx-bouncer
It's a Lua bouncer that hooks nginx's access_by_lua phase, checks each client against active ban decisions and, once we switch AppSec on, forwards requests for inspection. The installer registers it with the local API on your behalf. Worth noting for fresh builds: LumaDock offers an nginx + CrowdSec template at ordering time that deploys this exact pairing in seconds, engine and WAF-capable bouncer wired from first boot, so on a new VPS you can jump straight to the rule layers below.
Enable the AppSec component
The AppSec component ships inside the engine; it needs rules and an acquisition to wake up. Install both rule collections:
sudo cscli collections install crowdsecurity/appsec-virtual-patching
sudo cscli collections install crowdsecurity/appsec-crs
Then create the acquisition that starts the listener, exactly as the virtual patching and CRS guide documents:
# /etc/crowdsec/acquis.d/appsec.yaml
appsec_configs:
- crowdsecurity/appsec-default
- crowdsecurity/crs
labels:
type: appsec
listen_addr: 127.0.0.1:7422
source: appsec
sudo systemctl restart crowdsec
The component now listens on 127.0.0.1:7422. Point the bouncer at it by setting APPSEC_URL=http://127.0.0.1:7422 in /etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf, then reload nginx. The same file carries a failure-action setting that decides what happens when the AppSec endpoint is unreachable, fail open or fail closed. I run fail open on anything user-facing, because a hung WAF taking the whole site down is a worse Tuesday than a brief inspection gap. Fail closed belongs on admin panels and other low-traffic, high-value paths.
How the two rule layers split the work
Layer one is crowdsecurity/appsec-virtual-patching, running in-band. These rules are evaluated synchronously before the request proceeds, and they block on match. They target specific known CVEs plus the permanent background noise of probes for /.env files, exposed .git directories and their cousins. Because each rule matches one narrow exploit pattern, false positives sit near zero, which is what makes inline blocking safe to leave on.
Layer two is crowdsecurity/appsec-crs: the OWASP Core Rule Set, delivered through the Coraza engine, running out-of-band. Requests get analyzed asynchronously after the response has already gone out, so it adds nothing to response time. Matches feed the crowdsecurity/crowdsec-appsec-outofband scenario, which bans an IP after five rule violations. The attacker gets a handful of free probes and then loses access to everything the server runs, which is a trade I'll take every day over inline CRS false positives.
Why CRS ships non-blocking and when I move it in-band
Generic rulesets and false positives are inseparable. CRS will flag a legitimate user pasting an SQL snippet into a support form, a base64 blob in a cookie, all sorts of ordinary weirdness, and a WAF that inline-blocks those on day one gets disabled by day three. CrowdSec's default (detect asynchronously, ban only repeat offenders) is the right one, and the maintainers have been explicit that out-of-band is the intended default for CRS. My rule of thumb: run it out-of-band for two or three weeks, read what fires via cscli alerts inspect, then move CRS in-band only for paths that never receive user-generated content. Admin panels and login endpoints are the usual candidates. The docs cover the blocking variant when you get there.
Test the WAF with a .env probe
curl -I http://YOUR_SERVER_IP/.env
Expect HTTP/1.1 403 Forbidden. That's the in-band layer answering; the request never touched your application. For the out-of-band layer, the docs ship a set of CRS trigger payloads (script tags and SQL fragments in query strings), and you should fire those from a disposable IP because five of them earn a real four-hour ban. Then look at what got recorded:
sudo cscli alerts list
sudo cscli metrics
Alerts show the offending URIs and matched rule IDs, and metrics grow an appsec section once traffic flows through the component. Two pointers for later: if a legitimate user ever reports hitting a 403 wall, that's a decision to find and lift, covered in fixing the CrowdSec access forbidden page, and if you're nervous about enabling any of this on a production box, my safe rollout guide shows how to watch rules fire in simulation before they're allowed to bite.
WordPress rules and the CVE Explorer
If the box serves WordPress, add the appsec-wordpress collection, virtual patching aimed at the most-exploited WordPress CVEs. WordPress plugins produce vulnerabilities at a pace nginx core never will, and this collection exists because of it.
A related aside I find myself recommending a lot: CrowdSec's CVE Explorer, launched in August 2025, scores 400+ CVEs against real exploitation data observed across the network. When a scary advisory lands, it tells you if anyone is exploiting the thing in the wild yet, which beats triaging from CVSS scores alone. I check it before deciding what gets patched tonight versus this weekend.
Resource cost and the limits of virtual patching
Honesty section. AppSec adds a processing hop: every request makes a loopback round trip to port 7422 before nginx proxies it onward. The in-band rule set is deliberately lean and the CRS work happens off the request path, so on small VPSes with moderate traffic the overhead disappears into the noise; I run this on 2 vCPU machines fronting six-figure daily request counts without it showing on a graph. If you're pushing serious traffic through one box, benchmark before and after like you would with any middleware.
Last thing, and I mean it: a virtual patch is a tourniquet. vpatch-CVE-2025-55182 bought React shops the hours they needed, and the rule shielding your outdated plugin buys you the same, and none of it makes the vulnerable code any less vulnerable. Attackers eventually find encodings and paths rule authors didn't anticipate. Patch the application; let AppSec own the gap between disclosure and your next maintenance window, because that gap is where it's genuinely unbeatable.

