Mahbubur Riad
Back to blog
Hosting & Server 6 min read

Zero‑False‑Positive Intrusion Prevention on a Linux VPS: Configuring CrowdSec, Fail2Ban, and OpenResty WAF

Jun 17, 2026 · Mahbubur Riad

Learn how to combine CrowdSec, Fail2Ban, and OpenResty’s built‑in WAF on a low‑cost Linux VPS for accurate automated bans and hardened web traffic filtering.

On this page

Introduction

Running a cheap VPS for a personal project, a small SaaS, or a client demo is great—until the first brute‑force attack hits. The problem isn’t just the noise; it’s the risk of false positives that lock out legitimate users and waste admin time.

In this guide I’ll show you how to build a zero‑false‑positive intrusion prevention pipeline on a single‑CPU Linux VPS using three open‑source tools that play nicely together:

Tool What it does Why we need it
CrowdSec Community‑driven log parser & threat intelligence engine Detects attacks with low false‑positive rates thanks to crowd‑sourced scenarios
Fail2Ban Traditional IPTables/Firewalld bans based on regex Provides the actual packet‑filtering layer that CrowdSec can feed into
OpenResty (ngx_lua WAF) Embedded Lua‑based web application firewall inside Nginx Stops malicious HTTP traffic before it hits your app, reducing load on the OS‑level bans

By the end you’ll have:

  • Automatic bans that are validated by the CrowdSec community.
  • Fail2Ban acting as the enforcement engine, keeping the rule set simple.
  • An OpenResty WAF that drops bad HTTP requests early, cutting noise for the lower layers.
  • A tuned configuration that keeps false positives at (practically) zero.

Note: All commands assume a Debian‑based VPS (Ubuntu 22.04 LTS). Adapt package names for Alpine, CentOS, etc.


Why combine CrowdSec, Fail2Ban, and OpenResty?

Feature CrowdSec Fail2Ban OpenResty WAF
Community threat intel
Real‑time log parsing ✅ (custom regex) ✅ (access logs)
Low‑resource footprint ✅ (C‑based) ✅ (Python) ✅ (NGINX + Lua)
HTTP‑aware filtering
Automatic ban propagation ✅ (bouncer) ✅ (jail)
False‑positive mitigation ✅ (scenario scoring) ❌ (static regex) ✅ (rule sets)

CrowdSec gives you smart detection; Fail2Ban gives you fast, reliable packet drops; OpenResty WAF gives you application‑level protection. Together they form a defense‑in‑depth stack that is still light enough for a $5‑$10 VPS.


Prerequisites

Item Minimum
VPS OS Ubuntu 22.04 (or Debian 11)
Root access sudo privileges
Open ports 22 (SSH), 80/443 (web)
Packages curl, git, build-essential

Make sure your system is up to date:

Bash
sudo apt update && sudo apt upgrade -y

Step 1 – Install and configure CrowdSec

1.1 Install the CrowdSec agent

Bash
# Add the official repository
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash

# Install the agent
sudo apt install crowdsec -y

The agent runs as a systemd service and starts parsing /var/log/auth.log, /var/log/nginx/access.log, etc., out of the box.

1.2 Add useful parsers and scenarios

CrowdSec ships with a huge catalogue. For a typical web VPS we’ll enable:

Bash
sudo cscli collections install crowdsecurity/sshd
sudo cscli collections install crowdsecurity/http-bad-bots
sudo cscli collections install crowdsecurity/nginx

Verify that the parsers are active:

Bash
sudo cscli parsers list | grep -E "sshd|nginx"

1.3 Test detection

Trigger a fake SSH brute‑force:

Bash
for i in {1..5}; do
  ssh -o StrictHostKeyChecking=no -o ConnectTimeout=1 invalid@localhost
done

Then check CrowdSec alerts:

Bash
sudo cscli alerts list

You should see an alert with a low confidence score (e.g., 0.3). CrowdSec only escalates to a ban when the score crosses the bouncers threshold, which we’ll configure next.

1.4 Configure the Fail2Ban bouncer

CrowdSec ships a Fail2Ban bouncer that writes IPs to a Fail2Ban jail. Install it:

Bash
sudo apt install crowdsec-firewall-bouncer-iptables -y

Enable and start the bouncer:

Bash
sudo systemctl enable crowdsec-firewall-bouncer
sudo systemctl start crowdsec-firewall-bouncer

Now CrowdSec will feed bans directly to iptables via Fail2Ban’s infrastructure. We still need a Fail2Ban jail to listen for those IPs.


2.1 Install Fail2Ban

Bash
sudo apt install fail2ban -y

2.2 Create a dedicated jail for CrowdSec

Create /etc/fail2ban/jail.d/crowdsec.conf:

INI
[crowdsec]
enabled = true
filter = crowdsec
action = iptables[name=CrowdSec, port=all, protocol=all]
logpath = /var/log/fail2ban.log
maxretry = 1
bantime = 86400   ; 1 day

The filter crowdsec is provided by the crowdsec-firewall-bouncer-iptables package and simply reads the list of IPs that CrowdSec has marked for banning.

2.3 Reload Fail2Ban

Bash
sudo systemctl restart fail2ban
sudo fail2ban-client status crowdsec

You should see an empty list of banned IPs at this point.

2.4 Verify the whole loop

Run the same SSH brute‑force loop again, then:

Bash
sudo fail2ban-client status crowdsec

You should now see the offending IP in the Banned column, and iptables -L -n will show a rule inserted by Fail2Ban.


Step 3 – Install OpenResty and enable its built‑in WAF

OpenResty bundles Nginx with the LuaJIT engine, making it easy to run a lightweight WAF written in Lua.

3.1 Install OpenResty

Bash
# Add OpenResty repository
sudo apt install -y software-properties-common
sudo add-apt-repository -y ppa:openresty/ppa
sudo apt update

# Install
sudo apt install -y openresty

3.2 Basic server block

Create /usr/local/openresty/nginx/conf/sites-available/myapp.conf:

