Webserver Hardening with CrowdSec

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| CrowdSec

CrowdSec 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


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:

ScenarioWhat it catches
HTTP crawlAggressive scanning and crawling
HTTP probingVulnerability discovery attempts
HTTP CVE exploitsKnown attack patterns
Brute forceToo many failed requests
Bad user agentsKnown 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:

LayerProtection
FirewallUFW — only 22/80/443 open
Reverse Proxynginx-proxy with security headers
SSLLet's Encrypt, auto-renewed
DockerSocket proxy, limited API access
DetectionCrowdSec parsing all logs
Blockingiptables bouncer, auto-ban
IntelligenceCommunity blocklists

From "probably fine" to "properly hardened" — and the scanners will keep coming. Now they bounce off.


Resources


Previously: The 10-Minute Website Setup — Docker, nginx-proxy, and Let's Encrypt from scratch.