CrowdSec für Caddy: Reverse Proxy im Homelab mit dem nftables-Bouncer absichern
Ich hatte gerade eine neue öffentliche Subdomain angelegt, und ein paar Minuten später standen die ersten fremden Anfragen im Log. Lauter 401er, also Scanner, die Login-Seiten abklappern. Mein Caddy-Reverse-Proxy beantwortet das zwar brav, aber ich wollte, dass solche IPs gar nicht erst bis zu den Diensten durchkommen. Deshalb läuft jetzt CrowdSec auf dem Proxy.
Caddy läuft bei mir an jedem Standort in einem eigenen LXC auf Proxmox. CrowdSec habe ich direkt in diesen Container installiert, zusammen mit dem Firewall-Bouncer für nftables. Der Bouncer sperrt IPs auf Firewall-Ebene, deshalb brauchst du keinen eigenen Caddy-Build mit Bouncer-Modul.
Schritt 1: CrowdSec und Bouncer installieren
Auf einem Debian-LXC fügst du zuerst das offizielle Repository hinzu und installierst dann beide Pakete. Bei mir ist es CrowdSec 1.8.1.
curl -s https://install.crowdsec.net | sh
apt install crowdsec crowdsec-firewall-bouncer-nftables
Wenn der Bouncer auf derselben Maschine wie CrowdSec landet, registriert er sich bei der Installation selbst an der lokalen API. Danach kommen die Collections:
cscli collections install crowdsecurity/caddy crowdsecurity/http-cve \
crowdsecurity/base-http-scenarios crowdsecurity/whitelist-good-actors
systemctl reload crowdsec
crowdsecurity/caddybringt den Parser für die Caddy-Logs mit.crowdsecurity/http-cveerkennt Anfragen, die bekannte Sicherheitslücken ausnutzen wollen.crowdsecurity/base-http-scenariosenthält die allgemeinen Szenarien wie Scans und Bruteforce auf HTTP.crowdsecurity/whitelist-good-actorssorgt dafür, dass bekannte gute Bots nicht gesperrt werden.
Zusätzlich habe ich die Instanz in der CrowdSec Console angemeldet. Über die Central API (CAPI) bekommt sie dann die Community-Blocklist, also IPs, die andere CrowdSec-User bereits gemeldet haben.
Schritt 2: Caddy schreibt JSON-Logs
CrowdSec kann nur auswerten, was im Access-Log steht. Damit ich den Log-Block nicht in jede Site kopieren muss, steht er bei mir als Snippet im Caddyfile, und jede Site holt ihn sich per import:
(access_log) {
log {
output file /var/log/caddy/access.log
format json
}
}
app.example.de {
import access_log
reverse_proxy 192.168.1.20:8080
}
cloud.example.de {
import access_log
reverse_proxy 192.168.1.21:8443
}
Das Snippet muss im Caddyfile vor der ersten Site stehen, die es importiert. Steht es weiter unten, kennt Caddy es an der Stelle noch nicht.
Achte auch darauf, wem die Log-Datei gehört. Ich habe einmal caddy validate als root ausgeführt, bevor die Datei existierte. Dabei hat Caddy sie als root angelegt, und der eigentliche Caddy-Prozess, der als User caddy läuft, konnte danach nicht mehr reinschreiben. Logs kamen keine mehr an. Der Fix:
chown caddy:caddy /var/log/caddy/access.log
Schritt 3: Die Acquisition
Jetzt muss CrowdSec wissen, welche Datei es lesen soll und welcher Parser dazugehört. Das steht in /etc/crowdsec/acquis.d/caddy.yaml:
filenames:
- /var/log/caddy/access.log
labels:
type: caddy
Nach einem systemctl reload crowdsec siehst du mit cscli metrics, ob Zeilen aus dem Access-Log gelesen und geparst werden.

