Two gates in front of the website
Defense-in-depth at the homelab edge with CrowdSec: known-bad IPs dropped at the door, then an in-band WAF reads what's left. And why it's deliberately fail-open for now.
The website runs behind Traefik, which already gives it TLS and clean routing. But TLS only proves who you’re talking to, not what they’re sending. I wanted two more gates in front of the site: one that throws out visitors with a bad reputation before they cost me anything, and one that actually reads the request and refuses the obviously malicious. Both are CrowdSec.
Gate one, reputation, at the door
The first gate is the cheapest. A CrowdSec bouncer plugin sits inside Traefik and, for every request, checks the source IP against a cache of ban decisions it refreshes each minute. Two things fill that cache: local scenarios that read Traefik’s own access log and ban IPs caught probing (the crowdsecurity/traefik and http-cve rules), and the CrowdSec community blocklist: addresses other installs have already reported, pulled down automatically. It’s the bouncer checking your face against the list at the door. A known-bad IP never reaches the app. Blunt and fast, which is exactly right for the bulk of internet noise.
Gate two, a WAF that reads the request
Reputation can’t catch a clean-looking IP sending a dirty request. That’s the second gate: CrowdSec’s AppSec component, a WAF built on the Coraza engine. Where gate one checks your face, this is the bag scanner reading what you’re actually carrying. The same bouncer forwards each allowed request to it for in-band inspection. The verdict comes back before the request reaches the site. Today it runs virtual-patching rules: 182 signatures for specific, known CVE exploits, the near-zero-false-positive kind. Match one and you get a 403, inline. The test is satisfying: GET / → 200, GET /.git/config → 403.
# Gate 2 (WAF) live: a clean request passes, an exploit probe is blocked inline.
curl -sI https://www.jit-c.eu/ | head -1 # HTTP/2 200
curl -sI https://www.jit-c.eu/.git/config | head -1 # HTTP/2 403 (CrowdSec AppSec)
# Gate 1 (IP reputation): what's banned right now, local scenarios + community blocklist?
cscli decisions list
The honest part: fail-open, and a crash on day one
First deploy, CrowdSec crash-looped. The stock config referenced a rule set nothing had installed, and it refused to start. The site never noticed. The bouncer is fail-open: if the WAF is unreachable, traffic still flows. The bouncer was passed out on the pavement and the party inside carried on. I’d rather serve an unfiltered request than give a misbehaving security tool a kill switch over my own website. The fix was a small self-contained config that loads only the rules I actually have; it came up clean.
A WAF that takes your site down when it breaks isn’t protecting your site. Start fail-open; earn fail-closed.
Fail-open is deliberate for now. Next: the OWASP Core Rule Set as a broader net, then a default-deny allowlist of what the app legitimately does, and only then the flip to fail-closed, once I’ve watched it long enough to trust it. One loose end: the community blocklist updates itself, but the rule signatures don’t. A scheduled refresh is on the list. A virtual patch you never update is just a comment.