Point a brand-new web server at the internet and start a timer. Within minutes, often seconds, the first probe lands: a request for /.env, hoping you left a database password in it. Next comes /.git/config, then the URL of a CVE exploit from 2017 that still works on every server nobody got around to patching. None of it is aimed at you personally, but it finds you anyway, because scanners sweep the whole internet looking for the same short list of mistakes.
You can refuse that traffic on day one, without deploying a heavy ruleset that trips over your own customers. The approach is called virtual patching: block the exact request shapes of known exploits and leave everything else alone. CrowdSec gives you an open-source WAF that does exactly this, on Nginx and on whatever else you run in front of your apps.
This is the first post in a series on running that WAF on Nginx. Here we take a single Ubuntu box from a bare apt install to a working WAF in virtual-patching mode. It’s the safest way to start, because it blocks known-bad requests without second-guessing legitimate traffic. Later posts add the OWASP Core Rule Set and the things a classic WAF can’t do.
Why a new WAF, and why now
Three reasons to do this now rather than someday.
ModSecurity’s Nginx era is ending. If you have run a WAF on Nginx before, it was probably ModSecurity. F5 ended support for the Nginx ModSecurity WAF module on March 31, 2024. The engine still runs, but the maintained, first-party path is gone, and rebuilding a dynamic module against every Nginx release was never anyone’s idea of fun. ModSecurity earned its place; it is not the villain of this story. But if you are choosing a WAF for Nginx in 2026, you want something actively maintained and open source.
Virtual patching lets you start without fear of false positives. The usual reason people put off a WAF is exactly that fear: a broad ruleset that blocks real customers the day it goes live. Virtual patching sidesteps it. It matches the specific shapes of known exploits and known-bad paths, so legitimate traffic is never in question. You get real protection immediately, with almost nothing to tune. When you want broader, anomaly-based coverage, that’s the OWASP Core Rule Set, which is the subject of the next post. It comes deliberately later precisely because it does need tuning.
It is not tied to Nginx. The WAF logic lives in the CrowdSec agent, not in your web server. A thin bouncer plugs into whatever sits in front of your apps: Nginx and OpenResty here, but equally HAProxy, Traefik, Caddy, or Envoy. Change proxies, or run several at once, and the same rules and the same dashboard follow you. You are not buying into one web server’s plugin ecosystem.
CrowdSec is free and open source: no license key, no trial clock, and nothing metered per request. And it does something ModSecurity never did: every blocked attack becomes a signal the whole community benefits from.
What “CrowdSec’s WAF” actually is
Two pieces do the work, and it helps to know which is which before you start typing commands.
- The AppSec component is an HTTP server bundled inside the CrowdSec agent. It holds the rules and returns a verdict (allow or block) for any request it is shown. It does not sit inline with your traffic.
- The Nginx bouncer is a small Lua module inside Nginx. It intercepts each request, shows a copy to AppSec, and enforces whatever verdict comes back.
client ──► nginx ──► nginx bouncer ──► AppSec (127.0.0.1:7422)
│ │
│ verdict ◄───┘
▼
allow (200) / block (403)
Key points:
- AppSec makes the decision and the bouncer carries it out. AppSec never blocks anything on its own; the bouncer does.
- The verdict round-trip is local (loopback), so it costs microseconds, not a network hop.
- The same bouncer that enforces WAF verdicts also enforces IP bans from CrowdSec’s community blocklist, so it’s one module doing two jobs.
- We use the Nginx bouncer here. The HAProxy, Traefik, Caddy, and Envoy bouncers speak the same protocol to the same AppSec component, so from Step 3 on, the WAF rules and verdicts are identical whatever proxy you run.
Full reference: the AppSec component docs.
Prerequisites
- An Ubuntu server (we used 24.04/26.04) with Nginx already serving a site.
- Root or sudo.
- Outbound HTTPS to packagecloud.io (packages) and api.crowdsec.net (community signals).
That’s the whole list. You don’t need to stand up a database or rearchitect your reverse proxy.
Step 1 — Install the CrowdSec engine
Add the official repository and install the agent:
curl -s https://install.crowdsec.net | sudo sh # adds the signed CrowdSec repo
sudo apt install crowdsec
$ sudo cscli version
version: v1.7.8-debian-pragmatic-amd64
Codename: alphaga
The installer runs a setup wizard that detects what is already on the box, Nginx included, and wires up log acquisition for you. You can see it did:
# /etc/crowdsec/acquis.d/setup.nginx.yaml
filenames:
- /var/log/nginx/*.log
labels:
type: nginx
source: file
Key points:
- Install from the official packagecloud repo, not Ubuntu’s own crowdsec package, since the distro copy can be years behind.
- The wizard already installed the crowdsecurity/nginx collection, so CrowdSec is parsing your access logs from the first second.
- At this stage CrowdSec detects bad behavior. It does not yet block anything; that’s the bouncer’s job, next.
Step 2 — Install and wire the Nginx bouncer
sudo apt install crowdsec-nginx-bouncer
The package registers itself with the local API and drops its Nginx config in place, with no manual key copying:
$ sudo cscli bouncers list
Name Valid Auth Type
crowdsec-nginx-bouncer-1783001915 ✔️ api-key
Prove the enforcement path works before adding any WAF rules. Ban your own loopback for two minutes and watch Nginx start refusing you:
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/ # 200
$ sudo cscli decisions add --ip 127.0.0.1 --duration 2m --reason test
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/ # 403
$ sudo cscli decisions delete --ip 127.0.0.1
$ curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/ # 200
200 → 403 → 200. The bouncer is live and enforcing decisions. Now we give it a WAF to consult.
Step 3 — Deploy the WAF in virtual-patching mode
This is the core of the setup, and the reason to start here rather than with a big generic ruleset.
Virtual patching blocks requests that match specific, known-bad shapes: the URL of a published CVE exploit, a request for /.env, an attempt to read /.git/config. These patterns have no legitimate reason to appear in your traffic, so the false-positive risk is close to zero. You are not guessing whether a request is malicious; you are recognizing an exploit you already know by name. That makes it the safe first move: turn it on in blocking mode on day one and sleep fine.
Install the WAF collections:
sudo cscli collections install \
crowdsecurity/appsec-virtual-patching \
crowdsecurity/appsec-generic-rules
Install both together — the default AppSec config draws on rules from each, and the two collections give you the full virtual-patching set (191 rules on our box).
Tell the AppSec component to listen, by creating /etc/crowdsec/acquis.d/appsec.yaml:
appsec_config: crowdsecurity/appsec-default
labels:
type: appsec
listen_addr: 127.0.0.1:7422
source: appsec
Reload and confirm the WAF is listening:
$ sudo systemctl reload crowdsec
$ sudo ss -lntp | grep 7422
LISTEN 0 4096 127.0.0.1:7422 0.0.0.0:* users:(("crowdsec",...))
Finally, point the Nginx bouncer at it. The bouncer ships with the WAF switched off (APPSEC_URL empty); one line turns it on:
sudo sed -i 's|^APPSEC_URL=.*|APPSEC_URL=http://127.0.0.1:7422|' \
/etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
sudo systemctl reload nginx
Key points:
crowdsecurity/appsec-defaultis the config that carries the virtual-patching rules and the health-check test rule, all in one config, ready to go.- The listener is loopback-only (
127.0.0.1) because AppSec and Nginx share this host. On a distributed setup you would bind a private interface instead, which is a topic for a later post. - Reusing the bouncer’s existing API key for AppSec is automatic. There is no second key to create.
Step 4 — Prove it blocks real attacks
Fire a few malicious requests at the server and one honest one:
.env access -> 403
phpunit CVE-2017-9841 -> 403
git config leak -> 403
benign homepage -> 200
Then read the WAF’s own counters:
$ sudo cscli metrics show appsec
Appsec Engine | Processed | Blocked
127.0.0.1:7422/ | 6 | 3
Rule ID | Triggered
crowdsecurity/appsec-generic-test | 2
crowdsecurity/vpatch-CVE-2017-9841 | 1
crowdsecurity/vpatch-env-access | 2
Each block is attributed to the exact rule that caught it. Now look at what CrowdSec recorded:
$ sudo cscli alerts list
value | reason | kind | decisions
Ip:203.0.113.42 | crowdsecurity/vpatch-git-config | waf |
Ip:203.0.113.42 | crowdsecurity/CVE-2017-9841 | crowdsec | ban:1
Ip:203.0.113.42 | crowdsecurity/appsec-vpatch | crowdsec | ban:1
Ip:203.0.113.42 | crowdsecurity/vpatch-CVE-2017-9841 | waf |
Ip:203.0.113.42 | crowdsecurity/vpatch-env-access | waf |
Two things happened at once, and this is where CrowdSec pulls ahead of a plain WAF:
kind: waf:each malicious request was refused on the spot with a 403. That’s virtual patching doing its job, per request.kind: crowdsecwithban:1: because the same IP kept probing, CrowdSec’s scenarios aggregated the behavior and banned the address outright. The next request from that IP, to any path, is refused before the WAF even looks at it.
$ sudo cscli decisions list
crowdsec | Ip:203.0.113.42 | crowdsecurity/CVE-2017-9841 | ban | FR | 3h59m
A classic WAF blocks the request. CrowdSec blocks the request and remembers the attacker.
One gotcha: whitelisted source IPs CrowdSec whitelists private and loopback ranges by default, so probes from 127.0.0.1 are blocked (you still get the 403) but produce no alert. Test from a real, external IP, as we did above from 203.0.113.42, if you want to see alerts and bans appear.
There is also an official smoke test that ships with CrowdSec. Requesting the path /crowdsec-test-NtktlJHV4TfBSK3wvlhiOBnl triggers the crowdsecurity/appsec-generic-test rule; the full health-check flow is documented here.
Step 5 — See it in the Console
Blocking attacks from the command line is satisfying, but you will want a dashboard, one place to watch alerts across every server you protect. The CrowdSec Console is that view, and it is free for the community tier.
Enroll this engine (grab an enrollment key from app.crowdsec.net → Security Engines → Enroll):
sudo cscli console enroll --name crowdsec-vm-nginx-waf
sudo systemctl reload crowdsec
Then accept the instance in the Console webapp — enrollment is two-step, and nothing shows up until you click Accept. Turn on richer alert detail while you are at it:
sudo cscli console enable context
sudo systemctl reload crowdsec


Key points:
- You enroll the engine, not each bouncer; bouncers appear in the Console through the engine they report to.
cscli capi statusconfirms the community signals loop: sharing enabled, community blocklist pulling. That blocklist is the payoff of open source: every CrowdSec user’s confirmed attackers become IPs you block before they ever reach you.- The Console is a lens on data that already lives on your box. Turn it off and your WAF keeps working exactly the same.
Where this leaves you
In a few minutes you went from a bare Nginx server to an open-source WAF that:
- refuses known exploit shapes on sight, with virtually no false-positive risk;
- bans persistent attackers automatically, across your whole site;
- pulls a community-sourced blocklist so you benefit from attacks aimed at everyone else;
- reports into a single Console dashboard.
Virtual patching is a floor, not a ceiling. It only catches attacks it already knows by name. To catch the ones it doesn’t, the novel injection or the odd traversal, you bring in the OWASP Core Rule Set, and you decide whether to run it inband (block now) or out-of-band (watch first, block once you trust it). That’s exactly the false-positive tradeoff virtual patching let us skip, and it’s where the next post picks up.
After that: the hooks that let CrowdSec’s WAF do things a signature engine can’t, and how to run the WAF and the engine as a distributed pair across many servers.
You built all of this with software you can read, audit, and run for free. No black box, no vendor lock-in, nothing metered per request: just your server, refusing the requests that were never meant well.
Next in this series: Going further — OWASP CRS inband vs out-of-band with CrowdSec. Full documentation lives at docs.crowdsec.net/docs/appsec, and the rules are open on the CrowdSec Hub.



