Skip to content

Commit fbbf0a7

Browse files
committed
docs: CrowdSec reputation example and peers-mesh HAProxy config
haproxy-reputation.cfg is a standalone alternative config with the peers section, reputation stick-tables and the block/min-level policy; the crowdsec example overlays a CrowdSec container onto the regular single-container docker setup with the reputation service embedded in the agent.
1 parent 9c3aeb6 commit fbbf0a7

5 files changed

Lines changed: 266 additions & 0 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,18 @@ Customize the maps under [`examples/haproxy/maps/`](examples/haproxy/maps/) and
7474
Map keys are lowercase literal substrings without whitespace. User-Agent headers are trivial to spoof, so this policy is
7575
traffic shaping only; it must not protect authenticated or otherwise sensitive endpoints.
7676

77+
### Optional IP reputation (CrowdSec)
78+
79+
Berghain ships a reputation service that acts as a [CrowdSec](https://www.crowdsec.net/) bouncer and pushes
80+
decisions (plus static feeds such as Tor exit nodes) into HAProxy stick-tables **live over the peers protocol** —
81+
no reloads, and Berghain stays stateless. Bans are silent-dropped, captcha decisions raise the minimum challenge
82+
level, and per-decision durations are honored via timed stick-table entries.
83+
84+
It runs either embedded in the agent (a `reputation:` section in the spop config) or standalone
85+
(`cmd/feedupdater`) for setups that scale the feed separately; the daemon behaves as a first-class peer in
86+
meshes with multiple HAProxy instances. See [`examples/crowdsec/`](examples/crowdsec/) and
87+
[`examples/haproxy/haproxy-reputation.cfg`](examples/haproxy/haproxy-reputation.cfg).
88+
7789
## Running with Docker
7890

7991
To run the project using Docker, follow these steps:

examples/crowdsec/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# CrowdSec IP reputation (optional)
2+
3+
Berghain can act as a [CrowdSec](https://www.crowdsec.net/) *bouncer*: its
4+
reputation service polls the CrowdSec local API decision stream and pushes
5+
every decision into HAProxy stick-tables **live over the peers protocol** — no
6+
map files, no reloads, and Berghain itself stays stateless. Decisions map to
7+
the `gpt0` tag consumed by
8+
[`examples/haproxy/haproxy-reputation.cfg`](../haproxy/haproxy-reputation.cfg):
9+
10+
| CrowdSec decision | gpt0 | HAProxy behaviour |
11+
|-------------------|------|--------------------------------------|
12+
| `ban` | 1 | `silent-drop` |
13+
| `captcha` | 3 | minimum Berghain challenge level 3 |
14+
| anything else | 1 | fail closed to a ban |
15+
16+
Per-decision durations are honored: entries are pushed as timed stick-table
17+
updates and expire in HAProxy exactly when the decision does, even if the
18+
service is down at that moment. The same service also challenges Tor exit
19+
nodes (static feed) unless `tor_exits: false` is set.
20+
21+
## Running it
22+
23+
The service runs **embedded** in the Berghain agent (`reputation:` section in
24+
the spop config), so the docker setup stays two containers: the existing
25+
haproxy+berghain container and CrowdSec.
26+
27+
1. Start CrowdSec once so you can register the bouncer:
28+
29+
```sh
30+
docker compose -f docker-compose.yml -f examples/crowdsec/docker-compose.crowdsec.yml up -d crowdsec
31+
docker compose -f docker-compose.yml -f examples/crowdsec/docker-compose.crowdsec.yml \
32+
exec crowdsec cscli bouncers add berghain
33+
```
34+
35+
2. Export the printed key and start the rest:
36+
37+
```sh
38+
export CROWDSEC_API_KEY=<key from step 1>
39+
docker compose -f docker-compose.yml -f examples/crowdsec/docker-compose.crowdsec.yml up
40+
```
41+
42+
3. Try it — add a decision and watch the stick-table:
43+
44+
```sh
45+
docker compose -f docker-compose.yml -f examples/crowdsec/docker-compose.crowdsec.yml \
46+
exec crowdsec cscli decisions add --ip 203.0.113.7 --type ban --duration 5m
47+
```
48+
49+
Within the poll interval (10s by default) requests from that address are
50+
silent-dropped; `cscli decisions delete --ip 203.0.113.7` lifts it again
51+
within one poll.
52+
53+
CrowdSec only produces decisions when it can *see* traffic (or when another
54+
machine in your CrowdSec network reports it): feed it your HAProxy logs via an
55+
acquisition file, or rely on the community blocklist that comes with console
56+
enrollment. Both are standard CrowdSec configuration — see their
57+
[HAProxy collection](https://app.crowdsec.net/hub/author/crowdsecurity/collections/haproxy).
58+
59+
## Standalone mode
60+
61+
Deployments that scale HAProxy and the feed separately can run the exact same
62+
service as its own daemon instead of embedding it:
63+
64+
```sh
65+
go run ./cmd/feedupdater \
66+
-peer-listen 0.0.0.0:10001 \
67+
-crowdsec-url http://crowdsec:8080 # key via CROWDSEC_API_KEY
68+
```
69+
70+
Every HAProxy in the peers mesh then lists `berghain_feed` once and they all
71+
learn the same tables; the daemon copes fine with being one peer among many
72+
(it validates handshakes and acknowledges the updates the other peers teach).

examples/crowdsec/config.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Berghain config for the CrowdSec example: the default challenge levels plus
2+
# the embedded IP reputation service. The API key comes from the
3+
# CROWDSEC_API_KEY environment variable (see examples/crowdsec/README.md).
4+
secret: JMal0XJRROOMsMdPqggG2tR56CTkpgN3r47GgUN/WSQ=
5+
6+
reputation:
7+
peer_listen: 127.0.0.1:10001
8+
local_peer: berghain_feed
9+
crowdsec:
10+
url: http://crowdsec:8080
11+
12+
default:
13+
levels:
14+
- duration: 24h
15+
type: none
16+
- duration: 30m
17+
type: pow
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# CrowdSec IP reputation overlay (issue #82).
2+
#
3+
# Runs CrowdSec next to the regular single-container Berghain setup and turns
4+
# on Berghain's embedded reputation service: it polls the CrowdSec LAPI
5+
# decision stream as a bouncer and pushes decisions into HAProxy stick-tables
6+
# over the peers protocol. See examples/crowdsec/README.md for the key setup.
7+
#
8+
# docker compose -f docker-compose.yml -f examples/crowdsec/docker-compose.crowdsec.yml up
9+
10+
services:
11+
berghain-haproxy:
12+
volumes:
13+
- ./examples/haproxy/haproxy-reputation.cfg:/app/haproxy.cfg
14+
- ./examples/haproxy/berghain.cfg:/app/examples/haproxy/berghain.cfg
15+
- ./examples/crowdsec/config.yaml:/app/config.yaml
16+
# -L names this HAProxy's own entry in the peers section.
17+
command: sh -c "haproxy -L haproxy_local -f haproxy.cfg & ./berghain -config config.yaml"
18+
environment:
19+
# Register with: docker compose exec crowdsec cscli bouncers add berghain
20+
- CROWDSEC_API_KEY=${CROWDSEC_API_KEY:?run cscli bouncers add berghain and export the key}
21+
depends_on:
22+
- crowdsec
23+
24+
# Prefer the standalone daemon instead of embedded mode? Disable the
25+
# reputation section in config.yaml and run cmd/feedupdater as its own
26+
# service with the same flags-equivalent settings.
27+
28+
crowdsec:
29+
image: crowdsecurity/crowdsec:latest
30+
environment:
31+
- COLLECTIONS=crowdsecurity/base-http-scenarios
32+
volumes:
33+
- crowdsec-db:/var/lib/crowdsec/data
34+
- crowdsec-config:/etc/crowdsec
35+
restart: unless-stopped
36+
37+
volumes:
38+
crowdsec-db:
39+
crowdsec-config:
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# Optional live IP-reputation policy for Berghain (issue #82).
2+
#
3+
# This is a complete alternative to examples/haproxy/haproxy.cfg. Berghain's
4+
# reputation service (embedded in cmd/spop via the `reputation:` config
5+
# section, or standalone as cmd/feedupdater) joins the peers mesh below as
6+
# "berghain_feed" and pushes per-IP reputation into the st_reputation
7+
# stick-tables live — no map files, no reloads. The gpt0 tag encodes the
8+
# action: 1 = block (silent-drop), >= 2 = minimum challenge level.
9+
#
10+
# HAProxy must know which peer entry is itself: run it with
11+
# haproxy -L haproxy_local -f examples/haproxy/haproxy-reputation.cfg
12+
#
13+
# The mesh survives more peers: add further HAProxy instances to the peers
14+
# section and they all learn the same reputation tables.
15+
#
16+
# Validate from the repository root with:
17+
# haproxy -c -L haproxy_local -f examples/haproxy/haproxy-reputation.cfg
18+
19+
global
20+
log stdout format raw local0
21+
22+
defaults
23+
mode http
24+
log global
25+
timeout client 5s
26+
timeout server 5s
27+
timeout connect 5s
28+
option httplog
29+
30+
listen stats
31+
bind 127.0.0.1:8000
32+
stats enable
33+
stats uri /
34+
stats refresh 15s
35+
36+
# The reputation service listens as the "berghain_feed" peer. HAProxy dials it
37+
# on startup to resync and keeps the session for live pushes.
38+
peers berghain
39+
peer haproxy_local 127.0.0.1:10000
40+
peer berghain_feed 127.0.0.1:10001
41+
42+
frontend test
43+
bind *:8080
44+
log-format "%ci:%cp\ [%t]\ %ft\ %b/%s\ %Th/%Ti/%TR/%Tq/%Tw/%Tc/%Tr/%Tt\ %ST\ %B\ %CC\ %CS\ %tsc\ %ac/%fc/%bc/%sc/%rc\ %sq/%bq\ %hr\ %hs\ %{+Q}r\ %ID spoa-error:\ %[var(txn.berghain.error)]"
45+
46+
acl berghain_path path /cdn-cgi/challenge-platform/challenge
47+
48+
# HAProxy issues the initial support ID; continuation requests carry it in their body.
49+
http-request set-var-fmt(txn.berghain.session) "bh@%[uuid()]" if berghain_path METH_GET
50+
51+
# Individual-IP reputation is pushed live into the st_reputation tables by
52+
# the Berghain reputation service over the peers protocol. Stick-tables
53+
# key on exact addresses, so CIDR feeds (e.g. the generated
54+
# maps/cloudflare-ips.lst) would be matched with `src -f <file>` instead.
55+
acl src_is_v4 src -m ip 0.0.0.0/0
56+
http-request set-var(req.rep) src,table_gpt0(st_reputation_v4) if src_is_v4
57+
http-request set-var(req.rep) src,table_gpt0(st_reputation_v6) if !src_is_v4
58+
59+
# Reputation tag 1 (e.g. a CrowdSec ban): drop without an answer. Keep the
60+
# Berghain endpoint out so a ban lifted mid-challenge cannot wedge clients.
61+
http-request silent-drop if { var(req.rep) -m int eq 1 }
62+
63+
http-request track-sc1 src table st_src
64+
65+
filter spoe engine berghain config examples/haproxy/berghain.cfg
66+
67+
# Base challenge level from request rate; rules are increasing so the
68+
# highest match wins.
69+
http-request set-var(req.berghain.level) int(1) if { sc1_http_req_rate gt 5 }
70+
http-request set-var(req.berghain.level) int(2) if { sc1_http_req_rate gt 10 }
71+
http-request set-var(req.berghain.level) int(3) if { sc1_http_req_rate gt 15 }
72+
73+
# Flagged sources (reputation tag >= 2, e.g. a CrowdSec captcha decision or
74+
# a Tor exit) raise the MINIMUM level — only ever upward, via the ge guard.
75+
http-request set-var(req.berghain.level) int(3) if { var(req.rep) -m int ge 2 } !{ var(req.berghain.level) -m int ge 3 }
76+
77+
acl berghain_active var(req.berghain.level) -m found
78+
79+
http-request send-spoe-group berghain validate if !berghain_path berghain_active
80+
http-request return status 501 if { var(txn.berghain.error) -m found }
81+
82+
acl berghain_valid var(txn.berghain.valid) -m bool
83+
acl is_ssl ssl_fc
84+
85+
http-request return status 403 content-type "text/html" file "web/dist/default/index.html" if !berghain_valid !berghain_path berghain_active !is_ssl
86+
http-request return status 403 content-type "text/html" file "web/dist/native-crypto/index.html" if !berghain_valid !berghain_path berghain_active is_ssl
87+
http-request wait-for-body time 5s if berghain_path METH_POST
88+
use_backend berghain_http if berghain_path
89+
90+
default_backend app_backend
91+
92+
backend st_src
93+
stick-table type ipv6 size 1m expire 15m store http_req_rate(10s)
94+
95+
# The reputation tables. Names, key types and the expire value must match the
96+
# reputation service configuration (table_v4/table_v6/table_expiry).
97+
backend st_reputation_v4
98+
stick-table type ip size 1m expire 24h store gpt0 peers berghain
99+
100+
backend st_reputation_v6
101+
stick-table type ipv6 size 1m expire 24h store gpt0 peers berghain
102+
103+
backend app_backend
104+
mode http
105+
http-request return status 200 content-type "text/plain" string "Hello World!"
106+
107+
backend berghain_http
108+
mode http
109+
filter spoe engine berghain_challenge config examples/haproxy/berghain.cfg
110+
111+
acl is_challenge_path path /cdn-cgi/challenge-platform/challenge
112+
113+
http-request send-spoe-group berghain_challenge challenge if is_challenge_path
114+
http-request return status 501 if { var(txn.berghain.error) -m found }
115+
116+
acl has_token var(txn.berghain.token) -m found
117+
118+
http-after-response add-header set-cookie "berghain=%[var(txn.berghain.token)]; %[var(txn.berghain.domain)] path=/;" if has_token
119+
http-request return status 200 content-type "application/json" lf-string "%[var(txn.berghain.response)]" if is_challenge_path
120+
121+
http-request return status 404
122+
123+
backend berghain_spop
124+
mode tcp
125+
option spop-check
126+
server localhost unix@./spop.sock check

0 commit comments

Comments
 (0)