<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>CrowdSec Blog</title>
        <link>https://crowdsec.net/blog</link>
        <description>Latest updates and insights from CrowdSec</description>
        <lastBuildDate>Thu, 10 Sep 2026 08:25:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>CrowdSec Blog</title>
            <url>https://cms.crowdsec.net/wp-content/uploads/2024/10/crowdsec-logo.png</url>
            <link>https://crowdsec.net/blog</link>
        </image>
        <item>
            <title><![CDATA[How to Block Bots, Headless Browsers, and Scrapers on Nginx with CrowdSec]]></title>
            <link>https://crowdsec.net/blog/nginx-bot-protection-crowdsec</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/nginx-bot-protection-crowdsec</guid>
            <pubDate>Tue, 08 Sep 2026 09:29:58 GMT</pubDate>
            <description><![CDATA[<p>Set up open-source Nginx bot protection with CrowdSec to block scrapers, headless browsers, AI crawlers, and other unwanted automated traffic.</p>
]]></description>
            <content:encoded><![CDATA[
<figure class="wp-block-image aligncenter size-full"><img loading="lazy" decoding="async" width="500" height="730" src="https://cms.crowdsec.net/wp-content/uploads/2026/09/intro-meme.png" alt="" class="wp-image-6845" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/09/intro-meme.png 500w, https://cms.crowdsec.net/wp-content/uploads/2026/09/intro-meme-205x300.png 205w" sizes="auto, (max-width: 500px) 100vw, 500px" /></figure>



<p class="wp-block-paragraph"><code>Robots.txt</code> is a sign that says &#8220;please don&#8217;t&#8221; in a language only polite people read. It has worked exactly as well as you&#8217;d expect for thirty years. Unfortunately, defending yourself against bots that are unwilling to comply is something that is notably hard, and even more so if you don’t have thousands of dollars and the willingness to give up privacy.</p>



<p class="wp-block-paragraph">But fear not, CrowdSec comes in to save the day as CrowdSec 1.8 ships bot protection!</p>



<h2 class="wp-block-heading">What the challenge actually does</h2>



<p class="wp-block-paragraph">Bot detection ships inside CrowdSec&#8217;s WAF. While the WAF itself focuses more on virtual patching, CVE rules, and generic attack signatures (see&nbsp; <a href="https://doc.crowdsec.net/docs/next/appsec/quickstart/nginxopenresty" target="_blank" rel="noreferrer noopener">Open-Source WAF for Nginx on Ubuntu with CrowdSec</a>), we will focus purely on the bot detection feature.</p>



<p class="wp-block-paragraph">The bot detection answers one question: what is on the other end of the wire, a real browser or a script wearing one as a costume? It does that with a proof-of-work and device-fingerprint page in front of your app, in four beats:</p>



<ol class="wp-block-list">
<li>A request arrives with no valid challenge cookie. Instead of your page, the engine serves a small challenge page.</li>



<li>That page runs three things in the browser: the open-source <a href="https://github.com/antoinevastel/fpscanner" target="_blank" rel="noreferrer noopener">fpscanner</a> fingerprinting library, a proof-of-work puzzle in a worker, and a per-epoch signing module.</li>



<li>The browser POSTs the result to <code>/crowdsec-internal/challenge/submit</code>. The engine validates the crypto and the PoW, then scoring rules look at the fingerprint and produce a verdict.</li>



<li>A client that passes gets a sealed <code>__crowdsec_challenge</code> cookie and is waved through until it expires (12 hours by default). A client that fails never reaches your origin.</li>
</ol>



<p class="wp-block-paragraph">A clarification, because the vocabulary here gets abused: This is not a captcha, nothing for a human to solve; the page solves itself.</p>



<h3 class="wp-block-heading">Three kinds of clients, and what happens to each</h3>



<p class="wp-block-paragraph">Bots that do not run JavaScript are the easy tier. <code>curl, wget</code>, most scraping frameworks and every &#8220;I&#8217;ll just parse the HTML&#8221; script get the challenge page and stop there.</p>



<p class="wp-block-paragraph">Naive automation is the second tier. Selenium, Playwright, Puppeteer, and anything driven over the Chrome DevTools Protocol will happily solve the proof of work (the PoW is only the ticket to the fingerprint check, which is where the filtering happens). They get caught on a declared-automation signal instead. All the fingerprints that are collected in the browser award scores, and if your score gets too high, you’re rejected. As an example, <code>cdp, webdriver, selenium, playwright</code>, and <code>bot_user_agent</code> are each worth 100 points on their own, which clears every shipped threshold.</p>



<p class="wp-block-paragraph">The third tier is automation that knows all this and tries to hide those flags. This is the tier people write off, and the default config still catches a fair share of it. Hiding obvious flags gets you past two signals out of about two dozen, and the environment still has to hold up.</p>



<p class="wp-block-paragraph">Does this make the barrier disappear? No. Nothing does. A patient adversary running a real browser on real hardware at a human pace is the case nobody solves, at any price, from any vendor. What you get is cost: their per-request budget goes up, and the cheap end of the scraping market (which is most of it) stops being profitable against you.</p>



<h2 class="wp-block-heading">Setting it up on Nginx</h2>



<p class="wp-block-paragraph">Let&#8217;s start from an Ubuntu box that already serves something with nginx, and nothing else.</p>



<p class="wp-block-paragraph"><strong>1. Install the engine.</strong></p>



<pre><code class="language-bash">curl -s https://install.crowdsec.net | sudo sh
sudo apt-get install -y crowdsec</code></pre>



<p class="wp-block-paragraph">The installer runs <code>cscli setup</code> for you, which detects your running services and wires the log acquisition on its own. On this box, it found nginx, sshd, postfix, and haproxy and installed the matching collections. That side of things is a happy accident here: we came for the bot challenge and got the log-based detection thrown in.</p>



<p class="wp-block-paragraph"><strong>2. Install the bot challenge collection.</strong></p>



<pre><code class="language-bash">sudo cscli collections install crowdsecurity/appsec-bot-challenge</code></pre>



<p class="wp-block-paragraph">Three bundles ship, and they differ only in where they draw the line:</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><strong>Collection</strong></th><th><strong>Rejects at score</strong></th></tr></thead><tbody><tr><td><code>crowdsecurity/appsec-bot-challenge</code></td><td>≥ 75 (balanced)</td></tr><tr><td><code>crowdsecurity/appsec-bot-challenge-strict</code></td><td>≥ 45</td></tr><tr><td><code>crowdsecurity/appsec-bot-challenge-permissive</code></td><td>≥ 100</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Install only one of them: if you install two, the strictest one silently wins.</p>



<p class="wp-block-paragraph"><strong>3. Point a WAF listener at it.</strong> Create <code>/etc/crowdsec/acquis.d/appsec.yaml</code>:</p>



<pre><code class="language-yaml">listen_addr: 127.0.0.1:7422
appsec_configs:
  - crowdsecurity/appsec-bot-*
labels:
  type: appsec
source: appsec</code></pre>



<p class="wp-block-paragraph">Such a config carries only the bot-detection, which is what the second line below is telling you:</p>



<pre><code class="language-bash">level=info msg="Loaded 0 inband rules" component=appsec_config
level=info msg="WAF challenge runtime initialized" cookie_ttl=12h0m0s max_cookie_len=4096 module=challenge pow_difficulty=20 rotation_interval=5m0s</code></pre>



<p class="wp-block-paragraph"><code>WAF challenge runtime initialized</code> is the line you want.</p>



<p class="wp-block-paragraph"><strong>4. Install the bouncer.</strong></p>



<pre><code class="language-bash">sudo apt-get install -y crowdsec-nginx-bouncer</code></pre>



<p class="wp-block-paragraph">The package registers its own bouncer and key, drops the lua snippet into <code>/etc/nginx/conf.d/</code>, and reloads nginx. Then point it at the WAF:</p>



<pre><code class="language-bash">sudo sed -i 's|^APPSEC_URL=.*|APPSEC_URL=http://127.0.0.1:7422|' \
&nbsp;&nbsp;&nbsp;&nbsp;/etc/crowdsec/bouncers/crowdsec-nginx-bouncer.conf
sudo nginx -t &amp;&amp; sudo systemctl restart nginx</code></pre>



<p class="wp-block-paragraph">That is the whole wiring.</p>



<h3 class="wp-block-heading">Does it work?</h3>



<p class="wp-block-paragraph">Ask <code>curl</code>, the most honest bot you have on hand:</p>



<pre><code class="language-bash">/&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 200&nbsp; 293950 bytes &nbsp; -&gt; challenge page
/style.css &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 404&nbsp; 162 bytes&nbsp; &nbsp; &nbsp; -&gt; origin page
/robots.txt&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 404&nbsp; 162 bytes&nbsp; &nbsp; &nbsp; -&gt; origin page</code></pre>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li><code>200</code> with a 290 KB body means you are looking at the challenge page. The engine answers the bouncer with an envelope, and the bouncer renders the page inside it with the status the envelope asks for.</li>



<li><code>/style.css</code> and <code>/robots.txt</code> skipped the challenge entirely. The collection ships path exclusions for static assets, crawler files, feeds, API paths, and webhooks.</li>
</ul>



<p class="wp-block-paragraph">Now point an automated browser at it. Anything CDP-driven does the job: Puppeteer, Playwright, or a Chrome started with <code>--remote-debugging-port</code>. It solves the puzzle, submits, and:</p>



<pre><code class="language-bash">level=info msg="on_challenge_submit rejected" automation=true is_bot=true \
&nbsp;&nbsp;reason="request score 100" signals="[cdp]" source=203.0.113.7 \
&nbsp;&nbsp;timezone=Europe/Paris url="http://198.51.100.10/no-bots-please/"
level=info msg="WAF bot-detection: 203.0.113.7 rejected by crowdsecurity/rejected-browser-submission (request score 100)"</code></pre>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li><code>signals="[cdp]"</code> is the whole story: the Chrome DevTools Protocol was live in that browser, which is worth 100 points on its own.</li>



<li><code>reason="request score 100"</code> against a balanced threshold of 75, so the submission is refused and no cookie is issued.</li>



<li><code>automation=true</code> and <code>is_bot=true</code> are the engine&#8217;s summary flags.</li>
</ul>



<p class="wp-block-paragraph">The nginx access log shows the same thing from the other side, and it is the clearest picture of the protocol:</p>



<pre><code class="language-bash">"GET /no-bots-please/ HTTP/1.1" 200 139306
"GET /crowdsec-internal/challenge/fpscanner.js HTTP/1.1" 200 36328
"GET /crowdsec-internal/challenge/pow-worker.js HTTP/1.1" 200 7544
"POST /crowdsec-internal/challenge/submit HTTP/1.1" 200 32</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">In layman&#8217;s terms: the bot got the challenge page, downloaded the fingerprinting library and the PoW worker, did the work, submitted, and got 32 bytes of rejection JSON back. There is no fifth line. It never reached the page.</p>



<p class="wp-block-paragraph">The alert lands with <code>kind</code> set to <code>bot-detection</code>:</p>



<pre><code class="language-bash">| ID | value &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | reason&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | decisions | kind&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; |
| 2&nbsp; | Ip:203.0.113.7&nbsp; | crowdsecurity/rejected-browser-submission | &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | bot-detection |</code></pre>



<p class="wp-block-paragraph">The <code>decisions</code> column is empty on purpose. One rejection records the catch without banning anyone, which is the right default: a single odd fingerprint is weak evidence. Keep the bot running, and <code>crowdsecurity/appsec-bot-challenge-too-many-submissions</code> fills up (5 submissions in 20 seconds) and turns it into a real decision. A human browser solves once, takes its cookie, and stops submitting, so it never goes near that bucket.</p>



<pre><code class="language-bash">sudo cscli metrics show bot-detection</code></pre>



<pre><code class="language-bash">| Bot Detection &nbsp; | Requested | Submitted | Solved | Granted | Exempt | Submissions Rejected |
| 127.0.0.1:7422/ | 3 &nbsp; &nbsp; &nbsp; &nbsp; | - &nbsp; &nbsp; &nbsp; &nbsp; | -&nbsp; &nbsp; &nbsp; | - &nbsp; &nbsp; &nbsp; | 2&nbsp; &nbsp; &nbsp; | -&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; |</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1545" height="222" src="https://cms.crowdsec.net/wp-content/uploads/2026/09/single-bot-detection-alert.png" alt="" class="wp-image-6844" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/09/single-bot-detection-alert.png 1545w, https://cms.crowdsec.net/wp-content/uploads/2026/09/single-bot-detection-alert-300x43.png 300w, https://cms.crowdsec.net/wp-content/uploads/2026/09/single-bot-detection-alert-766x110.png 766w, https://cms.crowdsec.net/wp-content/uploads/2026/09/single-bot-detection-alert-1536x221.png 1536w" sizes="auto, (max-width: 1545px) 100vw, 1545px" /></figure>



<h2 class="wp-block-heading">How CrowdSec bot detection compares to the commercial anti-bot vendors</h2>



<p class="wp-block-paragraph">Let&#8217;s be fair about the starting position: Cloudflare, Akamai, DataDome, and the rest have been doing anti-bot for years, and we are the newcomers in this particular field.</p>



<p class="wp-block-paragraph">However, we have a couple of hundred thousand CrowdSec installations sitting on a couple of hundred thousand different networks, each an independent vantage point, each reporting what actually hit it into a pool everyone pulls from. When a scraper works its way down a list of targets, the ones it reaches later already know about it.</p>



<p class="wp-block-paragraph">Tenure is worth less in 2026 than it used to be, though. Detection and evasion are being rewritten on a much shorter cycle now, with the same tooling on both sides, so a long head start matters less than how fast you can change your own rules. Which brings us to the part that separates these products.</p>



<p class="wp-block-paragraph"><strong>With a managed vendor, the thing deciding whether your visitor is a human is a black box you rent.</strong> You get a dashboard, a sensitivity slider, and a score you cannot audit. When it is right, fine. When it is wrong, you file a ticket.</p>



<p class="wp-block-paragraph">With CrowdSec, the scoring is a file you can <code>cat</code>:</p>



<pre><code class="language-yaml"># /etc/crowdsec/appsec-configs/appsec-bot-challenge-scoring.yaml
- filter: EvaluateMismatches().Has("cdp")
&nbsp;&nbsp;apply:
&nbsp;&nbsp;&nbsp;&nbsp;- AddRequestScore(100, "cdp")</code></pre>



<p class="wp-block-paragraph">And the threshold that acts on it is four lines:</p>



<pre><code class="language-yaml">name: crowdsecurity/appsec-bot-challenge-scoring-balanced
inband:
&nbsp;&nbsp;on_challenge_submit:
&nbsp;&nbsp;&nbsp;&nbsp;- filter: RequestScore() &gt;= 75
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;apply:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- 'RejectSubmission("request score " + string(RequestScore()), "verbose")'</code></pre>



<p class="wp-block-paragraph">That is the entire decision. Read it, diff it, change it. I would rather argue with a YAML file than with a support queue.</p>



<p class="wp-block-paragraph">A few concrete things follow from that:</p>



<ul class="wp-block-list">
<li><strong>Your users&#8217; traffic stays on your infrastructure.</strong> A cloud anti-bot service is a reverse proxy: it terminates TLS, so every request body, cookie, and credential your users send transits a third party in the clear on their side. CrowdSec computes the fingerprint in the browser and scores it in the engine on your own box, and nothing about your visitors leaves your infrastructure for that decision to be made.</li>



<li><strong>False positives are debuggable.</strong> Block a legitimate visitor, and score_reasons on the alert tells you which signals fired and what each was worth. You re-weight the offending one yourself in about four lines, rather than reporting it upstream and waiting.</li>



<li><strong>Cost does not scale with the attack.</strong> Per-request pricing has an awkward property: your popularity with the wrong people increases your bill. Metering differs by vendor and by plan, so the arithmetic is yours to do, and the shape holds either way: your bill tracks their volume. Self-hosted costs you some CPU on a box you already pay for.</li>



<li><strong>Leaving is an</strong> <strong><code>apt purge</code>.</strong> DNS-layer onboarding is great until the day you want to move, at which point your exit is a DNS migration. Here, it is uninstalling a package.</li>
</ul>



<p class="wp-block-paragraph">However, self-hosting means you own the tuning. If your threat model is a 2 Tbps DDoS, buy the CDN. If it is scrapers, credential stuffing, scalpers, and AI crawlers eating your content, you can do this yourself, on your own terms, for free.</p>



<h2 class="wp-block-heading">Going further</h2>



<p class="wp-block-paragraph">Out of the box, every request gets challenged, which might not be what you want.</p>



<p class="wp-block-paragraph">Nearly all of it is one custom appsec-config. Write yours under <code>/etc/crowdsec/appsec-configs/</code>, and list them <strong>by name</strong> in your acquisition: the <code>crowdsecurity/appsec-bot-</code>* glob does not match your namespace.</p>



<h3 class="wp-block-heading">Challenge only the part of the site that matters</h3>



<p class="wp-block-paragraph">Invert the problem and exempt everything except the funnel you care about.</p>



<pre><code class="language-yaml">name: mycorp/appsec-bot-challenge-path-scope
inband:
&nbsp;&nbsp;pre_eval:
&nbsp;&nbsp;&nbsp;&nbsp;- filter: &gt;-
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;!(req.URL.Path startsWith "/no-bots-please/")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;apply:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- ExemptFromChallenge("outside-protected-path")</code></pre>



<p class="wp-block-paragraph">Swap the path for whatever actually matters on your site.</p>



<p class="wp-block-paragraph">Per host works the same way with <code>req.Host</code>, though proxies sometimes forward foobar.com:443 rather than the bare host, so prefer <code>startsWith</code> or <code>endsWith ".foobar.com"</code>.</p>



<p class="wp-block-paragraph">Check your work with the exempt counters, which break down by reason:</p>



<pre><code class="language-bash">| Bot Detection — Exempted &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; |
+-----------------+------------------------+-------+
| Appsec Engine &nbsp; | Reason &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | Count |
+-----------------+------------------------+-------+
| 127.0.0.1:7422/ | googlebot&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | 1 &nbsp; &nbsp; |
| 127.0.0.1:7422/ | outside-protected-path | 24&nbsp; &nbsp; |
| 127.0.0.1:7422/ | static &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | 1 &nbsp; &nbsp; |
+-----------------+------------------------+-------+
| Total &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; |&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; | 26&nbsp; &nbsp; |</code></pre>



<p class="wp-block-paragraph">Twenty-four requests skipped the challenge for sitting outside the protected path; one was a verified crawler, and one a static asset.</p>



<h3 class="wp-block-heading">Let your own bots through</h3>



<p class="wp-block-paragraph">Your uptime monitor, your internal crawler, your partner&#8217;s integration. <code>MatchKnownBot</code> checks a JSON definition file, and its rule is deliberately strict: <em>(user-agent matches AND at least one path matches) AND (exact IP OR CIDR range OR forward-confirmed reverse DNS)</em>.</p>



<pre><code class="language-json">{"name":"mybot","user_agent":"mycorp-monitor","paths":["^/health(/|$)"],"ranges":["10.42.0.0/16"],"ips":["192.0.2.77"]}</code></pre>



<pre><code class="language-yaml">name: mycorp/appsec-bot-challenge-known-bot
inband:
&nbsp;&nbsp;pre_eval:
&nbsp;&nbsp;&nbsp;&nbsp;- filter: MatchKnownBot(req.RemoteAddr, req.UserAgent(), req.URL.Path, "legit_bots/mybot.json")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;apply:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- ExemptFromChallenge("mybot")
data:
&nbsp;&nbsp;- source_url: https://example.com/mybot.json
&nbsp;&nbsp;&nbsp;&nbsp;dest_file: legit_bots/mybot.json
&nbsp;&nbsp;&nbsp;&nbsp;type: bots</code></pre>



<p class="wp-block-paragraph">The <code>data</code>: block is not optional even when you place the file yourself: only declared files get registered in the datafile registry. A definition carrying only a user-agent, with no IP, range, or rDNS, is rejected outright at load. A user-agent proves nothing at all.</p>



<p class="wp-block-paragraph">This is how the shipped good-bot exclusions work too. The googlebot definition carries no IP list at all:</p>



<pre><code class="language-json">{"name":"googlebot","user_agent":"googlebot","rdns":["(^|\\.)googlebot\\.com$"]}</code></pre>



<p class="wp-block-paragraph">Everything rests on forward-confirmed reverse DNS: resolve the IP to a name, resolve that name back, check they agree. A scraper setting <code>User-Agent: Googlebot/2.1</code> from a random VPS gets challenged like everyone else. Anchor your own rDNS patterns the same way, or <code>evilgooglebot.com</code> will match.</p>



<h3 class="wp-block-heading">Waive the challenge for a specific user</h3>



<p class="wp-block-paragraph">For an authenticated session or an internal probe, <code>GrantChallengeCookie</code> hands out the cookie directly rather than exempting a single request:</p>



<pre><code class="language-yaml">name: mycorp/appsec-bot-challenge-internal-probe
inband:
&nbsp;&nbsp;pre_eval:
&nbsp;&nbsp;&nbsp;&nbsp;- filter: req.Header.Get("X-Internal-Probe") == "s3cr3t" &amp;&amp; req.RemoteAddr startsWith "10."
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;apply:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- GrantChallengeCookie("internal-probe", "24h")</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">The client gets a <code>307</code> back to where it was going, carrying the cookie. Note that a shared-secret header on its own is a bypass token from the moment it leaks, so this is a purely technical example.</p>



<h3 class="wp-block-heading">Re-weight the scoring for your own traffic</h3>



<p class="wp-block-paragraph">Every signal&#8217;s value is a line in a YAML file, so tune it for the audience you actually have:</p>



<pre><code class="language-yaml">name: mycorp/appsec-bot-challenge-reweight
inband:
&nbsp;&nbsp;on_challenge_submit:
&nbsp;&nbsp;&nbsp;&nbsp;# A UTC-only audience is unusual for us, so treat it as stronger evidence.
&nbsp;&nbsp;&nbsp;&nbsp;- filter: EvaluateMismatches().Has("utc_timezone")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;apply:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- AddRequestScore(45, "utc_timezone_mycorp")
&nbsp;&nbsp;&nbsp;&nbsp;# We are mobile-first, so a mobile UA mismatch is expected noise.
&nbsp;&nbsp;&nbsp;&nbsp;- filter: EvaluateMismatches().Has("ua_mobile")
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;apply:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- AddRequestScore(-15, "ua_mobile_expected")</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Scores can go negative, and you can add evidence the fingerprint cannot see, such as a checkout hit with no <code>Referer</code>.</p>



<p class="wp-block-paragraph"><strong>Load order is not cosmetic here.</strong> <code>RejectSubmission()</code> is terminal: it halts every remaining <code>on_challenge_submit</code> rule. So your re-weighting has to load <em>after</em> the scoring engine and <em>before</em> the threshold config, which means listing them explicitly instead of leaning on one glob:</p>



<pre><code class="language-yaml">listen_addr: 127.0.0.1:7422
appsec_configs:
&nbsp;&nbsp;- crowdsecurity/appsec-bot-challenge-exclude-*
&nbsp;&nbsp;- mycorp/appsec-bot-challenge-path-scope
&nbsp;&nbsp;- mycorp/appsec-bot-challenge-known-bot
&nbsp;&nbsp;- mycorp/appsec-bot-challenge-internal-probe
&nbsp;&nbsp;- crowdsecurity/appsec-bot-challenge-scoring
&nbsp;&nbsp;- mycorp/appsec-bot-challenge-reweight
&nbsp;&nbsp;- crowdsecurity/appsec-bot-challenge-scoring-balanced
labels:
&nbsp;&nbsp;type: appsec
source: appsec</code></pre>



<p class="wp-block-paragraph">The quickest way to convince yourself is to break it on purpose. Drop a temporary rule subtracting 60 points from <code>cdp</code>, then send the same CDP browser that scored 100 and got rejected earlier. <code>100 - 60 = 40</code>, comfortably under the balanced threshold of 75:</p>



<pre><code class="language-bash">"GET /no-bots-please/ HTTP/1.1" 200 140417 &nbsp; &nbsp; ← challenge page
"POST /crowdsec-internal/challenge/submit HTTP/1.1" 200 25 &nbsp; &nbsp; ← accepted
"GET /no-bots-please/ HTTP/1.1" 200 231&nbsp; &nbsp; &nbsp; &nbsp; ← the real page</code></pre>



<pre><code class="language-bash">| Bot Detection &nbsp; | Requested | Submitted | Solved | Granted | Exempt |
| 127.0.0.1:7422/ | 4 &nbsp; &nbsp; &nbsp; &nbsp; | 1 &nbsp; &nbsp; &nbsp; &nbsp; | 1&nbsp; &nbsp; &nbsp; | 1 &nbsp; &nbsp; &nbsp; | 3&nbsp; &nbsp; &nbsp; |</code></pre>



<p class="wp-block-paragraph">Same browser, same box, one config file moved into one position, opposite verdict. <code>Solved: 1</code>, cookie issued, origin page delivered in 231 bytes.</p>



<h2 class="wp-block-heading">Where this is at</h2>



<p class="wp-block-paragraph">Bot detection is marked <strong>alpha</strong> upstream; however, it works, it is in 1.8.0, and we run it.</p>



<p class="wp-block-paragraph">The scoring rules are hub content, so they are only as good as what the community feeds back into them. Found a signal that misfires on your traffic, or a bot family that walks straight through? That is a useful bug report: open an issue on <a href="https://github.com/crowdsecurity/hub" target="_blank" rel="noreferrer noopener">the Hub</a>, or hop onto our <a href="https://discord.gg/crowdsec" target="_blank" rel="noreferrer noopener">Discord</a> and tell us what you are seeing. We would rather fix a signal than ship a slider. 🙂</p>
]]></content:encoded>
            <category>AI</category>
            <category>CrowdSec</category>
            <category>Proactive Cybersecurity</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/09/sin-logo-blog.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[What’s New in CrowdSec 1.8: WAF Bot Detection, Kubernetes Datasource, and Performance Improvements]]></title>
            <link>https://crowdsec.net/blog/crowdsec-1-8-waf-bot-detection-kubernetes</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/crowdsec-1-8-waf-bot-detection-kubernetes</guid>
            <pubDate>Tue, 01 Sep 2026 09:49:27 GMT</pubDate>
            <description><![CDATA[<p>Discover what’s new in CrowdSec 1.8, including WAF bot detection, a dedicated Kubernetes datasource, faster LAPI synchronization, and improved Console alerts.</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph">I wrote this for humans; I’m aware it’s becoming a niche audience, so Hi anyway 🙂</p>



<p class="wp-block-paragraph">As the holidays are ending for most of us, it’s time for us to showcase what we’ve been cooking: CrowdSec 1.8 and more!</p>



<p class="wp-block-paragraph"></p>



<h3 class="wp-block-heading">WAF: Bot Detection&nbsp;</h3>



<p class="wp-block-paragraph">Your average bot in 2026 has a real Chrome with a consistent TLS fingerprint, a residential IP, and a lot more patience than your on-call engineer: According to most metrics, it’s a more exemplary user than your actual users. That’s why it’s tricky: blocking bots is easy, but not your users.</p>



<p class="wp-block-paragraph">That’s why we’re bringing bot detection to CrowdSec’s WAF! This is something that we’ve been contemplating for a while, have worked on for the past 6 months, and can finally release.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="800" height="398" src="https://cms.crowdsec.net/wp-content/uploads/2026/09/bot-protection.gif" alt="" class="wp-image-6818"/></figure>



<p class="wp-block-paragraph">What it actually means is that the WAF can now not only react to “what the user does” but also to “what the user is”: a human and a browser, or a bot pretending to be one. For a legitimate user, this is solved transparently, while an automated client cannot cheaply fake it.</p>



<p class="wp-block-paragraph">Our approach combines several techniques, most notably fingerprinting and proof of work: it forces the attacker to play on the anti-detection side, but also stack compute cost on top of it, raising the economics of botting.</p>



<p class="wp-block-paragraph">From a use-case perspective, it’s an efficient way to deal with distributed attacks, scraping, scalping, and generally any kind of aggressive automation.</p>



<p class="wp-block-paragraph">To ensure a production-ready approach, it comes with sets of allowlists and exclusions to avoid blocking legitimate bots that cannot solve such challenges, while also providing the usual fine-grained configuration to solve more edge cases that one might encounter &#8211; support for Web Bot Authentication is also planned. While provided baselines should deal with most common bots, the operator can easily implement its own detection patterns, relying on weak signals and various anomalies to deter specific bots targeting you.</p>



<p class="wp-block-paragraph">It’s already compatible with the <a href="https://doc.crowdsec.net/u/bouncers/nginx" target="_blank" rel="noreferrer noopener">latest nginx/openresty release</a>; haproxy is imminent, and we’re waiting on traefik and envoy releases. The goal is to have it supported by all major web bouncers; it’s just a small step for those already supporting the WAF feature. In terms of deployments, we tried our best to make it industrialization-friendly, with multi-tenancy support and a good amount of observability.</p>



<p class="wp-block-paragraph"><a href="https://doc.crowdsec.net/docs/next/appsec/bot_detection/intro">You can find everything here.</a></p>



<p class="wp-block-paragraph"></p>



<h3 class="wp-block-heading">Dedicated Kubernetes datasource</h3>



<p class="wp-block-paragraph">Previously, our Kubernetes users had to deploy multiple CrowdSec instances as a daemonset to monitor their infrastructure. This led to various undesired side-effects, mostly as a result of the induced complexity. The new datasource talks directly to the Kubernetes API, following <code>pods/logs</code> for pods matching the given selector, allowing users to deploy a single CrowdSec instance to monitor multiple pods.</p>



<p class="wp-block-paragraph">This will hopefully make both the build, the run, and the architecture of CrowdSec’s Kubernetes deployments a lot easier and cheaper.</p>



<p class="wp-block-paragraph"><a href="https://doc.crowdsec.net/docs/next/log_processor/data_sources/kubernetes" target="_blank" rel="noreferrer noopener">You can find more here.</a></p>



<p class="wp-block-paragraph"></p>



<h3 class="wp-block-heading">Bouncer to LAPI optimisation</h3>



<p class="wp-block-paragraph">We made significant optimisations to the interaction between the Local API and the bouncer to improve performance of decision synchronization.&nbsp;</p>



<p class="wp-block-paragraph">For example, on a setup with 1M alerts and 10 bouncers, time spent reduction ranges from x600 (MySQL) to x13 (PgSQL): This aims at better supporting the larger setups that we are seeing more and more often, while also improving the performance for the rest of us.</p>



<p class="wp-block-paragraph"></p>



<h3 class="wp-block-heading">Console: new alerts page</h3>



<p class="wp-block-paragraph">The console’s alert page is one of the most used features of the console &#8211; as it’s available to all tiers of users. We’re bringing a very significant revamp of this page to help our users make the most out of it:</p>



<ul class="wp-block-list">
<li><strong>Leveraging IP reputation</strong> in signals allows you to easily cut through the noise and isolate outliers.</li>



<li><strong>IP grouping and merging</strong> allow you to get a better overview of activity without having to look at detailed alerts.</li>



<li><strong>Detailed breakdown</strong> to explore data via any angle of the alert, from scenario to targeted asset to IP reputation.</li>



<li><strong>Save views</strong> to create shortcuts to your relevant alerts to streamline recurrent workflows.</li>
</ul>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1569" height="791" src="https://cms.crowdsec.net/wp-content/uploads/2026/09/console-new-alerts-pages-1.png" alt="" class="wp-image-6817" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/09/console-new-alerts-pages-1.png 1569w, https://cms.crowdsec.net/wp-content/uploads/2026/09/console-new-alerts-pages-1-300x151.png 300w, https://cms.crowdsec.net/wp-content/uploads/2026/09/console-new-alerts-pages-1-768x387.png 768w, https://cms.crowdsec.net/wp-content/uploads/2026/09/console-new-alerts-pages-1-1536x774.png 1536w" sizes="auto, (max-width: 1569px) 100vw, 1569px" /></figure>



<p class="wp-block-paragraph"></p>



<h3 class="wp-block-heading">CrowdSec skill</h3>



<p class="wp-block-paragraph">Although technically not tied to this CrowdSec release, we continue to expand <a href="https://www.skills.sh/crowdsecurity/crowdsec-skill" target="_blank" rel="noreferrer noopener">our crowdsec skill</a> : all of CrowdSec’s knowledge at your LLM’s fingertips!</p>



<p class="wp-block-paragraph"></p>



<h3 class="wp-block-heading">Closing word</h3>



<p class="wp-block-paragraph">By the time you’re reading this blog post, *nix packages, Docker images, and Helm charts should already be available; Windows packages, {opn,pf}sense are on their way (keep in mind some of those require 3rd-party validation).</p>



<p class="wp-block-paragraph">Don’t be a stranger; drop on <a href="https://discord.gg/crowdsec" target="_blank" rel="noreferrer noopener">Discord</a> to ask for help or report bugs,</p>



<p class="wp-block-paragraph">Happy hacking 🦙</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
            <category>AI</category>
            <category>CrowdSec</category>
            <category>Proactive Cybersecurity</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/09/Blog-images-1575-%C3%97-871px-30.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Why You Should Write a Skill for Your Software]]></title>
            <link>https://crowdsec.net/blog/ai-agent-skill-for-your-software</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/ai-agent-skill-for-your-software</guid>
            <pubDate>Tue, 11 Aug 2026 12:23:02 GMT</pubDate>
            <description><![CDATA[<p>Learn why AI agent skills make software setup more reliable, reduce LLM guesswork, improve troubleshooting, and can even expose gaps in your documentation.</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph">&nbsp;Imagine a very hypothetical situation, definitely not inspired by hundreds of Discord threads showing this exact pattern:</p>



<p class="wp-block-paragraph">Sometimes, a setup can go wrong. A bouncer that installs cleanly but never blocks a thing, or a parser that reads zero lines, and you just want the shortest path out. So you do what everyone actually does in 2026: you don&#8217;t open the docs, you alt-tab to an LLM and paste the question. Answer in seconds.</p>



<p class="wp-block-paragraph">And it sounds right. It hands you a packagecloud URL, a cscli command or two, a config path. You copy-paste. Sometimes it works. Sometimes the URL is two years stale, one of those confident flags doesn&#8217;t exist, and the config path moved three releases ago. Now you&#8217;re debugging the helper on top of the thing you started with.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="547" height="723" src="https://cms.crowdsec.net/wp-content/uploads/2026/08/Captura-de-pantalla-2026-08-11-a-las-13.41.27.png" alt="" class="wp-image-6774" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/08/Captura-de-pantalla-2026-08-11-a-las-13.41.27.png 547w, https://cms.crowdsec.net/wp-content/uploads/2026/08/Captura-de-pantalla-2026-08-11-a-las-13.41.27-227x300.png 227w" sizes="auto, (max-width: 547px) 100vw, 547px" /></figure>



<p class="wp-block-paragraph">To be fair to the model: You handed a fast improviser a question with no map, so it did the thing that setup rewards: it improvised. That&#8217;s just what you get when you ask someone to recite a manual they were only ever allowed to skim.</p>



<p class="wp-block-paragraph">So the fix is simple: give it a better map with the <strong><a href="https://www.skills.sh/crowdsecurity/crowdsec-skill/crowdsec" target="_blank" rel="noreferrer noopener">CrowdSec Skill</a></strong>.</p>



<p class="wp-block-paragraph">That&#8217;s what a skill is: our own CrowdSec docs, restructured into rails the agent can&#8217;t wander off. <a href="https://context7.com/" target="_blank" rel="noreferrer noopener">Context7</a> and friends already pipe fresh reference into your agent&#8217;s context, and that helps, but reference is still something the model has to read and choose to act on. A skill goes further and turns those docs into a procedure: the exact install command, a heads-up on the cscli flags that don&#8217;t exist, and a hard rule that it won&#8217;t touch your firewall without asking. Auto-generated docs make the model better informed, while skill makes it better behaved.</p>



<p class="wp-block-paragraph">Last year we shipped an MCP to author and test WAF rules for CrowdSec&nbsp; (<a href="https://github.com/crowdsecurity/crowdsec-local-mcp" target="_blank" rel="noreferrer noopener">crowdsec-local-mcp</a>), but it seems most people just want the boring part to work: install it, wire a bouncer, stop the probes. MCPs were so 2025, so this time we tried something simpler: a skill.</p>



<p class="wp-block-paragraph">And building it did something I didn&#8217;t plan for: it made our own docs better and improved the release testing process. I&#8217;ll come back to that.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">What a skill adds to an already powerful model</h2>



<p class="wp-block-paragraph">Models can <a href="https://openai.com/index/hugging-face-model-evaluation-security-incident/" target="_blank" rel="noreferrer noopener">now hack the planet over a weekend unnoticed</a> and <a href="https://x.com/__alpoge__/status/2079028340955197566" target="_blank" rel="noreferrer noopener">solve 87-year-old mathematical problems while you watch a football match</a> &#8211; does it struggle to set up a simple piece of software as CrowdSec? (Spoiler: yes, it did).</p>



<p class="wp-block-paragraph">I ran the same experiment with a “vanilla” model (Opus 4.8 High) and then with the CrowdSec skill enabled, and here is what changed:</p>



<ul class="wp-block-list">
<li>With the skill, it looked before it leapt. After the initial install, it inspected what service was detected&nbsp; (nginx, sshd, postfix) and if the logs were correctly ingested and parsed.</li>



<li>Vanilla model did some very reasonable guessing that ended up being wrong: It reached for <code>/etc/crowdsec/acquis.yaml</code> to add nginx log files monitoring. But the initial setup already detected and monitored those log files, so now they&#8217;re read twice, making every scenario twice as sensitive as it should be.</li>



<li>With the skill, it checked the outcome of its action before declaring victory, because the skill taught him to do so: inspect <code>cscli metrics</code>, run diagnostics, perform proper testing instead of &#8220;assuming&#8221;.</li>



<li>Vanilla model failed to properly test if the WAF was correctly configured and curled the AppSec port without the API-key header, declaring it a success prematurely.</li>



<li>Last but not least, the Vanilla model tried to whitelist itself. It ran <code>cscli decisions add --ip &lt;ip&gt; --type whitelist</code>. The command itself is syntaxically correct and even prints a <em>&#8220;Decision successfully added&#8221;</em>, but it does nothing: <code>whitelist</code> isn&#8217;t a decision type that has any meaning in a default setup.</li>
</ul>



<p class="wp-block-paragraph">Long story short: one setup was correct and relied on an actual check to ensure correctness; the other one made reasonable guesses that unfortunately ended up being just wrong enough. You can consider the skill as a loose form of harness &#8211; thanks to the feedback loop that cscli verification provides &#8211; and domain-specific knowledge in addition to an already powerful model.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">The fine line between &#8220;reasonable guess&#8221; and &#8220;improvisation&#8221;</h2>



<p class="wp-block-paragraph">As LLMs will never say no, the skill is also a good way to draw limits on what we know the model would get confidently wrong if we didn&#8217;t stop it. For example, the skill explicitly states that it doesn&#8217;t know how to write WAF rules or scenarios without a significant error margin.</p>



<p class="wp-block-paragraph">The skill is also a useful place to flag destructive and dangerous actions it shouldn&#8217;t attempt. I guess the internet is filled with enough &#8220;omg claude deleted database !!11&#8221; horror stories that I don&#8217;t need to expand on this.</p>



<p class="wp-block-paragraph">And the best part of it is that a skill is mostly just a bunch of markdown files that dictate how it should behave in a very plain, human language.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">The part that helped us, too</h2>



<p class="wp-block-paragraph">Here is the thing we did not expect when we started.</p>



<p class="wp-block-paragraph">To build this skill, we let Claude run unchained on dedicated VMs, with the actual docs as a bible, and told it to actually do the work: bare-metal, Docker, Kubernetes, OPNsense, with the nginx, haproxy, traefik, and firewall bouncers. Install, wire, break, verify; on real boxes it was free to trash.</p>



<p class="wp-block-paragraph">We also fed him a thousand or so of support threads from our Discord server as exercises to test its effectiveness.</p>



<p class="wp-block-paragraph">And it kept tripping over our own documentation.</p>



<p class="wp-block-paragraph">A command that had quietly changed. A config path that moved two releases ago and got left behind in the docs (that acquis.yaml the plain model reached for earlier? we still had stale references to it too). An approximation that was close enough to read past, but wrong enough to break a copy-paste. A flag that no longer existed. None of these were dramatic. All of them were the small lies that turn a ten-minute setup into a lost afternoon.</p>



<p class="wp-block-paragraph">Every time the agent hit one, it was a bug report we could not argue with, because there was a broken box sitting right there proving it. So we fixed the docs. Then re-ran. Then fixed the next one.</p>



<p class="wp-block-paragraph">That is the loop:</p>



<pre><code class="language-bash">   docs  ──►  run live on real VMs  ──►  build the skill
    ▲                                          │
    └──────────  fix the docs  ◄───────────────┘</code></pre>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="715" height="677" src="https://cms.crowdsec.net/wp-content/uploads/2026/08/Captura-de-pantalla-2026-08-11-a-las-14.00.28.png" alt="" class="wp-image-6775" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/08/Captura-de-pantalla-2026-08-11-a-las-14.00.28.png 715w, https://cms.crowdsec.net/wp-content/uploads/2026/08/Captura-de-pantalla-2026-08-11-a-las-14.00.28-300x284.png 300w" sizes="auto, (max-width: 715px) 100vw, 715px" /></figure>



<p class="wp-block-paragraph">Our docs were not &#8211; are not and never will be perfect. They had the same stale corners and confident approximations that tripped the plain model up in the comparison above. The difference is that we went looking, with a tool relentless enough to find them, and now there are fewer. The skill made CrowdSec easier to install.</p>



<h2 class="wp-block-heading">Beyond first-time setup</h2>



<p class="wp-block-paragraph">Setup is where I started because it is the wall most people hit first. The same conversation handles the two that come next.</p>



<p class="wp-block-paragraph">Debug. When things go the wrong way (logs parsed but no alerts, a decision that exists but does not block), you point the agent at it and let it perform exhaustive diagnostics by relying on metrics, <code>cscli support dump</code> and alike.</p>



<p class="wp-block-paragraph">Tune. The first false positive is a rite of passage, and the tempting fix, disabling a whole collection, trades one problem for a worse one. The skill walks you to the right suppression layer instead (allowlist, whitelist parser, or postoverflow).</p>



<p class="wp-block-paragraph">Setup is just the starting point. The same principle carries through debugging and tuning: give the model the right procedure, and make it verify its work.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
            <category>AI</category>
            <category>CrowdSec</category>
            <category>Proactive Cybersecurity</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/08/Blog-images-1575-%C3%97-871px-30.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[WAF for Traefik with CrowdSec: Virtual Patching in Docker]]></title>
            <link>https://crowdsec.net/blog/waf-traefik-crowdsec</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/waf-traefik-crowdsec</guid>
            <pubDate>Fri, 24 Jul 2026 11:49:29 GMT</pubDate>
            <description><![CDATA[<p>Add an open-source WAF to Traefik with CrowdSec. Wire the bouncer plugin, enable virtual patching, and block real attacks, step by step in Docker.</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph">Traefik gives you a real front door. It terminates TLS, discovers your services as they start and stop, routes by host and path, load-balances across replicas, retries failed backends, and chains middlewares for auth, redirects, rate limits, and headers. What it does not do, out of the box, is look at what is <em>inside</em> the requests it forwards. Traefik&#8217;s job is to deliver the request; deciding whether that request is an attack was always someone else&#8217;s.</p>



<p class="wp-block-paragraph">That someone is a WAF, and on Traefik it slots in exactly where the gap is: as one more middleware in the chain. CrowdSec gives you an open-source one, and you can switch it on the safe way first. That safe way is <strong>virtual patching</strong>, which blocks the specific shapes of known exploits and known-bad paths and leaves everything else untouched. You get real protection immediately, with no tuning required.</p>



<p class="wp-block-paragraph">This guide takes a Traefik + Docker Compose stack from no request inspection to a working WAF in <strong>virtual-patching mode</strong>, the safest way to start, because it refuses attacks it can name without ever getting in your users&#8217; way.</p>



<h2 class="wp-block-heading">Why Traefik needs a WAF of its own</h2>



<p class="wp-block-paragraph">Three reasons this earns a place in your middleware chain.</p>



<p class="wp-block-paragraph"><strong>You are running code you did not write.</strong> Traefik almost never sits in front of your own application. It fronts other people&#8217;s: a self-hosted dashboard, a vendor&#8217;s container, an app built on a framework you pulled from a registry. You cannot add a security library inside software you do not maintain, and you are not going to audit all of it. Traefik is the one place every one of those requests passes through in the clear, which makes it the only practical place to inspect them. A WAF here protects the apps you depend on but do not control.</p>



<p class="wp-block-paragraph"><strong>It closes the gap between a CVE and your upgrade.</strong> When a vulnerability in a popular app goes public, mass scanning for it starts within hours, the same internet-wide spray that finds every new host within minutes of it coming online. Your realistic window to read the advisory, test the patch, and roll it out is measured in days. Virtual patching covers that gap: the moment a rule for the exploit&#8217;s shape ships, the WAF refuses that request at the edge, whether or not the app behind it is patched yet. You stop racing the scanners and buy the time to upgrade on your own schedule.</p>



<p class="wp-block-paragraph"><strong>Its rules come from a network, not just a file.</strong> A classic WAF knows exactly what its own signature set knows, and nothing more. CrowdSec&#8217;s is fed by every other engine running it: an IP caught probing someone else&#8217;s server becomes an IP yours refuses before it ever reaches your apps, and the same Traefik plugin enforces those community bans in the same pass as the WAF verdict. Your defense gets sharper because everyone else&#8217;s is running too, something a lone WAF, however good its rules, cannot do.d, first-party path is gone, and rebuilding a dynamic module against every Nginx release was never anyone&#8217;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.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">What &#8220;CrowdSec&#8217;s WAF&#8221; actually is on Traefik</h2>



<p class="wp-block-paragraph">Two pieces do the work, and it helps to know which is which before you touch a config file.</p>



<ul class="wp-block-list">
<li><strong>The AppSec component</strong> is an HTTP server bundled inside the CrowdSec agent. It holds the rules and returns a verdict, <em>allow</em> or <em>block</em>, for any request it is shown. It does not sit inline with your traffic.</li>



<li><strong>The Traefik bouncer</strong> is a middleware plugin (<a href="https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" target="_blank" rel="noreferrer noopener">maxlerebourg/crowdsec-bouncer-traefik-plugin</a>), loaded through Traefik&#8217;s built-in Yaegi engine, with no separate binary and no sidecar. It intercepts each request, shows a copy to AppSec, and enforces whatever verdict comes back.</li>
</ul>



<pre><code class="language-bash">client ───▶ traefik ───▶ bouncer plugin ───▶ AppSec (crowdsec:7422)
                         │                    │
                         │          verdict ◀─┘
                         ▼
                    allow (200) / block (403)</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li>AppSec is the brain, the plugin is the muscle. AppSec never blocks anything itself; the plugin does.</li>



<li>The verdict round-trip stays on the Docker network, so it costs microseconds, not an internet hop.</li>



<li>The same plugin that enforces WAF verdicts also enforces IP bans from CrowdSec&#8217;s community blocklist. One middleware, two jobs.</li>



<li>The plugin speaks the same protocol to the same AppSec component that the Nginx, HAProxy, Caddy, and Envoy bouncers do, so the WAF rules and verdicts are identical whatever proxy you run.</li>
</ul>



<p class="wp-block-paragraph">Full reference: the <a href="https://docs.crowdsec.net/docs/appsec/intro" target="_blank" rel="noreferrer noopener">AppSec component docs</a>.</p>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>A host with <strong>Docker</strong> and the <strong>Compose</strong> plugin.</li>



<li>Outbound HTTPS to the CrowdSec hub (rules) and <code>api.crowdsec.net</code> (community signals).</li>
</ul>



<p class="wp-block-paragraph">That is the whole list. No database to stand up, no proxy rearchitecting; the WAF slots in as one more middleware.</p>



<h2 class="wp-block-heading">Step 1 — Stand up CrowdSec next to Traefik</h2>



<p class="wp-block-paragraph">The whole stack is one Compose file: the CrowdSec agent (which carries the WAF), Traefik, and a <code>whoami</code> container as the app to protect.<br></p>



<pre><code class="language-bash"># docker-compose.yml
services:
  crowdsec:
    image: crowdsecurity/crowdsec:v1.7.8
    container_name: crowdsec
    restart: unless-stopped
    environment:
      COLLECTIONS: "crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules"
      BOUNCER_KEY_traefik: "${BOUNCER_KEY_TRAEFIK}"
    volumes:
      - ./acquis.d:/etc/crowdsec/acquis.d
      - crowdsec-config:/etc/crowdsec        # persist config, credentials, Console enrollment
      - crowdsec-data:/var/lib/crowdsec/data
    networks: [web]

  traefik:
    image: traefik:v3.5
    container_name: traefik
    restart: unless-stopped
    depends_on: [crowdsec]
    ports: ["80:80"]
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - ./dynamic:/etc/traefik/dynamic:ro
      - traefik-plugins:/plugins-storage
    networks: [web]

  whoami:
    image: traefik/whoami:latest
    container_name: whoami
    restart: unless-stopped
    networks: [web]

networks:
  web:
volumes:
  crowdsec-config:
  crowdsec-data:
  traefik-plugins:</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Two lines in the <code>crowdsec</code> service do the heavy lifting. <code>COLLECTIONS</code> installs the virtual-patching rule sets on first boot. <code>BOUNCER_KEY_traefik</code> mints a fixed API key for the plugin, so it authenticates on start with no manual key copying. Pick any long random string and set it in a <code>.env</code> file next to the Compose file:</p>



<pre><code class="language-bash">echo "BOUNCER_KEY_TRAEFIK=$(openssl rand -hex 24)" > .env</code></pre>



<p class="wp-block-paragraph">Tell the AppSec component to listen, in acquis.d/appsec.yaml:</p>



<pre><code class="language-bash"># acquis.d/appsec.yaml
appsec_config: crowdsecurity/appsec-default
labels:
  type: appsec
listen_addr: 0.0.0.0:7422
source: appsec</code></pre>



<p class="wp-block-paragraph">Bring it up and confirm AppSec is listening:<br></p>



<pre><code class="language-bash">$ docker compose up -d
$ docker compose logs crowdsec | grep Appsec
crowdsec | Appsec listening on 0.0.0.0:7422
$ docker exec crowdsec cscli metrics show appsec
 Appsec Engine | Processed | Blocked
 0.0.0.0:7422/ | 0         | 0</code></pre>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li>Install <strong>both</strong> collections together. <code>crowdsecurity/appsec-default</code> draws on rules from <code>appsec-virtual-patching</code> <em>and</em> <code>appsec-generic-rules</code>; the pair gives you the complete virtual-patching set.</li>



<li>AppSec binds <code>0.0.0.0:7422</code>, not <code>127.0.0.1.</code> Loopback inside the CrowdSec container is unreachable from the Traefik container; they only share the <code>web</code> network. The listener is not published to the host, so it is not exposed outside Docker.</li>



<li>Persist <strong>both</strong> volumes. <code>crowdsec-data</code> holds the decisions and alerts database; <code>crowdsec-config</code> holds <code>/etc/crowdsec</code>, where your API credentials and Console enrollment live. Leave the config volume out and a <code>docker compose down</code> drops the enrollment, so you re-enroll after every recreate.</li>



<li>At this stage CrowdSec <em>detects</em>. It does not yet <em>block</em> anything reaching Traefik; that is the plugin&#8217;s job, next.</li>
</ul>



<h2 class="wp-block-heading">Step 2: Load the plugin and wire the middleware</h2>



<p class="wp-block-paragraph">Register the plugin in Traefik&#8217;s static config, and trust the proxy hop so Traefik keeps the real client IP:</p>



<pre><code class="language-bash"># traefik.yml (static)
entryPoints:
  web:
    address: ":80"
    forwardedHeaders:
      trustedIPs:
        - "172.16.0.0/12"      # the Docker network / any LB in front of Traefik

experimental:
  plugins:
    bouncer:
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: v1.6.0

providers:
  file:
    directory: /etc/traefik/dynamic
    watch: true</code></pre>



<p class="wp-block-paragraph">Then define the middleware (and attach it to a router) in the dynamic config:<br></p>



<pre><code class="language-bash"># dynamic/middlewares.yml
http:
  routers:
    whoami:
      rule: "PathPrefix(`/`)"
      entryPoints: [web]
      service: whoami
      middlewares: [crowdsec]        # every request is screened by the WAF

  services:
    whoami:
      loadBalancer:
        servers:
          - url: "http://whoami:80"

  middlewares:
    crowdsec:
      plugin:
        bouncer:
          enabled: true
          crowdsecMode: stream               # poll the decision list (prod default)
          updateIntervalSeconds: 10          # a fresh ban lands within this window
          crowdsecLapiScheme: http
          crowdsecLapiHost: crowdsec:8080    # LAPI, over the Docker network
          crowdsecLapiKey: "<your-bouncer-key>"   # == BOUNCER_KEY_traefik above
          crowdsecAppsecEnabled: true        # turn ON the inline WAF
          crowdsecAppsecHost: crowdsec:7422  # the AppSec listener
          forwardedHeadersTrustedIPs:
            - "172.16.0.0/12"</code></pre>



<p class="wp-block-paragraph">Reload Traefik (or let the file provider pick it up, since watch: true) and confirm the plugin registered as a bouncer:</p>



<pre><code class="language-bash">$ docker exec crowdsec cscli bouncers list
 Name    Valid  Type                             Version  Auth Type
 traefik  ✔️     Crowdsec-Bouncer-Traefik-Plugin  1.5.0    api-key
</code></pre>



<p class="wp-block-paragraph">Prove the enforcement path works before trusting the WAF with anything. Ban an IP, wait one stream interval, and present it as the forwarded client:<br></p>



<pre><code class="language-bash">$ curl -s -o /dev/null -w 'before ban: %{http_code}\n' -H 'X-Forwarded-For: 192.168.1.142' http://127.0.0.1/   # 200
$ docker exec crowdsec cscli decisions add --ip 192.168.1.142 --duration 5m --reason test
$ curl -s -o /dev/null -w 'after ban:  %{http_code}\n' -H 'X-Forwarded-For: 192.168.1.142' http://127.0.0.1/   # 403
$ curl -s -o /dev/null -w 'clean IP:   %{http_code}\n' -H 'X-Forwarded-For: 192.168.1.1'  http://127.0.0.1/   # 200
$ docker exec crowdsec cscli decisions delete --ip 192.168.1.142
</code></pre>



<p class="wp-block-paragraph"><code>200 → 403 → 200</code>, and an unrelated IP stays <code>200</code>. The plugin is live and enforcing decisions.</p>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li><code>crowdsecLapiKey</code> must equal the <code>BOUNCER_KEY_traefik</code> you set in Compose. Traefik&#8217;s file provider does not expand environment variables, so this one is a literal.</li>



<li><code>crowdsecAppsecEnabled</code> defaults to <strong>false</strong>, so the WAF is off until you set it <code>true</code>. That single line is the difference between an IP-ban bouncer and a WAF.</li>



<li><code>crowdsecMode: </code>stream means the plugin polls the full decision list every <code>updateIntervalSeconds</code> and answers locally. A fresh ban lands within that window, so a ban-then<code>-curl</code> with no pause looks like a miss when it isn&#8217;t.</li>
</ul>



<style>
  .codalent-tip-box {
    background: #f7f6ff;
    border: 1px solid rgba(78, 74, 153, 0.35);
    border-left: 4px solid #4e4a99;
    border-radius: 8px;
    padding: 18px 22px;
    color: #2d2d3a;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
    max-width: 1120px;
    box-sizing: border-box;
  }

  .codalent-tip-inner {
    display: flex;
    align-items: flex-start;
    gap: 14px;
  }

  .codalent-tip-icon {
    width: 24px;
    height: 24px;
    min-width: 24px;
    border-radius: 50%;
    background: rgba(78, 74, 153, 0.12);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 1px;
  }

  .codalent-tip-icon svg {
    width: 15px;
    height: 15px;
    stroke: #4e4a99;
  }

  .codalent-tip-content {
    min-width: 0;
  }

  .codalent-tip-label {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: #4e4a99;
    margin-bottom: 8px;
  }

  .codalent-tip-box p {
    margin: 0;
    font-size: 15px;
    line-height: 1.6;
  }

  .codalent-tip-box code {
    background: rgba(78, 74, 153, 0.1);
    border: 1px solid rgba(78, 74, 153, 0.18);
    border-radius: 4px;
    padding: 2px 5px;
    color: #277a48;
    font-family: Consolas, Monaco, "Courier New", monospace;
    font-size: 0.9em;
    overflow-wrap: anywhere;
  }

  .codalent-tip-box a {
    color: #4e4a99;
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  @media (max-width: 600px) {
    .codalent-tip-box {
      padding: 16px;
    }

    .codalent-tip-inner {
      gap: 12px;
    }

    .codalent-tip-box p {
      font-size: 14px;
    }
  }
</style>

<div class="codalent-tip-box">
  <div class="codalent-tip-inner">

    <div class="codalent-tip-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none">
        <path
          d="M12 9v4"
          stroke-width="1.8"
          stroke-linecap="round">
        </path>
        <path
          d="M12 17h.01"
          stroke-width="2.2"
          stroke-linecap="round">
        </path>
        <path
          d="M10.3 4.6 3.4 16.5A2 2 0 0 0 5.1 19.5h13.8a2 2 0 0 0 1.7-3L13.7 4.6a2 2 0 0 0-3.4 0Z"
          stroke-width="1.8"
          stroke-linejoin="round">
        </path>
      </svg>
    </div>

    <div class="codalent-tip-content">
      <div class="codalent-tip-label">Note</div>

      <p>
        <strong>Keep the key out of your config in production.</strong>
        The literal <code>crowdsecLapiKey</code> is fine for getting started, but it lives in
        <code>dynamic/middlewares.yml</code>, which you probably do not want in version control.
        For anything you commit or ship, mount the key as a file instead (a Docker secret, or
        any file the container can read) and point the plugin at it with
        <code>crowdsecLapiKeyFile: /path/to/key</code> in place of
        <code>crowdsecLapiKey</code>. The
        <a href="https://docs.crowdsec.net/u/bouncers/traefik" target="_blank" rel="noopener noreferrer">official Traefik bouncer guide</a>
        walks through the secret-mounted pattern.
      </p>
    </div>

  </div>
</div>



<style>
  .codalent-tip-box {
    background: #f7f6ff;
    border: 1px solid rgba(78, 74, 153, 0.35);
    border-left: 4px solid #4e4a99;
    border-radius: 8px;
    padding: 18px 22px;
    color: #2d2d3a;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
    max-width: 1120px;
    box-sizing: border-box;
  }

  .codalent-tip-inner {
    display: flex;
    align-items: flex-start;
    gap: 14px;
  }

  .codalent-tip-icon {
    width: 24px;
    height: 24px;
    min-width: 24px;
    border-radius: 50%;
    background: rgba(78, 74, 153, 0.12);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 1px;
  }

  .codalent-tip-icon svg {
    width: 15px;
    height: 15px;
    stroke: #4e4a99;
  }

  .codalent-tip-content {
    min-width: 0;
  }

  .codalent-tip-label {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: #4e4a99;
    margin-bottom: 8px;
  }

  .codalent-tip-box p {
    margin: 0;
    font-size: 15px;
    line-height: 1.6;
  }

  .codalent-tip-box code {
    background: rgba(78, 74, 153, 0.1);
    border: 1px solid rgba(78, 74, 153, 0.18);
    border-radius: 4px;
    padding: 2px 5px;
    color: #277a48;
    font-family: Consolas, Monaco, "Courier New", monospace;
    font-size: 0.9em;
    overflow-wrap: anywhere;
  }

  @media (max-width: 600px) {
    .codalent-tip-box {
      padding: 16px;
    }

    .codalent-tip-inner {
      gap: 12px;
    }

    .codalent-tip-box p {
      font-size: 14px;
    }
  }
</style>

<div class="codalent-tip-box">
  <div class="codalent-tip-inner">

    <div class="codalent-tip-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none">
        <path
          d="M12 9v4"
          stroke-width="1.8"
          stroke-linecap="round">
        </path>
        <path
          d="M12 17h.01"
          stroke-width="2.2"
          stroke-linecap="round">
        </path>
        <path
          d="M10.3 4.6 3.4 16.5A2 2 0 0 0 5.1 19.5h13.8a2 2 0 0 0 1.7-3L13.7 4.6a2 2 0 0 0-3.4 0Z"
          stroke-width="1.8"
          stroke-linejoin="round">
        </path>
      </svg>
    </div>

    <div class="codalent-tip-content">
      <div class="codalent-tip-label">Note</div>

      <p>
        <strong>The #1 Traefik gotcha: the real client IP.</strong>
        Traefik rewrites <code>X-Forwarded-For</code> to the immediate peer unless the
        <strong>entrypoint</strong> trusts that hop. Miss the
        <code>forwardedHeaders.trustedIPs</code> on the entrypoint and the plugin only ever
        sees the Docker gateway, so bans on the real client never match. Set it
        <strong>on the entrypoint and as the plugin&#8217;s</strong>
        <code>forwardedHeadersTrustedIPs</code>: two separate layers. And
        <strong>do not put</strong> your Docker range in the plugin&#8217;s
        <code>clientTrustedIPs</code>: anything listed there bypasses the bouncer entirely,
        and every request sails through.
      </p>
    </div>

  </div>
</div>



<h2 class="wp-block-heading">Step 3: What virtual patching is doing for you</h2>



<p class="wp-block-paragraph">The WAF is already on: crowdsecAppsecEnabled: true plus the two collections is the whole switch. Here is what it turned on.</p>



<p class="wp-block-paragraph"><strong>Virtual patching</strong> blocks requests that match <em>specific, known-bad shapes</em>: the query string of a published CVE exploit, a POST to a management endpoint for software you don&#8217;t even run, an injection payload that only a scanner would ever send. 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 is what makes it the safe first move: turn it on in blocking mode on day one and sleep fine.</p>



<h2 class="wp-block-heading">Step 4: Prove it blocks real attacks</h2>



<p class="wp-block-paragraph">Nothing exotic here. These are the exact paths scanners try on every host: a peek at a leaked <code>/.env</code>, a grab for <code>/.git/config</code>, an Exchange RCE probe. Each is a plain <code>curl</code>, so you can run them yourself.</p>



<p class="wp-block-paragraph">One wrinkle: to make CrowdSec <em>record</em> the attacks, not just answer them with a 403, the source has to look like a public IP, because CrowdSec whitelists private and loopback ranges by default. Testing from the box itself, you fake that with an <code>X-Forwarded-For</code> header (Traefik trusts it thanks to the entrypoint config from Step 2); from a separate machine you would not need to.</p>



<pre><code class="language-bash">IP=192.168.1.42   # any non-whitelisted address
curl -s -o /dev/null -w '%{http_code}  .env\n'       -H "X-Forwarded-For: $IP" http://127.0.0.1/.env
curl -s -o /dev/null -w '%{http_code}  git\n'        -H "X-Forwarded-For: $IP" http://127.0.0.1/.git/config
curl -s -o /dev/null -w '%{http_code}  exchange\n'   -H "X-Forwarded-For: $IP" 'http://127.0.0.1/autodiscover/autodiscover.json?a=powershell'
curl -s -o /dev/null -w '%{http_code}  benign\n'     -H "X-Forwarded-For: $IP" http://127.0.0.1/
</code></pre>



<pre><code class="language-bash">403  .env
403  git
403  exchange
200  benign</code></pre>



<p class="wp-block-paragraph">Three refused, one allowed. Send each bad request a few more times (scanners never knock once), then read the WAF&#8217;s own counters:</p>



<pre><code class="language-bash">$ docker exec crowdsec cscli metrics show appsec
 Appsec Engine | Processed | Blocked
 0.0.0.0:7422/ | 11        | 9

 Rule ID                             | Triggered
 crowdsecurity/vpatch-CVE-2022-41082 | 1
 crowdsecurity/vpatch-env-access     | 4
 crowdsecurity/vpatch-git-config     | 4</code></pre>



<p class="wp-block-paragraph">Each block is attributed to the exact rule that caught it. Now look at what CrowdSec recorded:</p>



<pre><code class="language-bash">$ docker exec crowdsec cscli alerts list
 value           | reason                              | kind     | decisions
 Ip:192.168.1.42 | crowdsecurity/vpatch-git-config     | waf      |
 Ip:192.168.1.42 | crowdsecurity/vpatch-env-access     | waf      |
 Ip:192.168.1.42 | crowdsecurity/appsec-vpatch         | crowdsec | ban:1
 Ip:192.168.1.42 | crowdsecurity/vpatch-CVE-2022-41082 | waf      |</code></pre>



<p class="wp-block-paragraph">Two things happened at once, and this is where CrowdSec pulls ahead of a plain WAF:</p>



<ol class="wp-block-list">
<li><code><strong>kind: waf</strong>:</code> each malicious request was refused on the spot with a 403. That is virtual patching doing its job, per request.</li>



<li><strong><code>kind: crowdsec </code>with <code>ban:1</code></strong>: because the same IP kept probing, a CrowdSec scenario aggregated the behavior and <em>banned the address outright</em>. The next request from that IP, to <em>any</em> path, is refused before the WAF even looks at it.</li>
</ol>



<pre><code class="language-bash">$ docker exec crowdsec cscli decisions list
 Source   | Scope:Value     | Reason                      | Action | Expiration
 crowdsec | Ip:192.168.1.42 | crowdsecurity/appsec-vpatch | ban    | 3h59m</code></pre>



<p class="wp-block-paragraph">And the ban is real, enforced by the same plugin:</p>



<pre><code class="language-bash">$ curl -s -o /dev/null -w '%{http_code}\n' -H 'X-Forwarded-For: 192.168.1.42' http://127.0.0.1/   # 403</code></pre>



<p class="wp-block-paragraph">A classic WAF blocks the request. CrowdSec blocks the request <em>and</em> remembers the attacker.</p>



<p class="wp-block-paragraph">There is also an official smoke test that ships with CrowdSec: requesting <code>/crowdsec-test-NtktlJHV4TfBSK3wvlhiOBnl</code> trips the <code>crowdsecurity/appsec-generic-test</code> rule so you can confirm the pipeline end to end. The full health-check flow is documented <a href="https://doc.crowdsec.net/u/getting_started/health_check" target="_blank" rel="noreferrer noopener">here</a>.</p>



<h2 class="wp-block-heading">Step 5: See it in the Console</h2>



<p class="wp-block-paragraph">Blocking attacks from the command line is satisfying, but you will want a dashboard, one place to watch alerts across every proxy and server you protect. The <strong>CrowdSec Console</strong> is that view, and it is free for the community tier.</p>



<p class="wp-block-paragraph"><br>Enroll this engine (grab a key from <a href="https://app.crowdsec.net" target="_blank" rel="noreferrer noopener">app.crowdsec.net</a> → <strong>Security Engines → Enroll</strong>):<br></p>



<pre><code class="language-bash">docker exec crowdsec cscli console enroll <YOUR-ENROLL-KEY> --name crowdsec-vm-traefik-waf
docker exec crowdsec cscli console enable context   # richer alert detail for the WAF view
docker restart crowdsec</code></pre>



<p class="wp-block-paragraph">Then <strong>accept the instance in the Console webapp</strong>. Enrollment is two-step, and nothing appears until you click Accept.<br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="702" src="https://cms.crowdsec.net/wp-content/uploads/2026/07/image-1-1024x702.png" alt="" class="wp-image-6727" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/07/image-1-1024x702.png 1024w, https://cms.crowdsec.net/wp-content/uploads/2026/07/image-1-300x206.png 300w, https://cms.crowdsec.net/wp-content/uploads/2026/07/image-1-768x526.png 768w, https://cms.crowdsec.net/wp-content/uploads/2026/07/image-1.png 1156w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li>You enroll the <strong>engine</strong>, not the bouncer; the Traefik plugin appears in the Console <em>through</em> the engine it reports to.</li>



<li><code>cscli capi status</code> confirms the community signal loop: sharing enabled, community blocklist pulling. That blocklist is the payoff of open source: every CrowdSec user&#8217;s confirmed attackers become IPs you block before they ever reach Traefik.</li>



<li>The Console is a lens on data that already lives on your box. Turn it off and the WAF keeps working exactly the same.</li>
</ul>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="1309" height="645" src="https://cms.crowdsec.net/wp-content/uploads/2026/07/Captura-de-pantalla-2026-07-27-a-las-14.16.42.png" alt="" class="wp-image-6748" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/07/Captura-de-pantalla-2026-07-27-a-las-14.16.42.png 1309w, https://cms.crowdsec.net/wp-content/uploads/2026/07/Captura-de-pantalla-2026-07-27-a-las-14.16.42-300x148.png 300w, https://cms.crowdsec.net/wp-content/uploads/2026/07/Captura-de-pantalla-2026-07-27-a-las-14.16.42-1024x505.png 1024w, https://cms.crowdsec.net/wp-content/uploads/2026/07/Captura-de-pantalla-2026-07-27-a-las-14.16.42-768x378.png 768w" sizes="auto, (max-width: 1309px) 100vw, 1309px" /></figure>



<h2 class="wp-block-heading">Where this leaves you</h2>



<p class="wp-block-paragraph">In a few minutes you went from an unprotected Traefik stack to an open-source WAF that:</p>



<ul class="wp-block-list">
<li>refuses known exploit shapes on sight, with virtually no false-positive risk;</li>



<li>bans persistent attackers automatically, across every route Traefik serves;</li>



<li>pulls a community-sourced blocklist so you benefit from attacks aimed at everyone else;</li>



<li>reports into a single Console dashboard.</li>
</ul>



<p class="wp-block-paragraph">Virtual patching is the floor, not the ceiling. It only catches attacks it already knows by name. To catch the ones it doesn&#8217;t, the novel injection or the odd traversal, you bring in the <strong>OWASP Core Rule Set</strong>, and you decide whether to run it inband (block now) or out-of-band (watch first, block once you trust it). That is exactly the false-positive tradeoff virtual patching let you skip, and it is a natural next step from here.</p>



<p class="wp-block-paragraph">You built all of this with software you can read, audit, and run for free. No black box, no vendor lock, no per-request meter. Just Traefik, refusing the requests that were never meant well.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><em>Full documentation lives at </em><a href="https://docs.crowdsec.net/docs/appsec/intro" target="_blank" rel="noreferrer noopener"><em>docs.crowdsec.net/docs/appsec</em></a><em>, the Traefik plugin is open at </em><a href="https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" target="_blank" rel="noreferrer noopener"><em>github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin</em></a><em>, and the rules are on the </em><a href="https://app.crowdsec.net/hub" target="_blank" rel="noreferrer noopener"><em>CrowdSec Hub</em></a><em>.</em></p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
            <category>Integrations</category>
            <category>Proactive Cybersecurity</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/07/257.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Open-Source WAF for Nginx on Ubuntu with CrowdSec]]></title>
            <link>https://crowdsec.net/blog/open-source-waf-for-nginx-on-ubuntu-with-crowdsec</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/open-source-waf-for-nginx-on-ubuntu-with-crowdsec</guid>
            <pubDate>Thu, 09 Jul 2026 09:05:50 GMT</pubDate>
            <description><![CDATA[<p>Deploy an open-source WAF for Nginx on Ubuntu with CrowdSec. Install the AppSec component, enable virtual patching, and block attacks step by step</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph">Point a brand-new web server at the internet and start a timer. Within minutes, often seconds, the first probe lands: a request for <code>/.env</code>, hoping you left a database password in it. Next comes <code>/.git/config</code>, 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.</p>



<p class="wp-block-paragraph">You can refuse that traffic on day one, without deploying a heavy ruleset that trips over your own customers. The approach is called <strong>virtual patching</strong>: 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.</p>



<p class="wp-block-paragraph">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&#8217;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&#8217;t do.</p>



<h2 class="wp-block-heading">Why a new WAF, and why now</h2>



<p class="wp-block-paragraph">Three reasons to do this now rather than someday.</p>



<p class="wp-block-paragraph"><strong>ModSecurity&#8217;s Nginx era is ending.</strong> If you have run a WAF on Nginx before, it was probably ModSecurity. F5 ended support for the Nginx ModSecurity WAF module on <strong>March 31, 2024</strong>. The engine still runs, but the maintained, first-party path is gone, and rebuilding a dynamic module against every Nginx release was never anyone&#8217;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.</p>



<p class="wp-block-paragraph"><strong>Virtual patching lets you start without fear of false positives.</strong> 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 <em>specific</em> 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&#8217;s the OWASP Core Rule Set, which is the subject of the next post. It comes deliberately later precisely because it <em>does</em> need tuning.</p>



<p class="wp-block-paragraph"><strong>It is not tied to Nginx.</strong> The WAF logic lives in the CrowdSec agent, not in your web server. A thin <em>bouncer</em> 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&#8217;s plugin ecosystem.</p>



<p class="wp-block-paragraph">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.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading">What &#8220;CrowdSec&#8217;s WAF&#8221; actually is</h2>



<p class="wp-block-paragraph">Two pieces do the work, and it helps to know which is which before you start typing commands.</p>



<ul class="wp-block-list">
<li><strong>The AppSec component</strong> is an HTTP server bundled inside the CrowdSec agent. It holds the rules and returns a verdict (<em>allow</em> or <em>block</em>) for any request it is shown. It does not sit inline with your traffic.</li>



<li><strong>The Nginx bouncer</strong> is a small Lua module inside Nginx. It intercepts each request, shows a copy to AppSec, and enforces whatever verdict comes back.</li>
</ul>



<pre><code class="language-bash">client ──► nginx ──► nginx bouncer ──► AppSec (127.0.0.1:7422)
                          │                  │
                          │      verdict ◄───┘
                          ▼
                    allow (200) / block (403)
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li>AppSec makes the decision and the bouncer carries it out. AppSec never blocks anything on its own; the bouncer does.</li>



<li>The verdict round-trip is local (loopback), so it costs microseconds, not a network hop.</li>



<li>The same bouncer that enforces WAF verdicts also enforces IP bans from CrowdSec&#8217;s community blocklist, so it&#8217;s one module doing two jobs.</li>



<li>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.</li>
</ul>



<p class="wp-block-paragraph">Full reference: the <a href="https://docs.crowdsec.net/docs/appsec/intro" target="_blank" rel="noreferrer noopener">AppSec component docs</a>.</p>



<h2 class="wp-block-heading">Prerequisites</h2>



<ul class="wp-block-list">
<li>An Ubuntu server (we used 24.04/26.04) with <strong>Nginx</strong> already serving a site.</li>



<li>Root or sudo.</li>



<li>Outbound HTTPS to packagecloud.io (packages) and api.crowdsec.net (community signals).</li>
</ul>



<p class="wp-block-paragraph">That&#8217;s the whole list. You don&#8217;t need to stand up a database or rearchitect your reverse proxy.<br></p>



<h2 class="wp-block-heading">Step 1 — Install the CrowdSec engine</h2>



<p class="wp-block-paragraph">Add the official repository and install the agent:</p>



<pre><code class="language-bash">curl -s https://install.crowdsec.net | sudo sh   # adds the signed CrowdSec repo

sudo apt install crowdsec
</shadowed></code></pre>



<pre><code class="language-bash">$ sudo cscli version
version: v1.7.8-debian-pragmatic-amd64
Codename: alphaga
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">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:</p>



<pre><code class="language-bash"># /etc/crowdsec/acquis.d/setup.nginx.yaml
filenames:
  - /var/log/nginx/*.log
labels:
  type: nginx
source: file
</code></pre>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li>Install from the <strong>official packagecloud repo</strong>, not Ubuntu&#8217;s own crowdsec package, since the distro copy can be years behind.</li>



<li>The wizard already installed the crowdsecurity/nginx collection, so CrowdSec is parsing your access logs from the first second.</li>



<li>At this stage CrowdSec <em>detects</em> bad behavior. It does not yet <em>block</em> anything; that&#8217;s the bouncer&#8217;s job, next.</li>
</ul>



<h2 class="wp-block-heading">Step 2 — Install and wire the Nginx bouncer</h2>



<pre><code class="language-bash">sudo apt install crowdsec-nginx-bouncer
</code></pre>



<p class="wp-block-paragraph">The package registers itself with the local API and drops its Nginx config in place, with no manual key copying:</p>



<pre><code class="language-bash">$ sudo cscli bouncers list

 Name                               Valid  Auth Type

 crowdsec-nginx-bouncer-1783001915  ✔️     api-key
</code></pre>



<p class="wp-block-paragraph">Prove the enforcement path works before adding any WAF rules. Ban your own loopback for two minutes and watch Nginx start refusing you:</p>



<pre><code class="language-bash">$ 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
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"><code>200 → 403 → 200</code>. The bouncer is live and enforcing decisions. Now we give it a WAF to consult.</p>



<h2 class="wp-block-heading">Step 3 — Deploy the WAF in virtual-patching mode</h2>



<p class="wp-block-paragraph">This is the core of the setup, and the reason to start here rather than with a big generic ruleset.</p>



<p class="wp-block-paragraph"><strong>Virtual patching</strong> blocks requests that match <em>specific, known-bad shapes</em>: the URL of a published CVE exploit, a request for <code>/.env</code>, an attempt to read <code>/.git/config</code>. 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.</p>



<p class="wp-block-paragraph">Install the WAF collections:</p>



<pre><code class="language-bash">sudo cscli collections install \

    crowdsecurity/appsec-virtual-patching \

    crowdsecurity/appsec-generic-rules
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">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).</p>



<p class="wp-block-paragraph">Tell the AppSec component to listen, by creating <code>/etc/crowdsec/acquis.d/appsec.yaml</code>:</p>



<pre><code class="language-bash">appsec_config: crowdsecurity/appsec-default

labels:

  type: appsec

listen_addr: 127.0.0.1:7422

source: appsec
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Reload and confirm the WAF is listening:</p>



<pre><code class="language-bash">$ sudo systemctl reload crowdsec

$ sudo ss -lntp | grep 7422

LISTEN 0 4096 127.0.0.1:7422 0.0.0.0:*  users:(("crowdsec",...))
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Finally, point the Nginx bouncer at it. The bouncer ships with the WAF switched off (<code>APPSEC_URL</code> empty); one line turns it on:</p>



<pre><code class="language-bash">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
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li><code>crowdsecurity/appsec-default</code> is the config that carries the virtual-patching rules <em>and</em> the health-check test rule, all in one config, ready to go.</li>



<li>The listener is loopback-only (<code>127.0.0.1</code>) 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.</li>



<li>Reusing the bouncer&#8217;s existing API key for AppSec is automatic. There is no second key to create.</li>
</ul>



<h2 class="wp-block-heading">Step 4 — Prove it blocks real attacks</h2>



<p class="wp-block-paragraph">Fire a few malicious requests at the server and one honest one:</p>



<pre><code class="language-bash">.env access             -> 403

phpunit CVE-2017-9841   -> 403

git config leak         -> 403

benign homepage         -> 200
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Then read the WAF&#8217;s own counters:</p>



<pre><code class="language-bash">$ 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
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Each block is attributed to the exact rule that caught it. Now look at what CrowdSec recorded:</p>



<pre><code class="language-bash">$ 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      |
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">Two things happened at once, and this is where CrowdSec pulls ahead of a plain WAF:</p>



<ol class="wp-block-list">
<li><code><strong>kind: waf</strong>:</code> each malicious request was refused on the spot with a 403. That&#8217;s virtual patching doing its job, per request.</li>



<li><strong><code>kind: crowdsec</code> with <code>ban:1</code></strong>: because the same IP kept probing, CrowdSec&#8217;s scenarios aggregated the behavior and <em>banned the address outright</em>. The next request from that IP, to <em>any</em> path, is refused before the WAF even looks at it.</li>
</ol>



<pre><code class="language-bash">$ sudo cscli decisions list

 crowdsec | Ip:203.0.113.42 | crowdsecurity/CVE-2017-9841 | ban | FR | 3h59m
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">A classic WAF blocks the request. CrowdSec blocks the request <em>and</em> remembers the attacker.</p>



<style>
  .codalent-tip-box {
    background: #f7f6ff;
    border: 1px solid rgba(78, 74, 153, 0.35);
    border-left: 4px solid #4e4a99;
    border-radius: 8px;
    padding: 18px 22px;
    color: #2d2d3a;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
    max-width: 1120px;
    box-sizing: border-box;
  }

  .codalent-tip-inner {
    display: flex;
    align-items: flex-start;
    gap: 14px;
  }

  .codalent-tip-icon {
    width: 24px;
    height: 24px;
    min-width: 24px;
    border-radius: 50%;
    background: rgba(78, 74, 153, 0.12);
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 1px;
  }

  .codalent-tip-icon svg {
    width: 15px;
    height: 15px;
    stroke: #4e4a99;
  }

  .codalent-tip-label {
    font-size: 10px;
    font-weight: 800;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: #4e4a99;
    margin-bottom: 8px;
  }

  .codalent-tip-box p {
    margin: 0;
    font-size: 15px;
    line-height: 1.6;
  }

  @media (max-width: 600px) {
    .codalent-tip-box {
      padding: 16px;
    }

    .codalent-tip-inner {
      gap: 12px;
    }

    .codalent-tip-box p {
      font-size: 14px;
    }
  }
</style>

<div class="codalent-tip-box">
  <div class="codalent-tip-inner">
    <div class="codalent-tip-icon" aria-hidden="true">
      <svg viewBox="0 0 24 24" fill="none">
        <path d="M12 9v4" stroke-width="1.8" stroke-linecap="round"></path>
        <path d="M12 17h.01" stroke-width="2.2" stroke-linecap="round"></path>
        <path d="M10.3 4.6 3.4 16.5A2 2 0 0 0 5.1 19.5h13.8a2 2 0 0 0 1.7-3L13.7 4.6a2 2 0 0 0-3.4 0Z" stroke-width="1.8" stroke-linejoin="round"></path>
      </svg>
    </div>

    <div class="codalent-tip-content">
      <div class="codalent-tip-label">Note</div>

      <p><b>One gotcha</b>: 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.</p>
    </div>
  </div>
</div>



<p class="wp-block-paragraph">There is also an official smoke test that ships with CrowdSec. Requesting the path <code>/crowdsec-test-NtktlJHV4TfBSK3wvlhiOBnl</code> triggers the <code>crowdsecurity/appsec-generic-test rule</code>; the full health-check flow is documented <a href="https://doc.crowdsec.net/u/getting_started/health_check" target="_blank" rel="noreferrer noopener">here</a>.</p>



<h2 class="wp-block-heading">Step 5 — See it in the Console</h2>



<p class="wp-block-paragraph">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 <strong>CrowdSec Console</strong> is that view, and it is free for the community tier.</p>



<p class="wp-block-paragraph">Enroll this engine (grab an enrollment key from <a href="https://app.crowdsec.net" target="_blank" rel="noreferrer noopener">app.crowdsec.net</a> → <strong>Security Engines → Enroll</strong>):</p>



<pre><code class="language-bash">sudo cscli console enroll <YOUR-ENROLL-KEY> --name crowdsec-vm-nginx-waf

sudo systemctl reload crowdsec
</code></pre>



<p class="wp-block-paragraph">Then <strong>accept the instance in the Console webapp</strong> — enrollment is two-step, and nothing shows up until you click Accept. Turn on richer alert detail while you are at it:</p>



<pre><code class="language-bash">sudo cscli console enable context

sudo systemctl reload crowdsec</code></pre>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="681" src="https://cms.crowdsec.net/wp-content/uploads/2026/07/console-engine-1-1024x681.png" alt="" class="wp-image-6711" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/07/console-engine-1-1024x681.png 1024w, https://cms.crowdsec.net/wp-content/uploads/2026/07/console-engine-1-300x200.png 300w, https://cms.crowdsec.net/wp-content/uploads/2026/07/console-engine-1-768x511.png 768w, https://cms.crowdsec.net/wp-content/uploads/2026/07/console-engine-1.png 1165w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="105" src="https://cms.crowdsec.net/wp-content/uploads/2026/07/waf-alerts-1-1024x105.png" alt="" class="wp-image-6712" srcset="https://cms.crowdsec.net/wp-content/uploads/2026/07/waf-alerts-1-1024x105.png 1024w, https://cms.crowdsec.net/wp-content/uploads/2026/07/waf-alerts-1-300x31.png 300w, https://cms.crowdsec.net/wp-content/uploads/2026/07/waf-alerts-1-768x79.png 768w, https://cms.crowdsec.net/wp-content/uploads/2026/07/waf-alerts-1-1536x158.png 1536w, https://cms.crowdsec.net/wp-content/uploads/2026/07/waf-alerts-1.png 1540w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>Key points:</strong></p>



<ul class="wp-block-list">
<li>You enroll the <strong>engine</strong>, not each bouncer; bouncers appear in the Console <em>through</em> the engine they report to.</li>



<li><code>cscli capi status</code> confirms the community signals loop: sharing enabled, community blocklist pulling. That blocklist is the payoff of open source: every CrowdSec user&#8217;s confirmed attackers become IPs you block before they ever reach you.</li>



<li>The Console is a lens on data that already lives on your box. Turn it off and your WAF keeps working exactly the same.</li>
</ul>



<h2 class="wp-block-heading">Where this leaves you</h2>



<p class="wp-block-paragraph">In a few minutes you went from a bare Nginx server to an open-source WAF that:</p>



<ul class="wp-block-list">
<li>refuses known exploit shapes on sight, with virtually no false-positive risk;</li>



<li>bans persistent attackers automatically, across your whole site;</li>



<li>pulls a community-sourced blocklist so you benefit from attacks aimed at everyone else;</li>



<li>reports into a single Console dashboard.</li>
</ul>



<p class="wp-block-paragraph">Virtual patching is a floor, not a ceiling. It only catches attacks it already knows by name. To catch the ones it doesn&#8217;t, the novel injection or the odd traversal, you bring in the <strong>OWASP Core Rule Set</strong>, and you decide whether to run it inband (block now) or out-of-band (watch first, block once you trust it). That&#8217;s exactly the false-positive tradeoff virtual patching let us skip, and it&#8217;s where the next post picks up.</p>



<p class="wp-block-paragraph">After that: the <strong>hooks</strong> that let CrowdSec&#8217;s WAF do things a signature engine can&#8217;t, and how to run the WAF and the engine as a <strong>distributed</strong> pair across many servers.</p>



<p class="wp-block-paragraph">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.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><em>Next in this series: </em><strong><em>Going further — OWASP CRS inband vs out-of-band with CrowdSec.</em></strong><em> Full documentation lives at </em><a href="https://docs.crowdsec.net/docs/appsec/intro" target="_blank" rel="noreferrer noopener"><em>docs.crowdsec.net/docs/appsec</em></a><em>, and the rules are open on the </em><a href="https://app.crowdsec.net/hub" target="_blank" rel="noreferrer noopener"><em>CrowdSec Hub</em></a><em>.</em></p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
            <category>Integrations</category>
            <category>Proactive Cybersecurity</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/07/255-1.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[CISA gives you 3 days to patch and triage. Live Exploit Tracker gives you a head start]]></title>
            <link>https://crowdsec.net/blog/cisa-bod-26-04-live-exploit-tracker</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/cisa-bod-26-04-live-exploit-tracker</guid>
            <pubDate>Tue, 16 Jun 2026 13:23:19 GMT</pubDate>
            <description><![CDATA[<p>CISA BOD 26-04 mandates risk-based patching driven by real exploitation evidence. Live Exploit Tracker delivers that evidence live — per CVE, per vendor.</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph"><em>On June 10, 2026, CISA fundamentally rewrote how federal agencies must prioritize patching. The new directive assumes you can answer one question continuously, for every CVE in your environment: &#8220;Is this being exploited, right now, and by whom?&#8221; That is precisely the question Live Exploit Tracker was built to answer.</em><em><br></em><em><br></em><em>CISA&#8217;s BOD 26-04 Just Made Exploitation Evidence Mandatory. We&#8217;ve Been Collecting It Live, at Scale, for Years</em>.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading"><strong>What BOD 26-04 actually changes</strong></h2>



<p class="wp-block-paragraph"><a href="https://www.cisa.gov/news-events/directives/bod-26-04-prioritizing-security-updates-based-risk" target="_blank" rel="noreferrer noopener">Binding Operational Directive 26-04, &#8220;Prioritizing Security Updates Based on Risk&#8221;</a>, supersedes both BOD 22-01 (KEV remediation) and BOD 19-02 (internet-accessible systems). The flat &#8220;<em>it&#8217;s in the KEV, patch it in 14 days</em>&#8221; era is over. In its place: a graduated model built on four binary risk variables, evaluated per vulnerability, per asset:</p>



<ol class="wp-block-list">
<li><strong>Publicly exposed</strong> — is the vulnerable asset reachable from the internet?</li>



<li><strong>Known exploitation</strong> — is the CVE in CISA&#8217;s KEV catalog?</li>



<li><strong>Automatable</strong> — can an adversary automate the full exploitation chain?</li>



<li><strong>Technical impact</strong> — does exploitation yield total or partial control?</li>
</ol>



<p class="wp-block-paragraph">The combination of answers maps to remediation timelines ranging from <strong>three calendar days with mandatory forensic triage</strong> at the top, down to &#8220;fix on next system upgrade&#8221; at the bottom. Notably, CISA no longer requires CVSS for prioritization. Severity scores are out; <em>evidence of real-world exploitation</em> is in.</p>



<p class="wp-block-paragraph">CISA is explicit about why now: AI is compressing the window between disclosure and weaponization, while remediation performance is moving the wrong way — per the 2026 Verizon DBIR figures CISA cites, only 26% of KEV vulnerabilities were fully remediated in 2025, down from 38% the year before.</p>



<p class="wp-block-paragraph">The directive&#8217;s logic is sound. Its operational demand is brutal: agencies now need <strong>continuous, current, per-CVE exploitation intelligence</strong> — not a quarterly threat report, not a static severity score. And for the highest tier, they need to determine within 72 hours whether they&#8217;ve <em>already</em> been compromised.</p>



<p class="wp-block-paragraph">This is exactly the gap<a href="https://www.crowdsec.net/live-exploit-tracker" target="_blank" rel="noreferrer noopener"> Live Exploit Tracker</a> closes.</p>



<h2 class="wp-block-heading"><strong>Live Exploit Tracker: ground truth on exploitation, from hundreds of thousands of real sensors</strong></h2>



<p class="wp-block-paragraph"><a href="https://tracker.crowdsec.net/?utm_source=website&amp;utm_medium=article&amp;utm_campaign=CISA" target="_blank" rel="noreferrer noopener">Live Exploit Tracker</a> (L.E.T.) is powered by the CrowdSec Network — the largest crowdsourced threat intelligence network in the world, with servers deployed across real production infrastructure in 180+ countries. When an attacker probes or exploits a CVE anywhere in the network, we see it. Not a honeypot guess. Not a vendor advisory. Actual exploitation attempts against actual workloads.</p>



<p class="wp-block-paragraph">For every tracked CVE, L.E.T. tells you:</p>



<ul class="wp-block-list">
<li><strong>The exploitation phase</strong> — recon, targeted exploitation, mass exploitation, or decline. Watching a CVE move from &#8220;insufficient data&#8221; to &#8220;targeted exploitation&#8221; is watching your remediation deadline accelerate in real time.</li>



<li><strong>Momentum</strong> — is this threat growing, stable, or fading? A KEV entry from 2023 that nobody exploits anymore and a CVE that gained 80 attacking IPs this week are not the same priority. BOD 26-04 finally lets agencies act on that distinction.</li>



<li><strong>Who is attacking</strong> — every observed attacker IP, with full CTI profiling: origin, hosting provider, known/unknown actor, behavioral history across the network.</li>



<li><strong>What they&#8217;re after</strong> — takeover, ransom, or exfiltration, derived from observed attacker behavior.</li>



<li><strong>Indicators of compromise</strong> — the actual HTTP patterns and payloads observed exploiting the CVE in the wild.</li>
</ul>



<h2 class="wp-block-heading"><strong>Mapping L.E.T. to the directive, variable by variable</strong></h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td><strong>BOD 26-04 requirement</strong></td><td><strong>What it demands operationally</strong></td><td><strong>What Live Exploit Tracker provides</strong></td></tr><tr><td><strong>Known exploitation</strong> (variable 2)</td><td>Track KEV continuously; react the moment a CVE is added</td><td>Live exploitation telemetry that frequently precedes KEV listing — we observe in-the-wild exploitation directly, giving you a head start before the clock officially starts</td></tr><tr><td><strong>Automatable</strong> <br>(variable 3)</td><td>Assess whether exploitation can be fully automated</td><td>Direct field evidence: our targeted-vs-mass-exploitation classification distinguishes hand-driven campaigns from spray-and-pray automation, based on observed attacker behavior, not theoretical analysis</td></tr><tr><td><strong>3-day forensic triage</strong></td><td>Determine within 72h whether you were already compromised</td><td>Downloadable attacker IP lists and HTTP-level IoCs per CVE — grep your logs against the exact IPs and request patterns observed exploiting that vulnerability, going back months</td></tr><tr><td><strong>Dynamic timelines</strong></td><td>Re-prioritize the moment exploitation status changes</td><td>Phase-transition events and momentum scoring, updated continuously from live network signals, available via API for automated ingestion</td></tr><tr><td><strong>&#8220;KEV is not the only list&#8221;</strong> (CISA&#8217;s own implementation guidance)</td><td>Track exploitation beyond the KEV catalog</td><td>Coverage of actively exploited CVEs regardless of KEV status, including pre-KEV recon campaigns against specific vendors and products</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Variable 1 — knowing which of <em>your</em> assets are publicly exposed — is your asset inventory&#8217;s job (EASM). L.E.T. doesn&#8217;t scan your network; it tells you what the adversary is doing, so your exposure data plus our exploitation data equals a complete BOD 26-04 picture.</p>



<h2 class="wp-block-heading"><strong>Case in point: when CVSS says 5.3 and reality says 9/10</strong></h2>



<p class="wp-block-paragraph">CISA dropped CVSS as a mandatory prioritization input for good reason. Here&#8217;s a live example of why.</p>



<p class="wp-block-paragraph"><a href="https://tracker.crowdsec.net/cves/CVE-2025-13956?utm_source=website&amp;utm_medium=article&amp;utm_campaign=CISA" target="_blank" rel="noreferrer noopener"><strong>CVE-2025-13956</strong></a> is a missing-authorization flaw in LearnPress, a WordPress LMS plugin. CVSS score: <strong>5.3</strong>. Medium. Under severity-based triage, it sits in the backlog forever.</p>



<p class="wp-block-paragraph">Here&#8217;s what the CrowdSec Network actually observed: the CVE was published in March 2025 and stayed quiet for over a year. Then, in April 2026, we detected the first in-the-wild exploitation. By June 12, 2026, exploitation patterns shifted enough that our system reclassified it to <strong>targeted exploitation</strong> — 168 unique attacker IPs, momentum 4/5, targeting score 5/5, with 79% of attacking IPs profiled as pursuing system takeover. CrowdSec Score: <strong>9/10 — critical active campaign</strong>.</p>



<p class="wp-block-paragraph">A CVSS-driven program ignores this CVE. An exploitation-evidence-driven program — the kind BOD 26-04 now mandates — catches the phase transition the day it happens and re-prioritizes immediately. That&#8217;s the difference between a severity score assigned once at publication and a risk signal recomputed continuously from live attack data.</p>



<h2 class="wp-block-heading"><strong>The vendor view: see the campaign, not just the CVE</strong></h2>



<p class="wp-block-paragraph">Agencies don&#8217;t run CVEs; they run products. Federal networks are full of the exact appliances attackers systematically target—and if the last few years of emergency directives have taught us anything, it&#8217;s that edge devices from a handful of vendors pose disproportionate risk.</p>



<p class="wp-block-paragraph">L.E.T.&#8217;s vendor pages aggregate all observations across a vendor&#8217;s entire product line. Take our Ivanti page: 20 tracked CVEs, 3 active recon campaigns, and 12,800+ attacker IPs currently engaged — including 13,500 IPs probing Connect Secure panels <em>before any specific CVE is even in play</em>. That recon activity is your earliest warning signal: adversaries are mapping targets for the next vulnerability before it becomes publicly known.</p>



<p class="wp-block-paragraph">For a federal security team, this turns the directive&#8217;s per-CVE obligation into something manageable: one view of your actual vendor exposure, ranked by live adversary attention.</p>



<h2 class="wp-block-heading"><strong>Beyond prioritization: shrink the exposure window while you patch</strong></h2>



<p class="wp-block-paragraph">BOD 26-04 gives you three days for the worst cases. Patching an enterprise fleet in three days is hard. Patching it while the vulnerability is under active exploitation is harder.</p>



<p class="wp-block-paragraph">This is where L.E.T. goes further than any prioritization feed: every CVE and vendor page exposes <strong>dynamic, subscribable IP datasets</strong>. Point your firewall, WAF, CDN, or cloud security layer at the blocklist for a specific CVE — or an entire vendor — and the IPs actively hunting that vulnerability are preemptively blocked from ever reaching your workloads. The dataset updates as the campaign evolves; no manual list management.</p>



<p class="wp-block-paragraph">It&#8217;s not a substitute for patching — the directive is unambiguous about that, and so are we. It&#8217;s a compensating control that buys you the 72 hours you need and materially derisks the gap between &#8220;deadline starts&#8221; and &#8220;patch deployed.&#8221; Plug the holes in the hull while you&#8217;re still in the dry dock queue.</p>



<p class="wp-block-paragraph">And when the forensic triage requirement kicks in, the same data answers the other question the directive forces: pull the full attacker IP intelligence and IoC patterns for the CVE via API, run them against your logs, and you have an evidence-based answer to &#8220;were we already hit?&#8221; — within the window, not after a six-week IR engagement.</p>



<h2 class="wp-block-heading"><strong>Built for automation, because three days leave no room for manual work</strong></h2>



<p class="wp-block-paragraph">Everything in L.E.T. is API-first: CVE details, exploitation phase, scores, attacker IPs, IoCs. Feed it into your vulnerability management platform, your SOAR, your reporting pipeline. The directive expects agencies to automate KEV tracking and remediation reporting; our data is structured to drop straight into that machinery. A <code>curl</code> away:</p>



<p class="wp-block-paragraph"><code>curl -s -H "x-api-key: YOUR_API_KEY" \</code></p>



<p class="wp-block-paragraph"><code>  "https://admin.api.crowdsec.net/v1/cves/CVE-2025-13956"</code></p>



<h2 class="wp-block-heading"><strong>The bottom line</strong></h2>



<p class="wp-block-paragraph">BOD 26-04 codifies what defenders have known for years: patch what&#8217;s being exploited, defer what isn&#8217;t, and find out fast if you&#8217;ve already been breached. The directive supplies the framework. It assumes you have the evidence.</p>



<p class="wp-block-paragraph">We have the evidence — live, global, behavioral, per-CVE and per-vendor — and the protection layer to act on it before your patch lands.</p>



<p class="wp-block-paragraph"><strong>Explore Live Exploit Tracker now at </strong><a href="https://tracker.crowdsec.net/?utm_source=website&amp;utm_medium=article&amp;utm_campaign=CISA" target="_blank" rel="noreferrer noopener"><strong>tracker.crowdsec.net</strong></a> — browse CVEs, vendors, and live exploitation data freely. Federal agency or supporting a federal mission? <a href="https://www.crowdsec.net/contact-crowdsec" target="_blank" rel="noreferrer noopener">Talk to our team</a> about API access, blocklist integration, and operationalizing BOD 26-04.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
            <category>Proactive Cybersecurity</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/06/Blog-images-1575-%C3%97-871px-24.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[CrowdSec 1.7.8 Security Release: Fixes High-Severity WAF Bypass and LAPI DoS Vulnerabilities]]></title>
            <link>https://crowdsec.net/blog/crowdsec-1-7-8-security-release-fixes-high-severity-waf-bypass-and-lapi-dos-vulnerabilities</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/crowdsec-1-7-8-security-release-fixes-high-severity-waf-bypass-and-lapi-dos-vulnerabilities</guid>
            <pubDate>Wed, 27 May 2026 13:04:16 GMT</pubDate>
            <description><![CDATA[<p>CrowdSec 1.7.8 fixes two security vulnerabilities: CVE-2026-44982, a high-severity WAF bypass, and CVE-2026-44981, a Local API denial-of-service issue. Upgrade now.</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph">The 1.7.8 release of CrowdSec fixes two vulnerabilities: one of medium impact and one of high impact. We recommend that all users upgrade to the patched version as soon as possible.</p>



<p class="wp-block-paragraph">If you are using the nginx (or OpenResty) remediation component, you will also need to upgrade it to its latest version to fully address CVE-2026-44982.</p>



<h2 class="wp-block-heading">CVE-2026-44982: Partial CrowdSec WAF Bypass (high severity)</h2>



<p class="wp-block-paragraph">The AppSec datasource failed to read the request body for any request whose Content-Length was not positive.</p>



<p class="wp-block-paragraph">In practice, this means HTTP/1.1 requests using <code>Transfer-Encoding: chunked</code> and HTTP/2 requests sent without a <code>content-length</code> header were evaluated against an empty body, silently bypassing any WAF rule targeting body content.</p>



<p class="wp-block-paragraph">Headers-only and URI-only rules are not affected.</p>



<h2 class="wp-block-heading">CVE-2026-44981: Local API Denial of Service</h2>



<p class="wp-block-paragraph">LAPI did not enforce a maximum decompressed body size on incoming gzip-compressed requests.</p>



<p class="wp-block-paragraph">A small compressed payload could decompress into hundreds of megabytes of valid JSON, and the unauthenticated <code>/v1/watchers</code> and <code>/v1/watchers/login</code> endpoints made this reachable without credentials.</p>



<p class="wp-block-paragraph">Sending enough concurrent requests causes LAPI to exhaust memory and be killed by the OS.</p>



<p class="wp-block-paragraph">By default, LAPI only listens on localhost, so this is not remotely exploitable.</p>



<p class="wp-block-paragraph">In a distributed setup, any attacker who can reach LAPI can exploit it. Until you upgrade, restrict LAPI access to trusted IPs at the network or reverse proxy layer.</p>



<p class="wp-block-paragraph">The impact is limited to the availability of new decisions: bouncers continue to enforce existing decisions, but new alerts cannot be processed, and new decisions cannot be distributed while LAPI is down.</p>
]]></content:encoded>
            <category>Product Updates</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/05/Blog-images-1575-%C3%97-871px-22.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Edge is the new endpoint: How to respond when edge CVEs go hot]]></title>
            <link>https://crowdsec.net/blog/edge-is-the-new-endpoint-how-to-respond-when-edge-cves-go-hot</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/edge-is-the-new-endpoint-how-to-respond-when-edge-cves-go-hot</guid>
            <pubDate>Thu, 14 May 2026 11:14:12 GMT</pubDate>
            <description><![CDATA[<p>Edge CVEs move fast from disclosure to exploitation. Learn a practical 60-minute response framework to assess exposure, reduce risk, deploy mitigations, and communicate clearly during edge vulnerability incidents.</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong><em>Disclaimer</em></strong><em>: This series of articles was created in collaboration with CISOs and SOC Managers from our user community. They aim to describe situations and processes in place within their respective companies and inspire readers to examine their own.</em></p>



<p class="wp-block-paragraph">–</p>



<p class="wp-block-paragraph">Edge systems (VPN gateways, firewalls, WAFs, reverse proxies, load balancers, IAM appliances, and remote access portals) are inherently exposed. They sit in front of identity, internal apps, and admin surfaces. They are also difficult to manage, and most organizations have more of them than they realize. Subsidiaries have their own stacks. Teams spin up temporary machines in cloud accounts. Old stuff lingers because “we’ll clean that up later.”</p>



<p class="wp-block-paragraph">Attackers love “later”.</p>



<p class="wp-block-paragraph">Every time an edge <a href="https://tracker.crowdsec.net/" target="_blank" rel="noreferrer noopener">CVE goes public</a>, teams fall into the same loop. Read the advisory, argue about CVSS, wait for a catalog update, and then decide whether the change window is worth it. That is understandable. Emergency changes can break production. But it is also risky. By the time you have a consensus, exploitation may already be underway.</p>



<p class="wp-block-paragraph">The better approach is simple. Start with reality, then move to exposure, then mitigate, then patch. You can do this without drama if your workflow is clear.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading"><strong>Why do edge vulnerabilities become incidents?</strong></h2>



<p class="wp-block-paragraph">Edge issues quickly become major incidents because they create a direct path to valuable outcomes:</p>



<ul class="wp-block-list">
<li>Remote code execution on an internet-facing system often leads to internal access.</li>



<li>Auth bypass can turn into an admin takeover.</li>



<li>SSRF on a gateway can expose cloud metadata, internal APIs, or secrets.</li>



<li>Weaknesses in edge products are often chainable. One bug is enough to get a foothold, then another gets privilege.</li>
</ul>



<p class="wp-block-paragraph">There is also a timing problem. Attackers move faster than most change processes. Within hours of a PoC being public, you can expect scanning at scale. Within days, you can expect mass exploitation attempts that are more reliable and more automated. Sometimes it is even faster than that.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading"><strong>Scanning is constant, exploitation is specific</strong></h2>



<p class="wp-block-paragraph">Your logs will fill with noise after a CVE drops. That does not mean you are compromised. It means you are on a target list.</p>



<p class="wp-block-paragraph">The distinction matters because it drives decisions. If you treat every scan spike like an incident, you will burn out your team and break production more often than you should. If you dismiss scans as noise, you may miss the point at which scanning becomes a compromise.</p>



<p class="wp-block-paragraph">Scanning tends to look like:</p>



<ul class="wp-block-list">
<li>Quick version checks</li>



<li>Broad requests across many paths</li>



<li>High volume from distributed sources</li>



<li>Low “intent” in payloads</li>
</ul>



<p class="wp-block-paragraph">Exploitation tends to look like:</p>



<ul class="wp-block-list">
<li>Repeated requests to a small set of paths tied to vulnerability</li>



<li>Payloads that attempt code execution, file writes, or auth bypass</li>



<li>Follow-on behavior, like attempts to log in, create sessions, or pull configs</li>



<li>Patterns that align with known indicators for a given exploit family</li>
</ul>



<p class="wp-block-paragraph">For a SOC, your goal is not perfection. It is to be fast and correct enough to prioritize action.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading"><strong>Start with the question that cuts through the noise</strong></h2>



<p class="wp-block-paragraph">When an edge CVE hits, ask two questions in this order:</p>



<ol class="wp-block-list">
<li>Is exploitation observed in the wild?</li>



<li>Are we exposed?</li>
</ol>



<p class="wp-block-paragraph">If exploitation is observed and you are exposed, you act immediately. You reduce exposure and add friction, even before you patch, because you need to buy time.</p>



<p class="wp-block-paragraph">If exploitation is not observed, you still do the exposure work and patch planning, but you avoid panic changes.</p>



<p class="wp-block-paragraph"><strong><a href="https://tracker.crowdsec.net/" target="_blank" rel="noreferrer noopener">CrowdSec Live Exploit Tracker</a></strong> is mandatory here because it provides a reality check. It is not a replacement for advisories or catalog entries. It is a way to answer “Is this happening now?” without guessing.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading"><strong>The 60-minute response pattern</strong></h2>



<p class="wp-block-paragraph">Here is a practical way to run the first hour. This works for both CISO-level decision making and SOC-level execution.</p>



<h3 class="wp-block-heading"><strong>Minutes 0 to 10: confirm reality</strong></h3>



<ul class="wp-block-list">
<li>Confirm whether exploitation is observed in the wild.</li>



<li>If exploitation is observed, decide to mitigate now and patch as soon as feasible.</li>



<li>If exploitation is not observed, proceed with exposure assessment and prepare mitigations, but avoid disruptive changes until you have a reason.</li>
</ul>



<p class="wp-block-paragraph">This is where many teams lose time. They debate the severity instead of determining whether the attacker&#8217;s activity is real.</p>



<h3 class="wp-block-heading"><strong>Minutes 10 to 25: determine exposure</strong></h3>



<p class="wp-block-paragraph">Do not rely on one inventory source. Use multiple angles:</p>



<ul class="wp-block-list">
<li>public IP and DNS review</li>



<li>cloud security posture data</li>



<li>network scans of ingress points</li>



<li>configuration management records for edge products</li>
</ul>



<p class="wp-block-paragraph">Make sure you include:</p>



<ul class="wp-block-list">
<li>HA pairs and standby nodes</li>



<li>systems in regional sites</li>



<li>test systems that accidentally became public</li>



<li>legacy devices that were “temporary.”</li>
</ul>



<p class="wp-block-paragraph">Then answer:</p>



<ul class="wp-block-list">
<li>Is the vulnerable interface reachable from the internet?</li>



<li>Is it reachable from partner networks?</li>



<li>Is it reachable from user segments?</li>
</ul>



<p class="wp-block-paragraph">Exposure drives urgency. You can have a severe bug with low exposure. You can also have a moderate bug with high exposure that becomes your biggest risk.</p>



<h3 class="wp-block-heading"><strong>Minutes 25 to 45: reduce exposure and add friction</strong></h3>



<p class="wp-block-paragraph">If exploitation is observed and you have exposure, focus on making quick, safe changes that reduce risk.</p>



<p class="wp-block-paragraph">Start with exposure reduction:</p>



<ul class="wp-block-list">
<li>Restrict management interfaces to allowlists. Do it now.</li>



<li>Move admin access behind a VPN or a jump host.</li>



<li>Remove direct internet access where possible.</li>
</ul>



<p class="wp-block-paragraph">Then add friction:</p>



<ul class="wp-block-list">
<li>Apply WAF rules or reverse-proxy filtering if traffic passes through them.</li>



<li>Add IPS signatures or firewall blocklists where available.</li>



<li>Rate limit endpoints that attackers hammer during exploitation.</li>



<li>Tighten ACLs around the edge service and its management plane.</li>
</ul>



<p class="wp-block-paragraph">CrowdSec provides both <strong>WAF virtual patching rules</strong> and <strong>real-time-updated blocklists</strong> to address the latest vulnerabilities. These mitigation assets could be pulled automatically by your edge devices to respond instantly.</p>



<h3 class="wp-block-heading"><strong>Minutes 45 to 60: verify and watch</strong></h3>



<p class="wp-block-paragraph">Mitigation without verification is just hope.</p>



<ul class="wp-block-list">
<li>Confirm that logging is enabled and that it is shipped to your SIEM.</li>



<li>Create targeted detections for the known exploit paths and payload patterns.</li>



<li>Watch for post-exploitation indicators:
<ul class="wp-block-list">
<li>New admin sessions</li>



<li>New users or API tokens</li>



<li>Config changes</li>



<li>Unusual outbound connections from the appliance</li>



<li>Unexpected access to internal services</li>
</ul>
</li>
</ul>



<p class="wp-block-paragraph">This last step matters because some exploit chains succeed quickly and quietly. Your mitigations might be late for one asset but still protect the rest.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading"><strong>How to communicate without drama</strong></h2>



<p class="wp-block-paragraph">CISOs get pulled into edge events because the risk is easy to explain and the consequences are high.</p>



<p class="wp-block-paragraph">A good update to leadership avoids raw CVE lists and vague reassurance. It includes:</p>



<ul class="wp-block-list">
<li>Exploitation status: observed or not observed</li>



<li>Exposure status: which assets are internet-facing and vulnerable</li>



<li>Mitigations deployed: what changed in the last hour</li>



<li>Patch plan: when and how, including constraints</li>



<li>Evidence: what the SOC sees after mitigation</li>
</ul>



<p class="wp-block-paragraph">This keeps the conversation grounded. It also helps you defend emergency actions later if you need to explain why a change freeze was overridden.</p>



<p class="wp-block-paragraph"></p>



<h2 class="wp-block-heading"><strong>Key takeaways</strong></h2>



<ul class="wp-block-list">
<li>Edge systems are high value because they are exposed and privileged.</li>



<li>Scanning is expected. Exploitation is what changes your response.</li>



<li>Start with “is exploitation observed” and “are we exposed.”</li>



<li>Reduce exposure first, then add filtering, then patch.</li>



<li>Report progress on exposure and controls, not on CV</li>
</ul>
]]></content:encoded>
            <category>CrowdSec</category>
            <category>Proactive Cybersecurity</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/04/Blog-images-1575-%C3%97-871px-19.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[ButanGas Enhances Cybersecurity with CrowdSec to Protect LPG Distribution]]></title>
            <link>https://crowdsec.net/blog/securing-lpg-distribution-butangas-and-crowdsec</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/securing-lpg-distribution-butangas-and-crowdsec</guid>
            <pubDate>Tue, 07 Apr 2026 09:35:42 GMT</pubDate>
            <description><![CDATA[<p>ButanGas strengthens its IT security using CrowdSec’s real-time threat intelligence &#038; blocklists, ensuring secure, uninterrupted LPG distribution across Italy.</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>ButanGas, a key player in the Italian energy sector, strengthens its cybersecurity posture with CrowdSec’s real-time threat intelligence and Platinum Blocklists, ensuring safe and uninterrupted liquified petroleum gas (LPG) distribution across the country.</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><a href="https://butangas.it/en/" target="_blank" rel="noreferrer noopener">ButanGas</a> operates in the energy sector, specializing in liquified petroleum gas (LPG) storage and distribution. Its main mission is to guarantee a safe, reliable, and continuous supply throughout Italy. As part of the Dragan Group, a multinational energy organization active in seven countries, ButanGas maintains a strong national presence with more than <strong>20 offices and 9 LPG storage and bottling depots</strong>.</p>



<p class="wp-block-paragraph">Behind these operations lies a complex and extended digital infrastructure. Supporting over <strong>700 users across multiple countries</strong>, ButanGas <span style="box-sizing: border-box; margin: 0px; padding: 0px;">manages<strong> IT</strong></span><strong> and cybersecurity centrally for Italy and additional legal entities abroad</strong>. They require consistent protection, centralized visibility, and scalable security controls across geographies.</p>



<p class="wp-block-paragraph">At the helm of this strategy is the organization’s CIO and CISO, Riccardo Morandotti. He is responsible for IT strategy, cybersecurity governance, risk management, and digital transformation initiatives across the company.&nbsp;</p>



<h2 class="wp-block-heading"><strong>The challenge: Too much noise, not enough insight</strong></h2>



<p class="wp-block-paragraph">Like many <a href="https://www.crowdsec.net/solutions/oil-and-energy" target="_blank" rel="noreferrer noopener">companies in the energy sector</a>, ButanGas faced a constant stream of malicious activity targeting its network perimeter.</p>



<p class="wp-block-paragraph"><strong>Hundreds of suspicious connections were hitting their firewalls every day</strong>, generating significant noise but little actionable intelligence. While Sophos firewalls were already in place, the team lacked clear visibility into what was actually happening within their logs.</p>



<p class="wp-block-paragraph">They needed more than just protection; they needed clarity. The volume of alerts made it difficult to distinguish meaningful threats from <a href="https://www.crowdsec.net/glossary/internet-background-noise" target="_blank" rel="noreferrer noopener">background noise</a>, and the lack of enriched data limited their ability to take informed action. At the same time, ButanGas had a clear strategic priority: <strong>adopting European cybersecurity solutions without compromising on the quality of threat intelligence</strong>. </p>



<h2 class="wp-block-heading"><strong>Why CrowdSec: European roots, global intelligence</strong></h2>



<p class="wp-block-paragraph">After evaluating several threat intelligence providers, Riccardo identified CrowdSec as the right fit. He explains, “What stood out was that CrowdSec is a European-based company, which aligned perfectly with our strategic goal of using European security tools whenever possible. At the same time, CrowdSec collects data globally and provides insights that few other vendors can match.”&nbsp;&nbsp;</p>



<p class="wp-block-paragraph">Equally important was the ease of deployment. Within just one hour, CrowdSec was integrated into their existing environment, and results followed almost immediately. From day one, <strong>hundreds of malicious connections were blocked daily, with a threefold increase in detected suspicious traffic.</strong> At the same time, <strong>false positives remained extremely low, below 1%, ensuring legitimate traffic was never disrupted.</strong></p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><em>CrowdSec is simple to deploy, operate, and integrate, letting our team focus on response rather than tool management. Its reliable threat intelligence and accurate detections give us high‑confidence security decisions with minimal noise, making it a truly dependable part of our security architecture.</em></p>



<p class="wp-block-paragraph">&#8211; Riccardo Morandotti, CIO and CISO of ButanGas</p>
</blockquote>



<h2 class="wp-block-heading"><strong>A smarter, more proactive firewall</strong></h2>



<p class="wp-block-paragraph">ButanGas initially deployed CrowdSec on its most critical firewall, with plans to extend coverage to ten more. Even this first step delivered a significant impact.</p>



<p class="wp-block-paragraph">All malicious traffic is now blocked before reaching the network, drastically reducing exposure. <strong>Firewall logs have also become far more readable and actionable, with a clear distinction between legitimate and malicious traffic</strong>, eliminating the confusion that previously slowed down analysis.</p>



<p class="wp-block-paragraph">The real transformation, however, goes beyond inbound protection.</p>



<p class="wp-block-paragraph"><a href="https://doc.crowdsec.net/u/console/blocklists/integrations/firewall/" target="_blank" rel="noreferrer noopener">Through the integration between CrowdSec, Sophos Firewall, and their MDR service</a>, ButanGas now benefits from <strong>proactive outbound control</strong> as well. Internal endpoints are prevented from establishing connections with known malicious IPs, and any suspicious outbound attempt is immediately blocked. In more critical cases, <strong>compromised machines can be automatically isolated, helping contain threats before they spread or escalate</strong>.</p>



<p class="wp-block-paragraph">This capability has proven essential in limiting <a href="https://www.crowdsec.net/glossary/lateral-movement">lateral movement</a> and preventing potential data exfiltration, <strong>turning the firewall into an active enforcement point rather than a passive filter</strong>.</p>



<h2 class="wp-block-heading"><strong>From blocking threats to understanding them</strong></h2>



<p class="wp-block-paragraph">While automated blocking significantly improved their security posture, the most meaningful shift came from <strong>enhanced visibility</strong>.</p>



<p class="wp-block-paragraph">With <a href="https://www.crowdsec.net/cyber-threat-intelligence" target="_blank" rel="noreferrer noopener">CrowdSec’s IP enrichment capabilities</a>, ButanGas now gains contextual intelligence on every threat, <strong>ranging from geographic origin to behavior patterns and risk scoring</strong>. This allows their team to understand not just that an IP is malicious, but why.</p>



<p class="wp-block-paragraph">For a European organization, this is particularly valuable. They benefit from strong visibility into threats impacting European countries, while still leveraging <a href="https://www.crowdsec.net/our-data" target="_blank" rel="noreferrer noopener">globally collected intelligence</a> to stay ahead of emerging risks.</p>



<h2 class="wp-block-heading"><strong>Building a scalable security model</strong></h2>



<p class="wp-block-paragraph">With CrowdSec in place, ButanGas has transformed its firewall into a <strong>proactive, intelligence-driven security layer</strong>.</p>



<p class="wp-block-paragraph">Malicious traffic is stopped before it becomes a risk, visibility has improved dramatically, and the operational burden on the IT team has been reduced. All of this has been achieved without disrupting legitimate business activity.</p>



<p class="wp-block-paragraph">As deployments grow across more <a href="https://www.crowdsec.net/glossary/what-does-a-firewall-do" target="_blank" rel="noreferrer noopener">firewalls</a>, ButanGas is creating a scalable, unified security model. It protects a <strong>complex, multinational infrastructure while supporting the company’s mission to deliver safe, reliable energy</strong>.</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
            <category>Success Story</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/03/245.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Protecting Your Web Applications with OWASP CRS and CrowdSec]]></title>
            <link>https://crowdsec.net/blog/protecting-your-web-applications-with-owasp-crs-and-crowdsec</link>
            <guid isPermaLink="false">https://crowdsec.net/blog/protecting-your-web-applications-with-owasp-crs-and-crowdsec</guid>
            <pubDate>Mon, 23 Mar 2026 12:59:05 GMT</pubDate>
            <description><![CDATA[<p>Deploy the OWASP Core Rule Set (CRS) with CrowdSec to detect SQL injection, XSS, and other attack patterns. Learn how to install, tune, and enable blocking.</p>
]]></description>
            <content:encoded><![CDATA[
<p class="wp-block-paragraph">If you&#8217;re already running <a href="https://www.crowdsec.net/solutions/application-security" target="_blank" rel="noreferrer noopener">CrowdSec&#8217;s AppSec</a> component with virtual patching rules, you&#8217;re well protected against known CVEs. But what about the attacks that don&#8217;t target a specific vulnerability: <strong>the SQL injections, cross-site scripting attempts, and command injections that probe your applications every day?</strong></p>



<p class="wp-block-paragraph">That&#8217;s where the OWASP Core Rule Set (CRS) comes in. <a href="https://www.crowdsec.net/blog/crowdsec-waf-the-collaborative-future-of-web-application-security" target="_blank" rel="noreferrer noopener">CrowdSec supports CRS natively</a> through the <a href="https://github.com/corazawaf/coraza" target="_blank" rel="noreferrer noopener">Coraza</a> WAF engine, giving you broad attack pattern detection alongside your existing virtual patching setup.</p>



<p class="wp-block-paragraph">In this post, we&#8217;ll walk through what CRS offers, how to deploy it, and how to tune it for your applications.</p>



<h2 class="wp-block-heading">What is OWASP Core Rule Set (CRS)?</h2>



<p class="wp-block-paragraph">The <a href="https://coreruleset.org/" target="_blank" rel="noreferrer noopener">OWASP Core Rule Set</a> is among the most widely deployed sets of WAF rules worldwide. Actively maintained by the OWASP community, it provides generic attack detection against:</p>



<ul class="wp-block-list">
<li>SQL injection</li>



<li>Cross-site scripting</li>



<li>Command injection</li>



<li>Path traversal</li>



<li>And more</li>
</ul>



<p class="wp-block-paragraph">Unlike virtual patching, which targets specific known CVEs, the CRS uses a more generic approach to detect exploitation of still-unknown vulnerabilities.</p>



<p class="wp-block-paragraph">This comes at a cost: <strong>a higher chance of false positives depending on the application.</strong></p>



<p class="wp-block-paragraph">Using both CRS and virtual patching rules is complementary: virtual patching rules for precise targeting of known vulnerabilities without the need for any configuration, and CRS to catch everything else.</p>



<h2 class="wp-block-heading">Using the CRS with CrowdSec</h2>



<p class="wp-block-paragraph">CrowdSec offers two deployment modes for CRS: <strong>out-of-band or in-band.</strong></p>



<h3 class="wp-block-heading">Out-of-band (non-blocking)</h3>



<p class="wp-block-paragraph">In this mode, CRS rules analyze your traffic asynchronously. No request is ever blocked by a single rule match. Instead, events are generated and fed into <a href="https://app.crowdsec.net/hub/scenarios" target="_blank" rel="noreferrer noopener">CrowdSec&#8217;s scenario engine</a>. If an IP triggers too many different rule violations in a short timespan, <strong>it gets banned</strong>.</p>



<p class="wp-block-paragraph">This is the recommended starting point because it lets you:</p>



<ul class="wp-block-list">
<li>See what CRS detects in your traffic without disrupting legitimate users</li>



<li>Identify false positives and tune the CRS accordingly</li>
</ul>



<h3 class="wp-block-heading">In-band (blocking)</h3>



<p class="wp-block-paragraph">In blocking mode, requests that reach the CRS threshold for blocking are <strong>dropped immediately</strong>. Alerts are generated by default, <strong>giving you visibility into what is blocked</strong>.</p>



<h2 class="wp-block-heading">Getting Started: Installation</h2>



<h3 class="wp-block-heading">Prerequisites</h3>



<p class="wp-block-paragraph">You need a working CrowdSec setup with a WAF-capable remediation component. This includes the Nginx/OpenResty bouncer, Traefik bouncer, HAProxy bouncer, or any other bouncer (also known as <a href="https://doc.crowdsec.net/u/bouncers/intro" target="_blank" rel="noreferrer noopener">Remediation Component</a>) that supports the AppSec protocol.</p>



<h3 class="wp-block-heading">Step 1: Install the CRS collection</h3>



<p class="wp-block-paragraph">For non-blocking mode:</p>



<pre><code class="language-bash">cscli collections install crowdsecurity/appsec-crs</code></pre>



<p class="wp-block-paragraph">This installs:</p>



<ul class="wp-block-list">
<li><code>crowdsecurity/crs</code>: the configuration that loads CRS rules in out-of-band mode</li>



<li><code>crowdsecurity/crowdsec-appsec-outofband</code>: a scenario that bans IPs after 5+ out-of-band rule violations to offer a base level of protection, even if the rules themselves do not block anything directly.</li>
</ul>



<h3 class="wp-block-heading">Step 2: Configure the acquisition</h3>



<p class="wp-block-paragraph">Add the CRS config to your acquisition file (e.g., <code>/etc/crowdsec/acquis.d/waf.yaml</code>):</p>



<pre><code class="language-yaml">source: appsec
appsec-configs:
  - crowdsecurity/crs
labels:
  type: appsec</code></pre>



<p class="wp-block-paragraph">If you already have an acquisition file for AppSec with other configs (like virtual patching), simply add <code>crowdsecurity/crs</code> to the existing <code>appsec-configs</code> list.</p>



<h3 class="wp-block-heading">Step 3: Enable per-match alerting</h3>



<p class="wp-block-paragraph">By default, non-blocking mode doesn&#8217;t generate an alert for every individual rule match. If you want visibility into each match (useful during the tuning phase), create <code>/etc/crowdsec/appsec-configs/crs-alerting.yaml</code>:</p>



<pre><code class="language-yaml">name: custom/crs-alerting
on_match:
  - filter: IsOutBand == true
    apply:
      - SendAlert()
      - CancelEvent()
</code></pre>



<p class="wp-block-paragraph">The <code>CancelEvent()</code> directive is optional: if set, no event is generated for the scenario engine, meaning CrowdSec won&#8217;t take automated decisions based on these matches. This is useful if you want alerts for monitoring but don&#8217;t want any automated bans yet.</p>



<p class="wp-block-paragraph">Then add it to your acquisition:</p>



<pre><code class="language-yaml">source: appsec
appsec-configs:
  - crowdsecurity/crs
  - custom/crs-alerting
labels:
  type: appsec
</code></pre>



<p class="wp-block-paragraph">Even though an alert is generated, because the rules are loaded out-of-band, nothing will be blocked, but you will still know exactly what is matching.</p>



<h3 class="wp-block-heading">Step 4: Restart CrowdSec</h3>



<pre><code class="language-bash">sudo systemctl restart crowdsec</code></pre>



<h3 class="wp-block-heading">Step 5: Confirm it&#8217;s working</h3>



<p class="wp-block-paragraph">Now, we can see if everything is working as it should. Let&#8217;s exploit a (fake) SQL injection:</p>



<pre><code class="language-bash">$ curl "localhost/?a='+OR+'1'='1" -i
HTTP/1.1 200 OK
...
</code></pre>



<p class="wp-block-paragraph">Our request was not blocked: it&#8217;s expected as the CRS is configured in non-blocking mode.</p>



<p class="wp-block-paragraph">If we look at the list of alerts in crowdsec with <code>cscli alerts list</code> and <code>cscli alerts inspectd -d</code>, We can indeed see the request was detected by the CRS:</p>



<pre><code class="language-bash">root@instance-20240401-2335:~# cscli alerts inspect -d 105901

################################################################################################

 - ID           : 105901
 - Date         : 2026-03-12T09:44:32Z
 - Machine      : 717704f5186c4314928c2060bf5169f32E1tgFLtep049JcD
 - Simulation   : false
 - Remediation  : false
 - Reason       : anomaly score out-of-band: sql_injection: 10, anomaly: 10,
 - Events Count : 4
 - Scope:Value  : Ip:127.0.0.1
 - Country      :
 - AS           :
 - Begin        : 2026-03-12T09:44:32Z
 - End          : 2026-03-12T09:44:32Z
 - UUID         : 8cf71673-81d7-4ba0-b211-0d415a0f51fd


 - Context  :
╭───────────────┬─────────────────────────────────────────────────────╮
│      Key      │                        Value                        │
├───────────────┼─────────────────────────────────────────────────────┤
│ ja4h          │ ge11nn020000_cf69e1861692_000000000000_000000000000 │
│ matched_zones │ ARGS.a                                              │
│ method        │ GET                                                 │
│ msg           │ SQL Injection Attack Detected via libinjection      │
│ name          │ native_rule:942100                                  │
│ target_uri    │ /?a='+OR+'1'='1                                     │
│ user_agent    │ curl/7.81.0                                         │
╰───────────────┴─────────────────────────────────────────────────────╯

 - Events  :

- Date: 2026-03-12 09:44:32 +0000 UTC
╭───────────────┬──────────────────────────╮
│      Key      │           Value          │
├───────────────┼──────────────────────────┤
│ data          │                          │
│ matched_zones │ REQBODY_PROCESSOR        │
│ message       │ Enabling body inspection │
│ rule_name     │ native_rule:901340       │
│ target_fqdn   │ localhost                │
│ uri           │ /?a='+OR+'1'='1          │
╰───────────────┴──────────────────────────╯

- Date: 2026-03-12 09:44:32 +0000 UTC
╭───────────────┬──────────────────────────────────────────────────────╮
│      Key      │                         Value                        │
├───────────────┼──────────────────────────────────────────────────────┤
│ data          │ Matched Data: s&sos found within ARGS:a: ' OR '1'='1 │
│ matched_zones │ ARGS.a,ARGS.a                                        │
│ message       │ SQL Injection Attack Detected via libinjection       │
│ rule_name     │ native_rule:942100                                   │
│ target_fqdn   │ localhost                                            │
│ uri           │ /?a='+OR+'1'='1                                      │
╰───────────────┴──────────────────────────────────────────────────────╯

- Date: 2026-03-12 09:44:32 +0000 UTC
╭───────────────┬──────────────────────────────────────────────────╮
│      Key      │                       Value                      │
├───────────────┼──────────────────────────────────────────────────┤
│ data          │                                                  │
│ matched_zones │ TX.blocking_inbound_anomaly_score                │
│ message       │ Inbound Anomaly Score Exceeded (Total Score: 10) │
│ rule_name     │ native_rule:949110                               │
│ target_fqdn   │ localhost                                        │
│ uri           │ /?a='+OR+'1'='1                                  │
╰───────────────┴──────────────────────────────────────────────────╯

- Date: 2026-03-12 09:44:32 +0000 UTC
╭───────────────┬──────────────────────────────────────────────────────────────╮
│      Key      │                             Value                            │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ data          │                                                              │
│ matched_zones │ UNKNOWN                                                      │
│ message       │ Anomaly Scores: (Inbound Scores: blocking=10, detection=10,  │
│               │ per_pl=10-0-0-0, threshold=5) - (Outbound Scores:            │
│               │ blocking=0, detection=0, per_pl=0-0-0-0, threshold=4) -      │
│               │ (SQLI=10, XSS=0, RFI=0, LFI=0, RCE=0, PHPI=0, HTTP=0,        │
│               │ SESS=0, COMBINED_SCORE=10)                                   │
│ rule_name     │ native_rule:980170                                           │
│ target_fqdn   │ localhost                                                    │
│ uri           │ /?a='+OR+'1'='1                                              │
╰───────────────┴──────────────────────────────────────────────────────────────╯
</code></pre>



<p class="wp-block-paragraph">A note about the reason given for the block: CRS works with a threshold system. Each rule that matches on the request will increase by some value a global score, and if the global score exceeds a preconfigured threshold (5 by default), the request will be blocked.</p>



<p class="wp-block-paragraph">This is why the reason appears as <code>anomaly score out-of-band: sql_injection: 10, anomaly: 10,.</code></p>



<p class="wp-block-paragraph">Crowdsec automatically extracts the scoring information from the CRS to give you, at first glance, details about what was wrong with the request.</p>



<h3 class="wp-block-heading">Switching to blocking mode</h3>



<p class="wp-block-paragraph">Once you&#8217;re confident in your CRS tuning, switch to blocking mode:</p>



<pre><code class="language-bash">cscli collections install crowdsecurity/appsec-crs-inband</code></pre>



<p class="wp-block-paragraph">Update your acquisition:</p>



<pre><code class="language-yaml">source: appsec
appsec-configs:
  - crowdsecurity/crs-inband
labels:
  type: appsec
</code></pre>



<p class="wp-block-paragraph">In this mode, any request that reaches the default CRS threshold is dropped, and alerts are generated automatically.</p>



<p class="wp-block-paragraph">Let&#8217;s retry the same request as previously:</p>



<pre><code class="language-bash">$ curl "localhost/?a='+OR+'1'='1" -i
HTTP/1.1 403 Forbidden
...
</code></pre>



<p class="wp-block-paragraph">This time, the request actually got blocked!</p>



<p class="wp-block-paragraph">Again, we can inspect the alert:</p>



<pre><code class="language-bash">root@instance-20240401-2335:~# cscli alerts inspect -d 105902

################################################################################################

 - ID           : 105902
 - Date         : 2026-03-12T09:49:04Z
 - Machine      : 717704f5186c4314928c2060bf5169f32E1tgFLtep049JcD
 - Simulation   : false
 - Remediation  : false
 - Reason       : anomaly score block: sql_injection: 10, anomaly: 10,
 - Events Count : 4
 - Scope:Value  : Ip:127.0.0.1
 - Country      :
 - AS           :
 - Begin        : 2026-03-12T09:49:03Z
 - End          : 2026-03-12T09:49:03Z
 - UUID         : 7ef38529-5e7e-4d2e-9e21-24ec7a89a1ee


 - Context  :
╭───────────────┬─────────────────────────────────────────────────────╮
│      Key      │                        Value                        │
├───────────────┼─────────────────────────────────────────────────────┤
│ ja4h          │ ge11nn020000_cf69e1861692_000000000000_000000000000 │
│ matched_zones │ ARGS.a                                              │
│ method        │ GET                                                 │
│ msg           │ SQL Injection Attack Detected via libinjection      │
│ name          │ native_rule:942100                                  │
│ target_uri    │ /?a='+OR+'1'='1                                     │
│ user_agent    │ curl/7.81.0                                         │
╰───────────────┴─────────────────────────────────────────────────────╯

 - Events  :

- Date: 2026-03-12 09:49:03 +0000 UTC
╭───────────────┬──────────────────────────╮
│      Key      │           Value          │
├───────────────┼──────────────────────────┤
│ data          │                          │
│ matched_zones │ REQBODY_PROCESSOR        │
│ message       │ Enabling body inspection │
│ rule_name     │ native_rule:901340       │
│ target_fqdn   │ localhost                │
│ uri           │ /?a='+OR+'1'='1          │
╰───────────────┴──────────────────────────╯

- Date: 2026-03-12 09:49:03 +0000 UTC
╭───────────────┬──────────────────────────────────────────────────────╮
│      Key      │                         Value                        │
├───────────────┼──────────────────────────────────────────────────────┤
│ data          │ Matched Data: s&sos found within ARGS:a: ' OR '1'='1 │
│ matched_zones │ ARGS.a,ARGS.a                                        │
│ message       │ SQL Injection Attack Detected via libinjection       │
│ rule_name     │ native_rule:942100                                   │
│ target_fqdn   │ localhost                                            │
│ uri           │ /?a='+OR+'1'='1                                      │
╰───────────────┴──────────────────────────────────────────────────────╯

- Date: 2026-03-12 09:49:03 +0000 UTC
╭───────────────┬──────────────────────────────────────────────────╮
│      Key      │                       Value                      │
├───────────────┼──────────────────────────────────────────────────┤
│ data          │                                                  │
│ matched_zones │ TX.blocking_inbound_anomaly_score                │
│ message       │ Inbound Anomaly Score Exceeded (Total Score: 10) │
│ rule_name     │ native_rule:949110                               │
│ target_fqdn   │ localhost                                        │
│ uri           │ /?a='+OR+'1'='1                                  │
╰───────────────┴──────────────────────────────────────────────────╯

- Date: 2026-03-12 09:49:03 +0000 UTC
╭───────────────┬──────────────────────────────────────────────────────────────╮
│      Key      │                             Value                            │
├───────────────┼──────────────────────────────────────────────────────────────┤
│ data          │                                                              │
│ matched_zones │ UNKNOWN                                                      │
│ message       │ Anomaly Scores: (Inbound Scores: blocking=10, detection=10,  │
│               │ per_pl=10-0-0-0, threshold=5) - (Outbound Scores:            │
│               │ blocking=0, detection=0, per_pl=0-0-0-0, threshold=4) -      │
│               │ (SQLI=10, XSS=0, RFI=0, LFI=0, RCE=0, PHPI=0, HTTP=0,        │
│               │ SESS=0, COMBINED_SCORE=10)                                   │
│ rule_name     │ native_rule:980170                                           │
│ target_fqdn   │ localhost                                                    │
│ uri           │ /?a='+OR+'1'='1                                              │
╰───────────────┴──────────────────────────────────────────────────────────────╯
</code></pre>



<p class="wp-block-paragraph">It&#8217;s almost the same, except for the reason: <code>anomaly score block: sql_injection: 10, anomaly: 10,.</code></p>



<p class="wp-block-paragraph">The presence of <code>block</code> means the request was actually blocked and never reached the target application.</p>



<h2 class="wp-block-heading">Customizing CRS for Your Applications</h2>



<p class="wp-block-paragraph">Out of the box, CRS is tuned for broad coverage. For any moderately complex application, you&#8217;ll likely need some customization to eliminate false positives. <a href="https://www.crowdsec.net/blog/crowdsec-waf-in-action-real-world-use-cases" target="_blank" rel="noreferrer noopener">CrowdSec uses the CRS plugin system for all customizations</a>, keeping your changes separate from the CRS rules themselves.</p>



<h3 class="wp-block-heading">CRS Plugins for Popular Applications</h3>



<p class="wp-block-paragraph">The CRS community maintains exclusion plugins for popular web applications. These plugins contain pre-built rule exclusions that prevent common false positives for each application.</p>



<p class="wp-block-paragraph">The following plugins are available directly from the <a href="https://app.crowdsec.net/hub" target="_blank" rel="noreferrer noopener">CrowdSec Hub</a>:</p>



<ul class="wp-block-list">
<li>WordPress</li>



<li>NextCloud</li>



<li>PHPMyAdmin</li>



<li>CPanel</li>



<li>Drupal</li>



<li>PHPBB</li>



<li>DokuWiki</li>



<li>Xenforo</li>
</ul>



<h3 class="wp-block-heading">Installing a plugin</h3>



<pre><code class="language-bash">cscli collections install crowdsecurity/appsec-crs-exclusion-plugin-<application-name>
</code></pre>



<p class="wp-block-paragraph">For example, to install the WordPress plugin:</p>



<pre><code class="language-yaml">cscli collections install crowdsecurity/appsec-crs-exclusion-plugin-wordpress</code></pre>



<p class="wp-block-paragraph">Once installed, the plugin loads automatically after a CrowdSec restart.</p>



<p class="wp-block-paragraph">The plugins are updated automatically, like any other Hub items, so you will receive upstream changes automatically.</p>



<p class="wp-block-paragraph">You can also manually deploy custom plugins; refer to our <a href="https://docs.crowdsec.net/docs/next/appsec/crs/plugin_support#manually-installing-a-plugin" target="_blank" rel="noreferrer noopener">documentation</a>.</p>



<h2 class="wp-block-heading">Putting It All Together: Defense in Depth</h2>



<p class="wp-block-paragraph">Here&#8217;s how a complete AppSec configuration might look, combining virtual patching and CRS:</p>



<pre><code class="language-yaml"># /etc/crowdsec/acquis.d/waf.yaml
source: appsec
appsec-configs:
  - crowdsecurity/appsec-default # Virtual patching (in-band)
  - crowdsecurity/crs # OWASP CRS (out-of-band)
labels:
  type: appsec
</code></pre>



<p class="wp-block-paragraph">With this setup:</p>



<ol class="wp-block-list">
<li><strong>Virtual patching rules</strong> run in-band, immediately blocking requests that match known CVE patterns</li>



<li><strong>CRS rules</strong> run out-of-band, detecting broad attack patterns and feeding events into CrowdSec&#8217;s scenario engine</li>



<li><strong>IPs that trigger multiple CRS violations</strong> get banned automatically through the scenario system</li>



<li><strong>CrowdSec&#8217;s threat intelligence</strong> adds another layer, sharing decisions across the community</li>
</ol>



<p class="wp-block-paragraph">Once you&#8217;ve tuned CRS and are confident in your configuration, you can switch to the in-band CRS collection for immediate blocking with the following configuration:</p>



<pre><code class="language-yaml"># /etc/crowdsec/acquis.d/waf.yaml
source: appsec
appsec-configs:
  - crowdsecurity/appsec-default # Virtual patching (in-band)
  - crowdsecurity/crs-inband # OWASP CRS (in-band)
labels:
  type: appsec
</code></pre>



<h2 class="wp-block-heading">Next Steps</h2>



<p class="wp-block-paragraph">Now that you have deployed CRS with CrowdSec, here are some more resources and tools you can use to better protect your web applications:&nbsp;&nbsp;</p>



<ul class="wp-block-list">
<li>Browse the <a href="https://docs.crowdsec.net/docs/appsec/crs/intro" target="_blank" rel="noreferrer noopener">CRS documentation</a> for detailed reference</li>



<li>Search the <a href="https://app.crowdsec.net/hub" target="_blank" rel="noreferrer noopener">CrowdSec Hub</a> for available CRS plugins</li>



<li>Check out the <a href="https://docs.crowdsec.net/docs/appsec/intro" target="_blank" rel="noreferrer noopener">CrowdSec WAF quickstart guides</a> if you haven&#8217;t set up the AppSec component yet</li>



<li>Join the <a href="https://discourse.crowdsec.net/" target="_blank" rel="noreferrer noopener">CrowdSec community</a> to share your CRS tuning experiences</li>
</ul>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
            <category>Tutorial</category>
            <enclosure url="https://cms.crowdsec.net/wp-content/uploads/2026/03/242-1.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>