Episode 12/13: CrowdSec next to fail2ban, and an AppSec WAF in Caddy
Measured on the demo box against the same 24h of real traffic. Episode 12 (detection, blocklist, firewall): - crowdsec/ with install notes and the traps: port 8080 clash, empty first blocklist pull, replay forensic-mode dropping events (FIFO workaround), ssh-time-based-bf shipping remediation:false, the AppSec collections that refuse to start without extra rules. - firewall bouncer in nftables mode (input + forward), which blocks Docker-published ports and survives ufw reload; iptables/DOCKER-USER does not. - HARDENING.md episode 12 section with the numbers: fail2ban 4 banned IPs vs CrowdSec 23 detected / 1 blocked, blocklist overlap 50% of IPs but 19% of attempts, and the mode=normal blind spot for key-only root attempts. Episode 13 (AppSec WAF): - caddy/Dockerfile now builds the CrowdSec bouncer and AppSec handler alongside rate-limit, pinned to v0.14.1. - Caddyfile imports a crowdsec snippet (bouncer + appsec) on every site except vpn; compose passes CROWDSEC_BOUNCER_KEY and host.docker.internal. - Measured: virtual patching blocks known CVEs only (7 of 41 probes, near-zero false positives); OWASP CRS raises that to 27 of 41 but breaks git push and wiki editing at default paranoia. Default stays vpatch+generic. fail2ban config unchanged. No secrets or real domains/IPs in tracked files.
This commit is contained in:
parent
d147f21490
commit
88670f028f
11 changed files with 280 additions and 9 deletions
22
Caddyfile
22
Caddyfile
|
|
@ -5,6 +5,16 @@
|
|||
email {$ACME_EMAIL}
|
||||
# episode 10: rate_limit has to run before reverse_proxy hands the request on
|
||||
order rate_limit before basic_auth
|
||||
# episode 12: CrowdSec decides first (banned IP -> 403), then the AppSec WAF
|
||||
# checks the request itself. Both talk to CrowdSec on the host.
|
||||
order crowdsec first
|
||||
order appsec after crowdsec
|
||||
crowdsec {
|
||||
api_url http://host.docker.internal:8090
|
||||
api_key {$CROWDSEC_BOUNCER_KEY}
|
||||
ticker_interval 10s
|
||||
appsec_url http://host.docker.internal:7422
|
||||
}
|
||||
}
|
||||
|
||||
# episode 10: one snippet, imported into every site. JSON access log (fail2ban
|
||||
|
|
@ -22,8 +32,15 @@
|
|||
header -Server
|
||||
}
|
||||
|
||||
# episode 12: blocklist + WAF. Imported everywhere except vpn. (NetBird gRPC streams).
|
||||
(crowdsec) {
|
||||
crowdsec
|
||||
appsec
|
||||
}
|
||||
|
||||
git.{$DOMAIN} {
|
||||
import common
|
||||
import crowdsec
|
||||
# episode 10: throttle login attempts (10 per minute per source IP)
|
||||
rate_limit {
|
||||
zone git_login {
|
||||
|
|
@ -41,11 +58,13 @@ git.{$DOMAIN} {
|
|||
|
||||
wiki.{$DOMAIN} {
|
||||
import common
|
||||
import crowdsec
|
||||
reverse_proxy outline:3000
|
||||
}
|
||||
|
||||
vault.{$DOMAIN} {
|
||||
import common
|
||||
import crowdsec
|
||||
# episode 10: the admin panel is a setup tool, not a public page (was 200)
|
||||
@admin path /admin*
|
||||
respond @admin 404
|
||||
|
|
@ -65,12 +84,14 @@ vault.{$DOMAIN} {
|
|||
|
||||
status.{$DOMAIN} {
|
||||
import common
|
||||
import crowdsec
|
||||
reverse_proxy uptime-kuma:3001
|
||||
}
|
||||
|
||||
# Push notifications (episode 6). Websockets and long polling pass through as-is.
|
||||
ntfy.{$DOMAIN} {
|
||||
import common
|
||||
import crowdsec
|
||||
reverse_proxy ntfy:80
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +101,7 @@ ntfy.{$DOMAIN} {
|
|||
# one door that has to stay open - and it is exactly one door wide.
|
||||
n8n.{$DOMAIN} {
|
||||
import common
|
||||
import crowdsec
|
||||
@webhooks path /webhook/* /webhook-test/* /webhook-waiting/*
|
||||
handle @webhooks {
|
||||
reverse_proxy n8n:5678
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue