Webserver Hardening with CrowdSec
Your server is online. Docker's running, nginx-proxy handles routing, Let's Encrypt provides SSL. Everything works. But check your access logs — within hours of going live, you'll see requests for /wp-login.php, /.env, /actuator/health, and paths you've never heard of. Scanners like LeakIX, Shodan, and random botnets probe every public IP, constantly, automatically.
A firewall blocks ports. Security headers harden responses. But neither detects and reacts to attack patterns. That's where CrowdSec comes in: an open-source security engine that parses your logs in real time, detects malicious behavior, and blocks offenders at the firewall level — with shared community intelligence on top.
Think of it as fail2ban on steroids, with a global threat database.
The Architecture
graph TD
classDef default fill:#f8f9fa,stroke:#333,stroke-width:1px
classDef highlight fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
Internet((Internet)) -->|request| nginx-proxy
nginx-proxy -->|stdout| Docker[Docker Daemon]
Docker -->|docker.sock| CrowdSec:::highlight
CrowdSec -->|"ban IP"| Bouncer[Firewall Bouncer]:::highlight
Bouncer -->|iptables DROP| Internet
Community[(Community Blocklists)] -.->|shared intel| CrowdSecCrowdSec reads nginx-proxy's container logs via the Docker socket — no log file gymnastics needed. When it detects an attack pattern (brute force, path scanning, CVE exploits), it tells the firewall bouncer to DROP that IP.
What You Need
- A running nginx-proxy setup (see The 10-Minute Website Setup)
- Root SSH access
- 10 minutes
Step 1: Deploy CrowdSec
mkdir -p ~/crowdsec && cd ~/crowdsec
Create docker-compose.yml:
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: always
environment:
- COLLECTIONS=crowdsecurity/nginx crowdsecurity/http-cve crowdsecurity/whitelist-good-actors
volumes:
- crowdsec-db:/var/lib/crowdsec/data/
- crowdsec-config:/etc/crowdsec/
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/log:/var/log/host:ro
ports:
- "127.0.0.1:8080:8080"
volumes:
crowdsec-db:
crowdsec-config:
docker compose up -d
Wait 30 seconds for CrowdSec to initialize and install the collections.
Step 2: Point CrowdSec at nginx-proxy
Here's a subtlety: the nginx-proxy Docker image symlinks its logs to /dev/stdout. So there are no log files to read. But CrowdSec can read Docker container stdout directly — which is actually the cleaner solution.
docker exec crowdsec sh -c 'cat > /etc/crowdsec/acquis.d/docker-nginx.yaml << EOF
source: docker
container_name:
- nginx-proxy
labels:
type: nginx
EOF'
docker compose restart
Verify it's reading:
docker exec crowdsec cscli metrics
You should see acquisition metrics with lines being read and parsed from docker:nginx-proxy.
Step 3: Install the Firewall Bouncer
CrowdSec detects. Bouncers act. The firewall bouncer adds attackers to iptables — this runs on the host, not in Docker:
curl -s https://install.crowdsec.net | bash
apt install -y crowdsec-firewall-bouncer-iptables
Generate an API key and configure:
BOUNCER_KEY=$(docker exec crowdsec cscli bouncers add firewall-bouncer -o raw)
cat > /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml << EOF
mode: iptables
api_url: http://127.0.0.1:8080/
api_key: ${BOUNCER_KEY}
disable_ipv6: false
deny_action: DROP
deny_log: true
deny_log_prefix: "crowdsec: "
update_frequency: 10s
iptables_chains:
- INPUT
- DOCKER-USER
EOF
systemctl enable crowdsec-firewall-bouncer
systemctl start crowdsec-firewall-bouncer
The DOCKER-USER chain is critical. Traffic to Docker containers flows through FORWARD, not INPUT. Without it, CrowdSec would only protect services running directly on the host — not your containerized sites.
Step 4: Test It
# Ban a test IP
docker exec crowdsec cscli decisions add --ip 1.2.3.4 --duration 5m --reason "test"
# Wait for the bouncer to pick it up
sleep 15
# Verify it's blocked
iptables -L CROWDSEC_CHAIN -n
ipset list crowdsec-blacklists-0
# Clean up
docker exec crowdsec cscli decisions delete --ip 1.2.3.4
You should see 1.2.3.4 in the ipset. If the chain shows "0 references", the bouncer hasn't linked to the traffic chain — restart it and check again.
Step 5: Join the Community
CrowdSec's real power is collective. Create a free account at app.crowdsec.net and enroll:
docker exec crowdsec cscli console enroll <your-key>
Now you get pre-emptive blocks from IPs that attacked other CrowdSec users worldwide. Attack one, get blocked by thousands.
What Gets Blocked?
Out of the box with the nginx collection:
| Scenario | What it catches |
|---|---|
| HTTP crawl | Aggressive scanning and crawling |
| HTTP probing | Vulnerability discovery attempts |
| HTTP CVE exploits | Known attack patterns |
| Brute force | Too many failed requests |
| Bad user agents | Known malicious scanners |
Check your alerts after a day:
docker exec crowdsec cscli alerts list
You'll likely see hits within hours.
Useful Commands
# Current bans
docker exec crowdsec cscli decisions list
# Recent alerts
docker exec crowdsec cscli alerts list
# What's being parsed
docker exec crowdsec cscli metrics
# Manually ban an IP
docker exec crowdsec cscli decisions add --ip 1.2.3.4 --duration 24h --reason "manual"
# Unban
docker exec crowdsec cscli decisions delete --ip 1.2.3.4
# Installed scenarios
docker exec crowdsec cscli scenarios list
# Bouncer healthy?
systemctl status crowdsec-firewall-bouncer
The Full Picture
After this setup, your server has:
| Layer | Protection |
|---|---|
| Firewall | UFW — only 22/80/443 open |
| Reverse Proxy | nginx-proxy with security headers |
| SSL | Let's Encrypt, auto-renewed |
| Docker | Socket proxy, limited API access |
| Detection | CrowdSec parsing all logs |
| Blocking | iptables bouncer, auto-ban |
| Intelligence | Community blocklists |
From "probably fine" to "properly hardened" — and the scanners will keep coming. Now they bounce off.
Resources
- CrowdSec — Open-source security engine
- CrowdSec Hub — Collections, parsers, scenarios
- CrowdSec Console — Dashboard and community enrollment
- Firewall Bouncer — iptables/nftables integration
- nginx-proxy — JWilder's reverse proxy
Previously: The 10-Minute Website Setup — Docker, nginx-proxy, and Let's Encrypt from scratch.