JIT
← Notebook
Field notes SecurityWAFHomelab

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.

Client inbound HTTPS request Traefik — TLS termination reverse proxy · writes a JSON access log ① Bouncer — IP reputation source IP vs the ban cache (refreshed 60s) local scenarios + CrowdSec community blocklist ✗ known-bad IP — dropped allowed ② AppSec WAF · Coraza (in-band) reads the request against WAF rules today: virtual patching (known-CVE signatures) ✗ exploit signature — 403, inline clean Website — served · 200 CrowdSec agent parses the access log → runs scenarios → writes ban decisions CrowdSec CAPI (cloud) community blocklist — auto-pulled anonymised signals shared access log ban decisions
Two gates per request: reputation drops known-bad IPs, then the WAF reads what's left and blocks exploit signatures inline. Clean traffic reaches the site. Underneath, the access log becomes ban decisions that — with the community blocklist — keep refreshing the first gate.

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. 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. 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/config403.

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