diff --git a/.env.example b/.env.example index 01f28e9..25efe7d 100644 --- a/.env.example +++ b/.env.example @@ -78,9 +78,3 @@ RUNNER_UUID= # The runner still refuses any action or any target that is not on its own # hard-coded allowlists, so a leaked token cannot widen the blast radius. OPS_ACTIONS_TOKEN= - -# --- Episode 12: CrowdSec ------------------------------------------------- -# The Caddy bouncer's key for the CrowdSec Local API on the host: -# cscli bouncers add caddy-bouncer -o raw -# Without it Caddy still starts, but blocks nothing and logs auth errors. -CROWDSEC_BOUNCER_KEY= diff --git a/Caddyfile b/Caddyfile index 8c90a9d..e9f8758 100644 --- a/Caddyfile +++ b/Caddyfile @@ -5,16 +5,6 @@ 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 @@ -32,15 +22,8 @@ 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 { @@ -58,13 +41,11 @@ 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 @@ -84,14 +65,12 @@ 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 } @@ -101,7 +80,6 @@ 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 diff --git a/HARDENING.md b/HARDENING.md index d1baf30..4377ee9 100644 --- a/HARDENING.md +++ b/HARDENING.md @@ -86,12 +86,6 @@ advice from episode 1 got wrong: the ban in INPUT; Caddy's traffic is forwarded, so a bot "banned" in INPUT keeps getting 404s (measured). Set `chain = DOCKER-USER` as a plain jail parameter and the same bot runs into a refused connection instead. -- **Once passwords are off, `mode = normal` goes half blind** (episode 12). - Key-only root attempts log as `Connection closed by authenticating user - root [preauth]`, which the normal sshd filter ignores. Measured: 192 attempts - from one IP in 24 h, zero seen. `mode = aggressive` in the jail counts them; - it also counts the second line of each invalid-user attempt, so maxretry 5 - bans after roughly 3 tries. Operational note: `ufw reload` flushes `DOCKER-USER` and takes fail2ban's ban jumps with it, run `systemctl restart fail2ban` afterwards to restore them. @@ -380,70 +374,3 @@ agent doing the whole job against seven tools gets the counts right often but not always; splitting the work into three narrow roles, each with one skill file (`agents/skills/`), trades more tokens for a more reliable report. The numbers are in the lab notes. - -## Episode 12: CrowdSec next to fail2ban, same box, same logs - -A viewer asked for it: does CrowdSec replace fail2ban? We ran both on the demo -box and gave them the same 24 hours of real traffic (fail2ban live, CrowdSec by -replaying the identical sshd log). Setup and traps: `crowdsec/README.md`. - -**What fail2ban caught in 24 h** (password auth already off since episode 10): -1,458 SSH login attempts from 218 IPs. fail2ban banned **4 IPs** (6 bans). -Two blind spots, both measured: - -- *Slow bots.* One IP tried 273 times, one attempt every ~4.5 minutes. Never 5 - in 10 minutes, so never banned. -- *Root without a password path.* `Connection closed by authenticating user - root [preauth]` is not a failure in the default `mode = normal` of the sshd - filter. One IP made 192 of those attempts and fail2ban did not see a single - one. `mode = aggressive` matches 2,192 of 2,345 lines instead of 696. - -**What CrowdSec detected on the same log:** 23 IPs, including the slow one. -**What it would have banned: 1 IP.** 22 of the 23 came from -`ssh-time-based-bf`, and that scenario ships with `remediation: false`: it -raises an alert (and a community signal) but no decision. Out of the box, -CrowdSec *sees* more and *blocks* less than fail2ban on this traffic. If you -want the slow ones banned, add a profile for that scenario instead of editing -it: a modified hub scenario is "tainted", its signals stop counting, and a -non-contributing engine drops to the 3,000-IP Lite blocklist. - -**The community blocklist:** 15,000 IPs on day one (after a restart, see the -README). Of the 218 IPs that tried to log in during the previous 24 h, **109 -were already on it**, but those 109 made only **19 %** of the attempts. The list -knows the one-shot scanners; the persistent bots that hammer a single box were -mostly not on it (1 of the top 10). None of the 4 IPs fail2ban banned were -listed. The two tools overlap less than you would think. - -**The firewall bouncer and Docker, again.** In its default iptables mode the -bouncer only hooks `INPUT`: a banned IP gets a timeout on SSH and still 200 on -every Docker-published port. `DOCKER-USER` fixes it until `ufw reload` flushes -the chain (it takes fail2ban's jump with it; nothing restores either). The -bouncer's **nftables mode** hooks `input` and `forward` in its own table, blocks -the Docker ports, and survived `ufw reload` untouched. - -**Controlled attack, both watching:** - -| | fail2ban | CrowdSec | -|---|---|---| -| invalid user, 1 try/s | banned after 5 tries (~5 s) | detected after 3 tries, firewall drop ~3 s later | -| `root`, key-only, 1 try/s | never | detected after 6 tries, drop ~3 s later | -| 12 web probes on vault (`/.env` answers 422) | no ban (filter counts 401/403/404/429 only) | detected after 3 probes, drop ~3 s later | - -fail2ban acts the moment it matches (REJECT, "connection refused"). CrowdSec -decides fast but the bouncer polls every 10 s, so a few more requests slip -through (DROP, timeout). - -**AppSec (WAF) in Caddy:** every exploit request (`/.env`, `/.git/config`, the -PHPUnit RCE) got a **403 on the first try**, while normal requests in between -stayed 200. Two blocks and the IP is banned at the firewall. Cost: +1 to 2 ms -median time to first byte, one more round-trip to the host per request. - -**What it costs on a 4 GB box:** CrowdSec ~200 MB RSS, firewall bouncer -~30 MB, fail2ban ~50 MB. The engine, the bouncers and the hub are MIT or Apache -licensed and free; the 15k community list is free as long as you share -signals. Bigger lists and the console's premium tier are paid. - -**Verdict for this stack:** run both. fail2ban reacts instantly to the loud -ones on the lines you tell it to watch; CrowdSec adds the slow bots (as -alerts), the blocklist and the WAF, at 4x the memory. Neither replaces the -other on these numbers. diff --git a/README.md b/README.md index 4980d8a..73b2ad4 100644 --- a/README.md +++ b/README.md @@ -247,12 +247,6 @@ security updates, pinned image tags, offsite backups. The ready-to-paste configs from episode 10 (ufw `after.rules`, fail2ban jails and filter, sshd drop-in) live in **[hardening/](hardening/)**. -Episode 12 adds CrowdSec next to fail2ban (not instead of it): community -blocklist, a firewall bouncer in nftables mode that Docker cannot bypass, and an -AppSec WAF inside Caddy. Install notes and the traps we measured are in -**[crowdsec/](crowdsec/)**. Note the Caddy image now needs the key -`CROWDSEC_BOUNCER_KEY` in `.env`. - --- MIT — do whatever. If it saved you money, a YouTube subscription is the price: diff --git a/caddy/Dockerfile b/caddy/Dockerfile index e3f943e..108cbab 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -1,13 +1,9 @@ # episode 10: the stock caddy:2-alpine image cannot rate limit. The directive # lives in a plugin (github.com/mholt/caddy-ratelimit) that has to be compiled -# in with xcaddy - there is no runtime "enable" for it. -# episode 12: same trick for CrowdSec. The bouncer (http) and the AppSec -# handler (appsec) are plugins too, pinned to a release tag instead of @main. +# in with xcaddy - there is no runtime "enable" for it. Two stages so the final +# image stays the same small alpine, just with our own caddy binary. FROM caddy:2-builder-alpine AS builder -RUN xcaddy build \ - --with github.com/mholt/caddy-ratelimit \ - --with github.com/hslatman/caddy-crowdsec-bouncer/http@v0.14.1 \ - --with github.com/hslatman/caddy-crowdsec-bouncer/appsec@v0.14.1 +RUN xcaddy build --with github.com/mholt/caddy-ratelimit FROM caddy:2-alpine COPY --from=builder /usr/bin/caddy /usr/bin/caddy diff --git a/caddy/README.md b/caddy/README.md index cc64c9b..c3b9916 100644 --- a/caddy/README.md +++ b/caddy/README.md @@ -1,23 +1,16 @@ -# caddy/ (episode 10, extended in episode 12) +# caddy/ (episode 10) -Custom Caddy image with the rate limiting plugin, the CrowdSec bouncer and the -CrowdSec AppSec handler compiled in (see `crowdsec/README.md`). +Custom Caddy image with the rate limiting plugin compiled in. The stock `caddy:2-alpine` has no `rate_limit` directive; it is not a runtime flag, the module must be built into the binary with `xcaddy`. `compose.yml` carries both `image:` and `build: ./caddy`, so `docker compose build caddy` -produces `selfhostedops/caddy:crowdsec` locally and `docker compose up -d` +produces `selfhostedops/caddy:ratelimit` locally and `docker compose up -d` uses it. Do not build on a small box while the stack is running - a Go build can push a 2 GB machine into OOM and take the live services down with it. Build it elsewhere (a bigger machine, or a throwaway host) and move it over: - docker build -t selfhostedops/caddy:crowdsec ./caddy - docker save selfhostedops/caddy:crowdsec | ssh root@box docker load - -Measured in episode 12: the build with all three modules took 60 s on a 12 GB -build host, the image is 158 MB (the rate-limit-only image was 154 MB). - -The modules are pinned to the release tag (`@v0.14.1`), not `@main` as the -upstream README suggests, so a rebuild next month gives the same binary. + docker build -t selfhostedops/caddy:ratelimit ./caddy + docker save selfhostedops/caddy:ratelimit | ssh root@box docker load diff --git a/compose.yml b/compose.yml index d8d9a55..1ec8724 100644 --- a/compose.yml +++ b/compose.yml @@ -58,7 +58,7 @@ volumes: services: caddy: - image: selfhostedops/caddy:crowdsec # episode 12: caddy-ratelimit + CrowdSec bouncer + AppSec + image: selfhostedops/caddy:ratelimit # episode 10: own image with caddy-ratelimit build: ./caddy restart: unless-stopped ports: @@ -68,9 +68,6 @@ services: environment: DOMAIN: ${DOMAIN} ACME_EMAIL: ${ACME_EMAIL} - CROWDSEC_BOUNCER_KEY: ${CROWDSEC_BOUNCER_KEY} # episode 12, see crowdsec/README.md - extra_hosts: - - "host.docker.internal:host-gateway" # episode 12: CrowdSec runs on the host volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./logs/caddy:/var/log/caddy # episode 10: JSON access log on the host for fail2ban diff --git a/crowdsec/README.md b/crowdsec/README.md deleted file mode 100644 index a78b705..0000000 --- a/crowdsec/README.md +++ /dev/null @@ -1,199 +0,0 @@ -# crowdsec/ (episode 12) - -CrowdSec next to fail2ban on the same box, both reading the same logs. This -folder holds the files that differ from a stock install. Everything was -measured on the demo box; the numbers are in `HARDENING.md` (episode 12). - -CrowdSec runs on the **host**, not in the stack: it reads the sshd journal -directly and drives the host firewall. Caddy talks to it over the Docker -bridge. - -## Install (Debian 13) - -``` -curl -fsSL https://install.crowdsec.net | sh -apt install crowdsec -``` - -**Port 8080 trap.** The Local API defaults to `127.0.0.1:8080`. On this stack -the NetBird dashboard already publishes `127.0.0.1:8080`, so the installer -prints one line ("Not attempting to start crowdsec, port 8080 is already -used") and leaves the service **inactive**. Move it: - -``` -sed -i 's#listen_uri: 127.0.0.1:8080#listen_uri: 0.0.0.0:8090#' /etc/crowdsec/config.yaml -sed -i 's#url: http://127.0.0.1:8080#url: http://127.0.0.1:8090#' /etc/crowdsec/local_api_credentials.yaml -``` - -(`0.0.0.0` only because the Caddy container has to reach it, see below. If you -skip the Caddy bouncer, keep `127.0.0.1:8090`.) - -**First start pulls nothing.** The very first community-blocklist pull ran -before the scenario list was registered ("scenario list is empty, will not -pull yet", 0 entries). A `systemctl restart crowdsec` pulled 15,000 IPs a -minute later. Check with `cscli decisions list --origin CAPI | wc -l`. - -## Log sources and collections - -``` -cscli collections install crowdsecurity/caddy -cp acquis.d/caddy.yaml /etc/crowdsec/acquis.d/ -``` - -The installer already adds `crowdsecurity/sshd` and a journald source for -`ssh.service`. - -## Firewall bouncer - -``` -apt install crowdsec-firewall-bouncer-iptables -``` - -The package registers itself with the Local API. Then apply -`firewall-bouncer.yaml` (mode `nftables`) and restart it. Why not the -default iptables mode: it only inserts into `INPUT`. SSH is blocked, but every -Docker-published port (80, 443, 2222) stays reachable for a banned IP, the same -hole as fail2ban in episode 10. Adding `DOCKER-USER` fixes that until the next -`ufw reload`, which flushes the chain and nobody puts the rule back. - -## Caddy bouncer and AppSec (WAF) - -1. Build the Caddy image with the modules (`caddy/Dockerfile`), elsewhere, and - load it on the box. -2. AppSec collections, plus the rules the default config references but the - collections do not install (without them crowdsec **does not start**: - "no appsec-rules found for pattern crowdsecurity/experimental-*", then the - same for `generic-*`): - - ``` - cscli collections install crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules - cscli appsec-rules install crowdsecurity/experimental-no-user-agent \ - crowdsecurity/generic-freemarker-ssti crowdsecurity/generic-wordpress-uploads-listing \ - crowdsecurity/generic-wordpress-uploads-php - cp acquis.d/appsec.yaml /etc/crowdsec/acquis.d/ - crowdsec -t && systemctl restart crowdsec - ``` - - Run `crowdsec -t` first. A failed restart means no Local API; the firewall - keeps its current sets, but no new decisions arrive. -3. Let the Docker network in, and only that network: - - ``` - ufw allow proto tcp from 172.18.0.0/16 to 172.17.0.1 port 8090,7422 - ``` - - The container reaches the host as `host.docker.internal` (`extra_hosts: - host-gateway` in `compose.yml`, which is `172.17.0.1`). From the internet - both ports stay filtered. The mesh interface `wt0` is allowed wholesale by - episode 5, so mesh peers can reach both; each still needs an API key. -4. `cscli bouncers add caddy-bouncer -o raw` and put the key into `.env` as - `CROWDSEC_BOUNCER_KEY`, then `docker compose up -d caddy`. - -The `vpn.` site does not import the `crowdsec` snippet: it carries the NetBird -gRPC streams, and a WAF round-trip on every stream message is not what you want -between your peers. - -## Replaying old logs (and the forensic-mode trap) - -`crowdsec -dsn file:///path/to/log -type syslog` is documented as replay mode. -With CrowdSec 1.8.1 it shut down one second after reading the file and dropped -every event still in the pipeline: 47 or 49 alerts on two identical runs, none -after 02:07 of a 24 h log. Feeding the same file through a FIFO that stays open -60 s after the last line gives the full, repeatable result (133 alerts, twice): - -``` -mkfifo /tmp/replay.fifo -(cat sshd.log; sleep 60) > /tmp/replay.fifo & -crowdsec -c replay.yaml -order-event -dsn file:///tmp/replay.fifo -type syslog -``` - -Use a separate config (`replay.yaml`) with its own database, its own -`listen_uri` and **no `online_client`**, or the replayed alerts land in your -live database and go out as signals. - -## What goes out to CrowdSec - -With the default config, every alert from an unmodified hub scenario is pushed -as a signal: scenario name and hash, timestamp, your machine id, and **the -attacking IP**. Custom or modified ("tainted") scenarios and manual decisions -are not pushed unless you enroll in the console. Your own penetration tests are -real alerts too: switch `sharing: false` under `api.server.online_client` -while you attack your own box, or you report your test machine. - -## Optional: OWASP CRS for generic coverage, and the tuning it needs (episode 13) - -Virtual patching (the default above) only blocks known-CVE signatures. Measured -against 41 real probes: **7 blocked**. Generic SQLi, XSS, Log4Shell-via-header, -Struts OGNL and path traversal all passed, because there is no rule for them, -only per-CVE ones. `base-config` is just the Coraza body-processor setup, not -the OWASP Core Rule Set. - -The CRS is in the hub as an appsec-rule. Adding it raises coverage to **27 of 41**: - -``` -cscli appsec-rules install crowdsecurity/crs -``` - -But at default paranoia the CRS blocks legitimate use. Measured false positives -with CRS on and nothing else changed: git clone/push over HTTPS (the pack -negotiation body), Forgejo issues and file contents whose text contains code or -SQL, Outline documents with a code block, ntfy publish bodies, n8n webhook -bodies. Six normal workflows, all 403. For a stack where people push git and -write wiki pages, unusable as-is. - -The fix is a per-route exclusion. `appsec-rules/crs-route-exclusions.yaml` is a -small SecLang rule that runs in phase 1 and, for exactly the legitimate -application routes (Forgejo git smart-HTTP and REST API, Outline API, ntfy -publish, n8n webhooks), matched by `REQUEST_HEADERS:Host` + `REQUEST_URI`, -removes the CRS anomaly blocking rules with -`ctl:ruleRemoveById=949110,ctl:ruleRemoveById=959100` (949110 is the CRS -inbound anomaly-score blocker, 959100 the outbound one). CRS still scores the -request but no longer blocks it on those routes. - -Do **not** use `ctl:ruleEngine=Off` for this. It turns the whole Coraza engine -off for the request, and measured on the box that also disables the native -CrowdSec virtual-patching rules: on a route excluded with `ruleEngine=Off`, a -`.env` probe returned 404 (no WAF at all) instead of 403. With -`ctl:ruleRemoveById` the engine stays on, so virtual patching keeps firing: -measured, `.env`, the PHPUnit RCE and `.git/config` still return 403 on the -excluded routes, while generic SQLi/XSS now pass (that is the point). - -Rule order does not matter for `ctl:ruleRemoveById` (a phase-1 ctl removes the -phase-2 blocker regardless of file position); the exclusion sits first only for -readability. It would matter for `ruleEngine=Off`, one more reason to avoid it. - -`appsec-configs/appsec-tuned.yaml` wires it up: base-config, the exclusion rule -(must come **before** crs so the ctl takes effect in phase 1), crs, then -vpatch and generic. Deploy it instead of the default: - -``` -cp appsec-rules/crs-route-exclusions.yaml /etc/crowdsec/appsec-rules/ -cp appsec-configs/appsec-tuned.yaml /etc/crowdsec/appsec-configs/ -# point acquis.d/appsec.yaml at selfhostedops/appsec-tuned instead of crowdsecurity/appsec-default -crowdsec -t && systemctl restart crowdsec -``` - -Measured with the tuned config: **0 false positives** across every workflow -above (git clone/push, Forgejo issues and file contents, Outline documents with -an attack-looking code block, ntfy publish, n8n webhooks, and a full -authenticated Vaultwarden register/login/sync/cipher-create), still **27 of 41** -probes blocked on the non-excluded routes. The cost is precise: on an excluded -route (say the Forgejo API) generic SQLi/XSS now passes to the app, while -virtual patching still catches known CVEs there, and on a non-excluded host -(vault, status) the full CRS applies. Latency added over vpatch: about +2 ms -time-to-first-byte per request. RAM measured settled: +9 MB over vpatch after five minutes (vpatch ~184 MB, CRS+exclusions ~193 MB). The ~25-45 MB people cite is a load-time peak, not the steady cost. - -Traps found while tuning: -- `RemoveInBandRuleByName("crowdsecurity/crs")` in a `pre_eval` hook does **not** - disable CRS: the bouncer name does not match the compiled SecLang rules - (measured, globally and per route, SQLi stayed 403). Use `ctl:` inside SecLang. - Docs: https://docs.crowdsec.net/docs/appsec/hooks -- `ctl:ruleEngine=Off` is too broad: it disables virtual patching on the route - too. Use `ctl:ruleRemoveById` for the CRS blocking rules and keep the engine on. -- Every new app or new public path needs its own exclusion. A missed one blocks - a real workflow, and it fails only after login, so it is easy to miss. - -**What this repo runs by default: vpatch + generic** (the `appsec-default` -config, zero false positives, no per-app tuning). The CRS files above are the -measured opt-in for anyone who wants generic coverage and will budget the -per-route tuning. diff --git a/crowdsec/acquis.d/appsec.yaml b/crowdsec/acquis.d/appsec.yaml deleted file mode 100644 index 1080da0..0000000 --- a/crowdsec/acquis.d/appsec.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# episode 12: the AppSec (WAF) endpoint the Caddy bouncer asks on every request. -# Listens on all addresses; ufw only lets the Docker "proxy" network in -# (see crowdsec/README.md). Needs the appsec collections AND the rules listed -# in the README, or crowdsec refuses to start. -appsec_configs: - - crowdsecurity/appsec-default -labels: - type: appsec -listen_addr: 0.0.0.0:7422 -source: appsec diff --git a/crowdsec/acquis.d/caddy.yaml b/crowdsec/acquis.d/caddy.yaml deleted file mode 100644 index 185caca..0000000 --- a/crowdsec/acquis.d/caddy.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# episode 12: CrowdSec reads the same JSON access log fail2ban reads. -# The path is the host side of the ./logs/caddy volume in compose.yml. -filenames: - - /opt/selfhostedops-stack/logs/caddy/access.log -labels: - type: caddy -source: file diff --git a/crowdsec/appsec-configs/appsec-tuned.yaml b/crowdsec/appsec-configs/appsec-tuned.yaml deleted file mode 100644 index 3c04a75..0000000 --- a/crowdsec/appsec-configs/appsec-tuned.yaml +++ /dev/null @@ -1,16 +0,0 @@ -name: selfhostedops/appsec-tuned -default_remediation: ban -# ep13: vpatch + generic (nativ) plus OWASP CRS (SecLang). Die lokale Regel -# crs-route-exclusions entfernt je legitimer Route nur die CRS-Anomalie- -# Blockregeln (ctl:ruleRemoveById), die native vpatch-Abdeckung bleibt dort -# erhalten. Reihenfolge zu crowdsecurity/crs ist egal (Phase 1 vor Phase 2), -# der Uebersicht halber steht die Ausnahme oben. -inband_rules: - - crowdsecurity/base-config - - selfhostedops/crs-route-exclusions - - crowdsecurity/crs - - crowdsecurity/vpatch-* - - crowdsecurity/generic-* -outofband_rules: - - crowdsecurity/experimental-* - - crowdsecurity/appsec-generic-test diff --git a/crowdsec/appsec-rules/crs-route-exclusions.yaml b/crowdsec/appsec-rules/crs-route-exclusions.yaml deleted file mode 100644 index 64789f2..0000000 --- a/crowdsec/appsec-rules/crs-route-exclusions.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: selfhostedops/crs-route-exclusions -# ep13: entfernt je legitimer Route NUR die CRS-Anomalie-Blockregeln (949110 -# inbound, 959100 outbound), sodass das generische CRS dort nicht mehr sperrt. -# Die Coraza-Engine bleibt an (ruleEngine On), deshalb laufen die nativen -# CrowdSec-vpatch-Regeln auf diesen Routen weiter (bekannte CVEs bleiben -# geschuetzt). NICHT ctl:ruleEngine=Off benutzen, das schaltet auch vpatch ab. -# Die Regel muss VOR crowdsecurity/crs stehen (Phase 1). -seclang_rules: - # Forgejo git smart-HTTP (clone/push), binaerer Pack-Body - - | - SecRule REQUEST_HEADERS:Host "@beginsWith git." "id:900101,phase:1,pass,nolog,chain,msg:'ep13 crs-off git smart-http'" - SecRule REQUEST_URI "@rx (git-upload-pack|git-receive-pack|/info/refs\?service=)" "ctl:ruleRemoveById=949110,ctl:ruleRemoveById=959100" - # Forgejo REST-API: Issues, Datei-Inhalte - - | - SecRule REQUEST_HEADERS:Host "@beginsWith git." "id:900102,phase:1,pass,nolog,chain,msg:'ep13 crs-off forgejo api'" - SecRule REQUEST_URI "@beginsWith /api/v1/" "ctl:ruleRemoveById=949110,ctl:ruleRemoveById=959100" - # Outline Dokument-API - - | - SecRule REQUEST_HEADERS:Host "@beginsWith wiki." "id:900103,phase:1,pass,nolog,chain,msg:'ep13 crs-off outline api'" - SecRule REQUEST_URI "@beginsWith /api/" "ctl:ruleRemoveById=949110,ctl:ruleRemoveById=959100" - # ntfy Publish (POST /) - - | - SecRule REQUEST_HEADERS:Host "@beginsWith ntfy." "id:900104,phase:1,pass,nolog,chain,msg:'ep13 crs-off ntfy publish'" - SecRule REQUEST_METHOD "@streq POST" "ctl:ruleRemoveById=949110,ctl:ruleRemoveById=959100" - # n8n Webhook-Pfade - - | - SecRule REQUEST_HEADERS:Host "@beginsWith n8n." "id:900105,phase:1,pass,nolog,chain,msg:'ep13 crs-off n8n webhook'" - SecRule REQUEST_URI "@beginsWith /webhook" "ctl:ruleRemoveById=949110,ctl:ruleRemoveById=959100" diff --git a/crowdsec/firewall-bouncer.yaml b/crowdsec/firewall-bouncer.yaml deleted file mode 100644 index 270bee5..0000000 --- a/crowdsec/firewall-bouncer.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# episode 12: the keys changed in /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml -# (the package writes the rest, including api_url and api_key). -# -# nftables mode instead of the iptables default. The bouncer then owns its -# own tables (ip crowdsec, ip6 crowdsec6) with hooks on input AND forward at -# priority -10, so it drops before Docker's FORWARD rules, and `ufw reload` -# does not touch it. In iptables mode you must add DOCKER-USER to -# iptables_chains, and every `ufw reload` silently removes the rule again. -mode: nftables -nftables_hooks: - - input - - forward