Schritt 4: Whitelists, damit du dich nicht selbst aussperrst
Bevor CrowdSec anfängt zu sperren, sollten deine eigenen Netze auf der Whitelist stehen. Bei mir liegt dafür eine Datei in /etc/crowdsec/parsers/s02-enrich/, zum Beispiel my-whitelist.yaml:
name: local/my-whitelist
description: "Eigene Netze nie sperren"
whitelist:
reason: "LAN, Tailscale und eigene feste IP"
ip:
- "203.0.113.10"
cidr:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "172.16.0.0/12"
- "100.64.0.0/10"
Drin sind die privaten Adressbereiche, der Tailscale-Bereich 100.64.0.0/10 und die feste öffentliche IP meines Anschlusses zu Hause. Die 203.0.113.10 oben ist nur ein Platzhalter, da gehört deine eigene IP hin.
Der zweite Standort mit dynamischer IP
Mein zweiter Standort hat eine dynamische IP und ist über einen DDNS-Hostnamen erreichbar. Eine statische Whitelist hilft da wenig, weil die IP sich ändert. Deshalb gibt es eine zweite Whitelist-Datei, die ein kleines Script alle 15 Minuten neu schreibt:
#!/bin/sh
HOST="standort2.example.de"
FILE="/etc/crowdsec/parsers/s02-enrich/ddns-whitelist.yaml"
IP=$(getent ahostsv4 "$HOST" | awk 'NR==1 {print $1}')
[ -n "$IP" ] || exit 1
cat > "$FILE.tmp" <<EOF
name: local/ddns-whitelist
description: "Dynamische IP vom zweiten Standort"
whitelist:
reason: "zweiter Standort per DDNS"
ip:
- "$IP"
EOF
if ! cmp -s "$FILE.tmp" "$FILE"; then
mv "$FILE.tmp" "$FILE"
systemctl reload crowdsec
else
rm "$FILE.tmp"
fi
Leg das Script zum Beispiel unter /usr/local/bin/crowdsec-ddns-whitelist.sh ab. CrowdSec wird nur neu geladen, wenn sich die IP wirklich geändert hat. Gestartet wird es von einem systemd-Timer:
# /etc/systemd/system/crowdsec-ddns-whitelist.service
[Unit]
Description=CrowdSec Whitelist fuer DDNS-Standort aktualisieren
[Service]
Type=oneshot
ExecStart=/usr/local/bin/crowdsec-ddns-whitelist.sh
# /etc/systemd/system/crowdsec-ddns-whitelist.timer
[Unit]
Description=CrowdSec DDNS-Whitelist alle 15 Minuten
[Timer]
OnBootSec=2min
OnCalendar=*:0/15
[Install]
WantedBy=timers.target
chmod +x /usr/local/bin/crowdsec-ddns-whitelist.sh
systemctl daemon-reload
systemctl enable --now crowdsec-ddns-whitelist.timer
Die Pitfalls aus meinem Setup
1. Caddy hing in „reloading“
Nach den Änderungen am Caddyfile wollte ich mit systemctl reload caddy neu laden, und der Befehl lief in einen Timeout. Die systemd-Unit hing im Status „reloading“ fest. Funktioniert hat der Reload direkt über die Admin-API von Caddy:
caddy reload --config /etc/caddy/Caddyfile --force
Damit war die neue Config aktiv. Einen echten Restart des Dienstes habe ich später in einem ruhigen Moment nachgeholt, damit auch systemd wieder einen sauberen Status hat.
2. 512 MB RAM sind zu knapp
Mein Caddy-LXC hatte bis dahin 512 MB RAM, für Caddy allein war das genug. Mit CrowdSec dazu habe ich den Container auf 1 GB hochgesetzt.
Kontrollieren, was CrowdSec macht
Diese vier Befehle nutze ich am häufigsten:
| Befehl | Zeigt |
|---|---|
cscli decisions list | Aktuell gesperrte IPs mit Grund und Restlaufzeit |
cscli alerts list | Welche Szenarien wann ausgelöst haben |
cscli metrics | Gelesene Logzeilen, Parser, Bouncer-Aktivität |
cscli collections list | Installierte Collections und ob sie aktuell sind |

Wenn du testen willst, ob die Whitelist greift, schau nach ein paar Fehlversuchen aus dem eigenen LAN in cscli decisions list. Deine eigene IP sollte dort nie auftauchen.
Fazit
CrowdSec liest jetzt die JSON-Logs von Caddy, der nftables-Bouncer sperrt erkannte Scanner direkt an der Firewall, und die Community-Blocklist hält bekannte IPs schon vorher draußen. Caddy selbst läuft dabei weiter als Standard-Build ohne zusätzliche Module.
Nutzt du CrowdSec schon, oder setzt du auf Fail2ban? Schreib es in die Kommentare.