<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>Crowdsec - Tag - arleo.eu</title><link>https://www.arleo.eu/en/tags/crowdsec/</link><description>Crowdsec - Tag - arleo.eu</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 25 May 2026 20:51:17 +0200</lastBuildDate><atom:link href="https://www.arleo.eu/en/tags/crowdsec/" rel="self" type="application/rss+xml"/><item><title>Postmortem — CrowdSec AppSec: Heuristic False Positive on Sonarr/Radarr</title><link>https://www.arleo.eu/en/posts/postmortem-crowdsec-appsec-false-positive-sonarr/</link><pubDate>Mon, 25 May 2026 20:51:17 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/postmortem-crowdsec-appsec-false-positive-sonarr/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/postmortem-crowdsec-appsec-false-positive-sonarr-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="summary">Summary</h2>
<p>On <strong>May 25, 2026 at around 10:02 PM (local time)</strong>, Sonarr and Radarr became completely inaccessible from the home IP (<code>82.XX.XX.XX</code>), returning <strong>403</strong> on every URL including <code>/login</code>. The service was fully operational. Initial suspicion fell on the day&rsquo;s <code>crowdsec-cf-sync</code> refactor deployment — the real cause was a <strong>CrowdSec AppSec heuristic false positive</strong>.</p>
<hr>
<h2 id="timeline">Timeline</h2>
<table>
  <thead>
      <tr>
          <th>Time (local)</th>
          <th>Event</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>~10:00 PM</td>
          <td>Sonarr browser session cookie expired</td>
      </tr>
      <tr>
          <td>10:02:31 PM</td>
          <td>Browser loads Sonarr library → attempts to fetch 20+ <code>/MediaCover/*.jpg</code> simultaneously</td>
      </tr>
      <tr>
          <td>10:02:31 PM</td>
          <td>Sonarr returns 302 → <code>/login</code> for each image (invalid session)</td>
      </tr>
      <tr>
          <td>10:02:34 PM</td>
          <td>SignalR WebSocket connection succeeds (101) via <code>access_token</code> in URL</td>
      </tr>
      <tr>
          <td>~10:05 PM</td>
          <td>CrowdSec AppSec triggers heuristic rule <code>http-probing</code>: burst of failed requests from same IP</td>
      </tr>
      <tr>
          <td>10:11:37 PM</td>
          <td>All requests from <code>82.XX.XX.XX</code> return 403 — <code>cs_reason=heuristic</code> in nginx logs</td>
      </tr>
      <tr>
          <td>10:13:34 PM</td>
          <td>Even <code>/login</code> is blocked — IP cannot authenticate</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="root-cause">Root Cause</h2>
<p>CrowdSec AppSec maintains an <strong>in-memory heuristic state</strong>, separate from LAPI decisions. When the browser simultaneously tries to load many resources and receives 302/403 from the upstream application (Sonarr), AppSec interprets the burst of failures as aggressive probing (<code>http-probing</code>) and blocks the source IP.</p>]]></description></item><item><title>CrowdSec AppSec + OpenResty: Modern WAF Without ModSecurity</title><link>https://www.arleo.eu/en/posts/crowdsec-appsec-openresty/</link><pubDate>Mon, 18 May 2026 00:07:55 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/crowdsec-appsec-openresty/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/crowdsec-appsec-openresty-featured.jpg" referrerpolicy="no-referrer">
            </div><p>After years running ModSecurity + OWASP CRS on nginx, I migrated arleo.eu to a more modern stack: <strong>CrowdSec AppSec on OpenResty</strong>. The result is a tighter inline WAF architecture — better integrated, easier to maintain, and fully coherent with the rest of the security stack.</p>
<h2 id="why-drop-modsecurity">Why Drop ModSecurity?</h2>
<p>ModSecurity v2 is in maintenance mode. Managing OWASP CRS rules on classic nginx generates friction: frequent false positives, logs that are hard to correlate with CrowdSec, and a configuration spread across multiple tools with no unified view.</p>]]></description></item><item><title>NUC Security Audit: ModSecurity Removed, 500 MB Recovered</title><link>https://www.arleo.eu/en/posts/audit-securite-modsecurity-crowdsec/</link><pubDate>Thu, 14 May 2026 05:32:19 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/audit-securite-modsecurity-crowdsec/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/audit-securite-modsecurity-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="-tldr">⚡ TL;DR</h2>
<p>A security stack audit on the homelab NUC reveals <strong>redundant double WAF inspection</strong>: ModSecurity + OWASP CRS load 11,872 rules into memory despite <code>SecRuleEngine Off</code>, running in parallel with CrowdSec AppSec which already covers the same surface. After removing the ModSecurity nginx module and five other targeted fixes, nginx drops from <strong>~520 MB to ~27 MB PSS</strong>. Same security, memory footprint divided by 20.</p>
<hr>
<h2 id="-architecture-before-the-audit">🏗️ Architecture Before the Audit</h2>
<p>The security stack had six stacked layers:</p>]]></description></item><item><title>CrowdSec Log Pipeline with Vector: Filtering Noise and Capturing Real Bans</title><link>https://www.arleo.eu/en/posts/crowdsec-vector-pipeline/</link><pubDate>Wed, 15 Apr 2026 19:02:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/crowdsec-vector-pipeline/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/crowdsec-vector-pipeline-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="-in-short">⚡ In short</h2>
<p>The initial Vector pipeline was flooding BetterStack with ~500 events/24h, of which 434 were CAPI pulls with no local monitoring value. This work reconfigures the Vector filter to keep only high-value bans (<code>cscli</code>) and fixes a major blind spot: actual nginx-lua bouncer bans were not appearing anywhere in BetterStack.</p>
<h2 id="-why">🧠 Why</h2>
<p>This homelab&rsquo;s security stack relies on three components working together:</p>
<ul>
<li><strong>nginx</strong> with the CrowdSec lua bouncer (<code>lua-resty-crowdsec</code>) for real-time request blocking</li>
<li><strong>CrowdSec</strong> for threat detection and ban decision management</li>
<li><strong>Vector</strong> centralizing logs to BetterStack for monitoring</li>
</ul>
<p>After setting up the initial pipeline, two problems quickly became apparent. First, the signal was drowned in noise: out of 500 events/24h, 434 came from the hourly community CAPI pull and 66 from third-party lists — neither represents a threat detected on <em>this</em> infrastructure. Second, actual lua bouncer bans (real-time blocks in nginx) were not appearing anywhere in BetterStack, creating a blind spot on real security activity.</p>]]></description></item><item><title>Infrastructure Documentation</title><link>https://www.arleo.eu/en/documentation/</link><pubDate>Sun, 12 Apr 2026 21:25:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/documentation/</guid><description><![CDATA[<h2 id="1-security-architecture-10-layers">1. Security Architecture (10 layers)</h2>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>Layer</th>
          <th>Technology</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>DNS</td>
          <td>DNSSEC ECDSA P256-SHA256</td>
      </tr>
      <tr>
          <td>2</td>
          <td>Cloud CDN + WAF</td>
          <td>Cloudflare WAF + DDoS + AI Crawl Control</td>
      </tr>
      <tr>
          <td>3</td>
          <td>Network</td>
          <td>CrowdSec nftables Bouncer</td>
      </tr>
      <tr>
          <td>4</td>
          <td>Firewall</td>
          <td>Netgear PR60X SPI</td>
      </tr>
      <tr>
          <td>5</td>
          <td>Local WAF</td>
          <td>ModSecurity + OWASP CRS 4.x</td>
      </tr>
      <tr>
          <td>6</td>
          <td>IDS/IPS</td>
          <td>CrowdSec Agent + SSH/HTTP scenarios</td>
      </tr>
      <tr>
          <td>7</td>
          <td>HTTPS</td>
          <td>TLS 1.3 + HSTS preload</td>
      </tr>
      <tr>
          <td>8</td>
          <td>DNS-TLS</td>
          <td>DoH port 853</td>
      </tr>
      <tr>
          <td>9</td>
          <td>Application</td>
          <td>Grav CMS + CSP + Secure cookies</td>
      </tr>
      <tr>
          <td>10</td>
          <td>Monitoring</td>
          <td>BetterStack + CrowdSec poller + Vector</td>
      </tr>
  </tbody>
</table>
<hr>
<h2 id="2-crowdsec--cloudflare-sync-crowdsec-cf-syncpy">2. CrowdSec → Cloudflare Sync (<code>crowdsec-cf-sync.py</code>)</h2>
<p><strong>Location:</strong> <code>/usr/local/bin/crowdsec-cf-sync.py</code>
<strong>Service:</strong> <code>crowdsec-cf-sync.service</code>
<strong>Logs:</strong> <code>/var/log/crowdsec/cf-sync.log</code></p>]]></description></item><item><title>Infrastructure Scripts</title><link>https://www.arleo.eu/en/scripts/</link><pubDate>Sun, 12 Apr 2026 21:25:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/scripts/</guid><description><![CDATA[<h2 id="1-crowdsec-cf-syncpy">1. crowdsec-cf-sync.py</h2>
<p><strong>Location:</strong> <code>/usr/local/bin/crowdsec-cf-sync.py</code>
<strong>Systemd service:</strong> <code>crowdsec-cf-sync.service</code>
<strong>Logs:</strong> <code>/var/log/crowdsec/cf-sync.log</code></p>
<h3 id="installation">Installation</h3>
<div class="code-block code-line-numbers open" data-start="0">
    <div class="code-header language-bash">
        <span class="code-title"><i class="arrow fas fa-angle-right" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cp crowdsec-cf-sync.py /usr/local/bin/
</span></span><span class="line"><span class="cl">chmod +x /usr/local/bin/crowdsec-cf-sync.py
</span></span><span class="line"><span class="cl">systemctl <span class="nb">enable</span> crowdsec-cf-sync
</span></span><span class="line"><span class="cl">systemctl start crowdsec-cf-sync</span></span></code></pre></div></div>
<h3 id="features">Features</h3>
<ul>
<li>Syncs active CrowdSec bans → Cloudflare IP Access Rules (tag <code>crowdsec-local-ban</code>)</li>
<li>Reports banned IPs → AbuseIPDB (48h window, deduplicated)</li>
<li><strong>Repeat-offender escalation</strong>: 1st CrowdSec ban handles | 2nd → 24h | 3rd+ → 7d (7d window)</li>
<li><strong>ModSecurity</strong> score ≥ 5 → immediate 2h CF ban (tag <code>modsec-ban</code>) + AbuseIPDB report</li>
<li><strong>Automatic /24 ban</strong>: 2+ IPs from same block in 7d → CF + CrowdSec ban 24h (tag <code>crowdsec-cidr-ban</code>)</li>
</ul>
<h3 id="bug-fixes-april-2026">Bug fixes (April 2026)</h3>
<ul>
<li><code>cs_origin</code> vs <code>origin</code> in <code>get_recent_local_bans()</code> — the JSON field is <code>cs_origin</code></li>
<li>REST API pagination <code>/v1/decisions?limit=1000</code> misses <code>cscli</code> bans → replaced with <code>cscli decisions list --origin</code></li>
<li><code>Items</code> → <code>items</code> (lowercase) in CrowdSec allowlist JSON parsing</li>
</ul>
<blockquote>
<p>⚠️ <strong>Important</strong>: real tokens (<code>CF_API_TOKEN</code>, <code>CF_ZONE_ID</code>, <code>CS_API_KEY</code>, <code>ABUSEIPDB_KEY</code>) must be stored in <code>/etc/secrets/</code> with <code>chmod 600</code> and loaded via environment variables, not hardcoded in the script. The <code>VOTRE_*</code> values below are placeholders.</p>]]></description></item><item><title>Normalizing nginx and CrowdSec Logs in BetterStack with Vector</title><link>https://www.arleo.eu/en/posts/vector-logs-harmonisation-betterstack/</link><pubDate>Fri, 10 Apr 2026 21:04:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/vector-logs-harmonisation-betterstack/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/vector-logs-harmonisation-betterstack-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="-in-short">⚡ In short</h2>
<p>Two problems coexisted in BetterStack: <code>mcp-oauth.access.log</code> logs arrived as unreadable raw JSON, and CrowdSec logs produced visual duplicates. This work normalizes all logs so they display as structured clickable tags, with correct timestamps and without parasitic fields.</p>
<h2 id="-why">🧠 Why</h2>
<p>BetterStack displays logs as highlighted clickable tags in the Live Tail when JSON fields are properly structured. Before this work, observation was degraded on two fronts:</p>
<ul>
<li><code>mcp-oauth.access.log</code> logs arrived as unreadable raw JSON (custom format incompatible with Vector&rsquo;s nginx parser) — fields <code>nginx.client</code>, <code>nginx.path</code>, <code>nginx.status</code> were not extracted</li>
<li>CrowdSec and CF WAF logs arrived as plain text with duplicates (<code>Ban ban | ... | Ban ban</code>)</li>
</ul>
<p>The goal was to normalize all logs in BetterStack to display like standard nginx logs:</p>]]></description></item><item><title>Automating IP Bans with Cloudflare WAF, CrowdSec and AbuseIPDB</title><link>https://www.arleo.eu/en/posts/crowdsec-cloudflare-waf-autoban/</link><pubDate>Fri, 10 Apr 2026 00:23:00 +0200</pubDate><author>Jmr</author><guid>https://www.arleo.eu/en/posts/crowdsec-cloudflare-waf-autoban/</guid><description><![CDATA[<div class="featured-image">
                <img src="/images/crowdsec-cloudflare-waf-autoban-featured.jpg" referrerpolicy="no-referrer">
            </div><h2 id="-in-short">⚡ In short</h2>
<p>Passive monitoring is not enough. This pipeline automates <strong>closing the loop in under 5 minutes</strong> between an attack detected by Cloudflare WAF and the effective ban of the IP in CrowdSec, its synchronization to Cloudflare, and its report to AbuseIPDB. A Python script polls the Cloudflare GraphQL API every 5 minutes, applies a 3-hit threshold, and triggers the ban with recidivist escalation.</p>
<h2 id="-why">🧠 Why</h2>
<p>Seeing an attack in BetterStack logs after the fact does not stop the malicious IP from continuing to hammer the server. Without automation, the detection → ban loop takes hours or never closes. Cloudflare WAF actions (<code>block</code>, <code>challenge</code>, <code>managed_challenge</code>, <code>jschallenge</code>) are clear attack signals, but they remain confined to the Cloudflare dashboard — without a bridge to CrowdSec, no IP is banned locally, none is reported to the AbuseIPDB community.</p>]]></description></item></channel></rss>