NGINX
server {
    listen 80;
    server_name example.com;

    # Enable the built‑in WAF
    include /etc/openresty/waf/waf.conf;

    location / {
        proxy_pass http://127.0.0.1:8080;  # your app upstream
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Enable the site:

Bash
sudo ln -s /usr/local/openresty/nginx/conf/sites-available/myapp.conf \
          /usr/local/openresty/nginx/conf/sites-enabled/
sudo openresty -t && sudo systemctl restart openresty

3.3 Install the OpenResty WAF module

OpenResty ships a basic WAF under /etc/openresty/waf. If it’s missing, clone a popular open‑source rule set:

Bash
sudo git clone https://github.com/SpiderLabs/ModSecurity-nginx.git \
    /etc/openresty/waf

For simplicity we’ll use a minimal rule file waf.conf:

NGINX
# /etc/openresty/waf/waf.conf
lua_shared_dict waf_rules 10m;

init_by_lua_block {
    local rules = {
        -- Block common SQLi patterns
        sql_injection = [[(?i)(union\s+select|select\s+.*\s+from|drop\s+table)]],
        -- Block typical XSS payloads
        xss = [[(?i)<script|javascript:|onerror|onload]],
        -- Block known bad bots (user‑agent)
        bad_bot = [[(?i)masscan|zgrab|nikto|sqlmap]],
    }

    for k,v in pairs(rules) do
        ngx.shared.waf_rules:set(k, v)
    end
}

access_by_lua_block {
    local uri = ngx.var.request_uri
    local ua  = ngx.var.http_user_agent or ""
    local args = ngx.var.args or ""

    local function block(reason)
        ngx.log(ngx.ERR, "WAF blocked request: ", reason)
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    -- Simple pattern checks
    for name, pattern in pairs(ngx.shared.waf_rules:get_keys()) do
        local re = ngx.shared.waf_rules:get(name)
        if re and (uri:find(re) or args:find(re) or ua:find(re)) then
            block(name .. " pattern")
        end
    end
}

This Lua snippet loads a handful of regexes into a shared dictionary and checks every request in the access_by_lua_block. If any pattern matches, the request is denied with a 403.

3.4 Reload OpenResty

Bash
sudo openresty -t && sudo systemctl reload openresty

Now you have a web‑level firewall that catches obvious attacks before they even reach your application or the kernel.


Step 4 – Tuning thresholds to keep false positives at zero

4.1 CrowdSec scenario scores

CrowdSec scenarios have a score and a duration. The default bouncers threshold is 4. You can lower it if you want faster bans, but that raises false positives.

Bash
sudo cscli scenarios list | grep -E "score|duration"

If you notice legitimate traffic being banned, edit the scenario YAML under /etc/crowdsec/scenarios/ and reduce its score or increase duration.

4.2 Fail2Ban bantime

A long bantime is safe for obvious attacks, but for new IPs you may want a short trial ban (e.g., 10 min) before escalating to a day‑long ban.

INI
# /etc/fail2ban/jail.d/crowdsec.conf (add)
bantime = 600
findtime = 600
maxretry = 1

Then use a second jail that reads from a higher‑confidence CrowdSec list and applies a longer ban.

4.3 OpenResty WAF rule weighting

Instead of a flat block, you can log suspicious requests and only block after N hits from the same IP:

NGINX
access_by_lua_block {
    local ip = ngx.var.remote_addr
    local counter = ngx.shared.waf_rules:get(ip) or 0
    counter = counter + 1
    ngx.shared.waf_rules:set(ip, counter, 60)  -- keep for 60 s

    if counter > 5 then
        ngx.log(ngx.ERR, "WAF: IP ", ip, " exceeded request threshold")
        return ngx.exit(ngx.HTTP_FORBIDDEN)
    end

    -- existing pattern checks...
}

4.4 Monitoring

Enable CrowdSec’s metrics endpoint and feed it to Prometheus or just use cscli:

Bash
sudo cscli metrics list

Fail2Ban logs are in /var/log/fail2ban.log. OpenResty error logs (/usr/local/openresty/nginx/logs/error.log) will contain lines like WAF blocked request: sql_injection pattern.

Set up a simple cron that emails you when a new IP is banned:

Bash
# /etc/cron.daily/cs-f2b-report
#!/bin/bash
if sudo cscli alerts list -o json | jq -r '.[] | .source_ip' | grep -q .; then
    sudo cscli alerts list -o json | mail -s "CrowdSec new alerts" [email protected]
fi

Make it executable:

Bash
sudo chmod +x /etc/cron.daily/cs-f2b-report

Step 5 – Testing the full pipeline

5.1 Simulate an HTTP attack

Bash
curl -A "sqlmap" "http://your-vps-ip/?id=1 UNION SELECT password FROM users"

You should see:

Text
HTTP/1.1 403 Forbidden

OpenResty logs will contain WAF blocked request: sql_injection pattern.

5.2 Verify the IP is in the Fail2Ban jail

Bash
sudo fail2ban-client status crowdsec

You’ll see the attacking IP listed. Check iptables:

Bash
sudo iptables -L -n | grep <attacker_ip>

5.3 Ensure no legitimate request is blocked

Test a normal request:

Bash
curl -I http://your-vps-ip/

You should get 200 OK. If you notice a 403 on a legitimate call, revisit the Lua regexes or the CrowdSec scenario scores.


Checklist – Zero‑False‑Positive Intrusion Prevention

  • System updated (apt update && apt upgrade).
  • CrowdSec installed, parsers for SSH & Nginx enabled.
  • CrowdSec Fail2Ban bouncer installed and running.
  • Fail2Ban installed, crowdsec jail created, and service restarted.
  • OpenResty installed and basic site config created.
  • WAF Lua rules loaded (/etc/openresty/waf/waf.conf).
  • Scenario scores tuned to avoid premature bans.
  • Fail2Ban bantime/findtime set per risk appetite.
  • Logging enabled for CrowdSec, Fail2Ban, OpenResty.
  • Simple monitoring (cron email or Prometheus) in place.
  • Tested both malicious and normal traffic; no false positives observed.

FAQ

1. Do I really need all three components?
You can run any one of them alone, but each covers a different layer. CrowdSec gives you community‑driven detection, Fail2Ban enforces fast kernel‑level bans, and OpenResty blocks malicious HTTP before it reaches your app. Together they dramatically reduce noise and false positives.

2. How much RAM/CPU does this stack consume?
On a 1 vCPU, 512 MiB VPS the combined footprint stays under 80 MiB of RAM and negligible CPU (<2 %). The heavy lifting is done by CrowdSec’s compiled parsers; OpenResty’s Lua engine is lightweight.

3. Can I use nftables instead of iptables?
Yes. Install crowdsec-firewall-bouncer-nftables and adjust the Fail2Ban action to nftables. The rest of the workflow stays identical.

4. What if I host multiple domains on the same VPS?
Create separate OpenResty server blocks for each domain and point them to the same shared WAF config (include /etc/openresty/waf/waf.conf). CrowdSec will still parse all logs; you can add domain‑specific parsers if needed.

5. How often does CrowdSec update its threat database?
CrowdSec pulls updates from the community hub every 15 minutes by default. You can force a refresh with sudo cscli hub update.


Conclusion

By stitching CrowdSec, Fail2Ban, and OpenResty’s built‑in WAF together you get a low‑maintenance, high‑accuracy intrusion prevention system that works even on the cheapest VPS plans. The key is letting CrowdSec do the heavy detection, Fail2Ban enforce the bans, and OpenResty stop the bad HTTP traffic before it reaches your services.

Give it a try on your next low‑cost deployment and enjoy the peace of mind that comes from a truly zero‑false‑positive setup. For more hands‑on guides and real‑world tweaks, check out the tutorials at mahbuburriad.com.

Related

Related posts