Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions config/haproxy-lf-file-example.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# HAProxy configuration using lf-file templates (no Lua)
# Uses http-request deny for hard blocks (ban) and return for challenges (captcha)

global
log stdout format raw local0

defaults
log global
option httplog
timeout client 1m
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h

frontend test
mode http
bind *:9090

unique-id-format %[uuid()]
unique-id-header X-Unique-ID
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg

http-request set-header X-Crowdsec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
http-request set-header X-Crowdsec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }

# Redirect after successful captcha
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }

# CrowdSec remediation pages via lf-file
# Hard blocks (ban): Use http-request deny - logs show as denial, reinforces security decision
http-request deny deny_status 403 content-type "text/html; charset=utf-8" lf-file /etc/haproxy/errors/403-ban.http if { var(txn.crowdsec.remediation) -m str "ban" } !{ var(txn.crowdsec.contact_us_url) -m found }
http-request deny deny_status 403 content-type "text/html; charset=utf-8" lf-file /etc/haproxy/errors/403-ban-contact.http if { var(txn.crowdsec.remediation) -m str "ban" } { var(txn.crowdsec.contact_us_url) -m found }

# Challenge flow (captcha): Use http-request return - treated as additional auth step
http-request return status 403 content-type "text/html; charset=utf-8" lf-file /etc/haproxy/errors/403-captcha.http if { var(txn.crowdsec.remediation) -m str "captcha" }

# Captcha cookie management
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }

use_backend test_backend

backend test_backend
mode http
server s1 whoami:2020

backend crowdsec-spoa
mode tcp
balance roundrobin
server s2 spoa:9000
server s3 spoa:9001
33 changes: 12 additions & 21 deletions config/haproxy-upstreamproxy.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# https://www.haproxy.com/documentation/hapee/latest/onepage/#home
# HAProxy configuration example for deployment behind upstream proxy
# This config extracts real client IP from X-Real-IP header (set by nginx, Cloudflare, etc.)
# HAProxy configuration for deployment behind upstream proxy (nginx, Cloudflare, etc.)

global
log stdout format raw local0
Expand All @@ -13,11 +12,11 @@ defaults
log global
option httplog
timeout client 1m
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h

frontend test
mode http
Expand All @@ -26,36 +25,28 @@ frontend test
unique-id-format %[uuid()]
unique-id-header X-Unique-ID

# IMPORTANT: When behind a reverse proxy, use req.hdr_ip() in SPOE config
# to extract real client IP from headers (X-Real-IP, X-Forwarded-For, CF-Connecting-IP)
# See crowdsec-upstreamproxy.cfg for the SPOE configuration example
# When behind a reverse proxy, configure SPOE to extract real client IP
# from headers (X-Real-IP, X-Forwarded-For, CF-Connecting-IP)
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg

# Debug headers to verify IP extraction
http-request set-header X-Debug-Direct-IP %[src]

## If you dont want to render any content, you can use the following line
# Optional: reject without rendering content
# tcp-request content reject if !{ var(txn.crowdsec.remediation) -m str "allow" }

## Drop ban requests before http handler is called
# tcp-request content reject if { var(txn.crowdsec.remediation) -m str "ban" }

## Set a custom header on the request for upstream services to use
http-request set-header X-Crowdsec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
## Set a custom header on the request for upstream services to use
http-request set-header X-Crowdsec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }

## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
# Redirect after successful captcha
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }

## Call lua script only for ban and captcha remediations (performance optimization)
# Lua-based remediation pages
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "captcha" }
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "ban" }

## Handle captcha cookie management via HAProxy (new approach)
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
# Captcha cookie management
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }

use_backend test_backend
Expand Down
24 changes: 9 additions & 15 deletions config/haproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ defaults
log global
option httplog
timeout client 1m
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h

frontend test
mode http
Expand All @@ -24,28 +24,22 @@ frontend test
unique-id-header X-Unique-ID
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg

## If you dont want to render any content, you can use the following line
# Optional: reject without rendering content
# tcp-request content reject if !{ var(txn.crowdsec.remediation) -m str "allow" }

## Drop ban requests before http handler is called
# tcp-request content reject if { var(txn.crowdsec.remediation) -m str "ban" }

## Set a custom header on the request for upstream services to use
http-request set-header X-Crowdsec-Remediation %[var(txn.crowdsec.remediation)] if { var(txn.crowdsec.remediation) -m found }
## Set a custom header on the request for upstream services to use
http-request set-header X-Crowdsec-IsoCode %[var(txn.crowdsec.isocode)] if { var(txn.crowdsec.isocode) -m found }

## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
# Redirect after successful captcha
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }

## Call lua script only for ban and captcha remediations (performance optimization)
# Lua-based remediation pages
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "captcha" }
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "ban" }

## Handle captcha cookie management via HAProxy (new approach)
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
# Captcha cookie management
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_status) -m found } { var(txn.crowdsec.captcha_cookie) -m found }
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
http-after-response set-header Set-Cookie %[var(txn.crowdsec.captcha_cookie)] if { var(txn.crowdsec.captcha_cookie) -m found } !{ var(txn.crowdsec.captcha_status) -m found }

use_backend test_backend
Expand Down
85 changes: 85 additions & 0 deletions docker-compose.lf-file-example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
version: '3.8'

services:
# CrowdSec SPOA bouncer (built locally)
spoa:
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
depends_on:
- crowdsec
volumes:
- sockets:/run/
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
- geodb:/var/lib/crowdsec/data/
- ./config/crowdsec-spoa-bouncer.yaml.local:/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml.local
networks:
crowdsec:
ipv4_address: 10.5.5.254
deploy:
resources:
limits:
cpus: "4.0"
memory: 250M

# HAProxy with lf-file error templates (no Lua)
haproxy:
image: haproxy:3.0
ports:
- "9090:9090"
volumes:
- ./config/haproxy-lf-file-example.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
- ./config/crowdsec.cfg:/etc/haproxy/crowdsec.cfg:ro
# Mount the lf-file templates
- ./examples/haproxy/errorfiles-lf/:/etc/haproxy/errors/:ro
- sockets:/run/
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
depends_on:
- crowdsec
- spoa
- whoami
restart: unless-stopped
networks:
- crowdsec

# Test backend (whoami container)
whoami:
image: traefik/whoami:latest
networks:
- crowdsec
command:
- --port=2020

# CrowdSec engine
crowdsec:
image: crowdsecurity/crowdsec:latest
restart: unless-stopped
environment:
- BOUNCER_KEY_SPOA=+4iYgItcalc9+0tWrvrj9R6Wded/W1IRwRtNmcWR9Ws
- DISABLE_ONLINE_API=true
- CROWDSEC_BYPASS_DB_VOLUME_CHECK=true
volumes:
- geodb:/staging/var/lib/crowdsec/data/
networks:
- crowdsec

volumes:
lua:
driver: local
sockets:
driver: local
templates:
driver: local
geodb:
driver: local

networks:
crowdsec:
driver: bridge
ipam:
driver: default
config:
- subnet: "10.5.5.0/24"
105 changes: 105 additions & 0 deletions examples/haproxy/errorfiles-lf/403-ban-contact.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Access Denied</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #1a1a1a;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #e5e5e5;
padding: 20px;
}
.container {
background: #2a2a2a;
border-radius: 4px;
padding: 40px;
max-width: 500px;
width: 100%%;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
h1 {
font-size: 20px;
font-weight: 400;
margin-bottom: 20px;
text-align: center;
color: #e5e5e5;
}
.info-text {
font-size: 14px;
color: #b3b3b3;
text-align: center;
margin-top: 20px;
line-height: 1.5;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #404040;
font-size: 12px;
color: #999;
text-align: center;
}
.footer-brand {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-bottom: 10px;
}
.footer-brand a {
color: #e5e5e5;
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
}
.footer-brand a:hover {
text-decoration: underline;
}
.logo {
width: 20px;
height: 20px;
display: inline-block;
}
.unique-id {
font-size: 11px;
color: #999;
margin-top: 8px;
}
</style>
</head>
<body>
<div class="container">
<h1>Access Denied</h1>
<p class="info-text">Your request has been blocked.</p>
<p class="info-text" style="text-align: center;"><a href="%[var(txn.crowdsec.contact_us_url)]" style="display: inline-block; margin-top: 20px; padding: 10px 20px; background: #f38020; color: white; text-decoration: none; border-radius: 4px; font-size: 14px;">Contact Us</a></p>
<div class="footer">
<div class="footer-brand">
<span>Protected by</span>
<a href="https://crowdsec.net" target="_blank">
<svg class="logo" viewBox="0 0 254.4 253.2" fill="currentColor">
<defs>
<clipPath id="a"><path d="M0 52h84v201.2H0zm0 0" /></clipPath>
<clipPath id="b"><path d="M170 52h84.4v201.2H170zm0 0" /></clipPath>
</defs>
<path d="M59.3 128.4c1.4 2.3 2.5 4.6 3.4 7-1-4.1-2.3-8.1-4.3-12-3.1-6-7.8-5.8-10.7 0-2 4-3.2 8-4.3 12.1 1-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M207.8 128.4a42.9 42.9 0 013.4 7c-1-4.1-2.3-8.1-4.3-12-3.2-6-7.8-5.8-10.7 0-2 4-3.3 8-4.3 12.1.9-2.4 2-4.8 3.4-7.1 3.4-5.8 8.8-6 12.5 0M134.6 92.9c2 3.5 3.6 7 4.8 10.7-1.3-5.4-3-10.6-5.6-15.7-4-7.5-9.7-7.2-13.3 0a75.4 75.4 0 00-5.6 16c1.2-3.8 2.7-7.4 4.7-11 4.1-7.2 10.6-7.5 15 0M43.8 136.8c.9 4.6 3.7 8.3 7.3 9.2 0 2.7 0 5.5.2 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4M192.4 136.8c.8 4.6 3.7 8.3 7.2 9.2 0 2.7 0 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3.9 2.2 1.2 0 .6-3 .7-6.3 1-9.6.2-2.7.3-5.5.2-8.2 3.6-1 6.4-4.6 7.3-9.2a17.8 17.8 0 01-9.1 2.4c-3.4 0-6.6-1-9.1-2.4M138.3 104.6c-3.1 1.9-7 3-11.3 3-4.3 0-8.2-1.1-11.3-3 1 5.8 4.5 10.3 9 11.5 0 3.4 0 6.8.3 10.2.4 4.1.5 8.2 1.2 12 .4 2.9 1.2 2.7 1.6 0 .7-3.8.8-7.9 1.2-12 .3-3.4.3-6.8.3-10.2 4.5-1.2 8-5.7 9-11.5"/>
<path d="M51 146c0 2.7.1 5.5.3 8.2.3 3.3.4 6.6 1 9.6.3 2.3 1 2.2 1.3 0 .5-3 .6-6.3 1-9.6l.2-8.2c3.5-1 6.4-4.6 7.2-9.2a17.8 17.8 0 01-9 2.4c-3.5 0-6.6-1-9.2-2.4.9 4.6 3.7 8.3 7.3 9.2M143.9 105c-1.9-.4-3.5-1.2-4.9-2.3 1.4 5.6 2.5 11.3 4 17 1.2 5 2 10 2.4 15 .6 7.8-4.5 14.5-10.9 14.5h-15c-6.4 0-11.5-6.7-11-14.5.5-5 1.3-10 2.6-15 1.3-5.3 2.3-10.5 3.6-15.7-2.2 1.2-4.8 1.9-7.7 2-4.7.1-9.4-.3-14-1-4-.4-6.7-3-8-6.7-1.3-3.4-2-7-3.3-10.4-.5-1.5-1.6-2.8-2.4-4.2-.4-.6-.8-1.2-.9-1.8v-7.8a77 77 0 0124.5-3c6.1 0 12 1 17.8 3.2 4.7 1.7 9.7 1.8 14.4 0 9-3.4 18.2-3.8 27.5-3 4.9.5 9.8 1.6 14.8 2.4v8.2c0 .6-.3 1.5-.7 1.7-2 .9-2.2 2.7-2.7 4.5-.9 3.2-1.8 6.4-2.9 9.5a11 11 0 01-8.8 7.7 40.6 40.6 0 01-18.4-.2m29.4 80.6c-3.2-26.8-6.4-50-8.9-60.7a14.3 14.3 0 0014.1-14h.4a9 9 0 005.6-16.5 14.3 14.3 0 00-3.7-27.2 9 9 0 00-6.9-14.6c2.4-1.1 4.5-3 5.8-5 3.4-5.3 4-29-8-44.4-5-6.3-9.8-2.5-10 1.8-1 13.2-1.1 23-4.5 34.3a9 9 0 00-16-4.1 14.3 14.3 0 00-28.4 0 9 9 0 00-16 4.1c-3.4-11.2-3.5-21.1-4.4-34.3-.3-4.3-5.2-8-10-1.8-12 15.3-11.5 39-8.1 44.4 1.3 2 3.4 3.9 5.8 5a9 9 0 00-7 14.6 14.3 14.3 0 00-3.6 27.2A9 9 0 0075 111h.5a14.5 14.5 0 0014.3 14c-4 17.2-10 66.3-15 111.3l-1.3 13.4a1656.4 1656.4 0 01106.6 0l-1.4-12.7-5.4-51.3"/>
<g clip-path="url(#a)"><path d="M83.5 136.6l-2.3.7c-5 1-9.8 1-14.8-.2-1.4-.3-2.7-1-3.8-1.9l3.1 13.7c1 4 1.7 8 2 12 .5 6.3-3.6 11.6-8.7 11.6H46.9c-5.1 0-9.2-5.3-8.7-11.6.3-4 1-8 2-12 1-4.2 1.8-8.5 2.9-12.6-1.8 1-3.9 1.5-6.3 1.6a71 71 0 01-11.1-.7 7.7 7.7 0 01-6.5-5.5c-1-2.7-1.6-5.6-2.6-8.3-.4-1.2-1.3-2.3-2-3.4-.2-.4-.6-1-.6-1.4v-6.3c6.4-2 13-2.6 19.6-2.5 4.9.1 9.6 1 14.2 2.6 3.9 1.4 7.9 1.5 11.7 0 1.8-.7 3.6-1.2 5.5-1.6a13 13 0 01-1.6-15.5A18.3 18.3 0 0159 73.1a11.5 11.5 0 00-17.4 8.1 7.2 7.2 0 00-12.9 3.3c-2.7-9-2.8-17-3.6-27.5-.2-3.4-4-6.5-8-1.4C7.5 67.8 7.9 86.9 10.6 91c1.1 1.7 2.8 3.1 4.7 4a7.2 7.2 0 00-5.6 11.7 11.5 11.5 0 00-2.9 21.9 7.2 7.2 0 004.5 13.2h.3c0 .6 0 1.1.2 1.7.9 5.4 5.6 9.5 11.3 9.5A1177.2 1177.2 0 0010 253.2c18.1-1.5 38.1-2.6 59.5-3.4.4-4.6.8-9.3 1.4-14 1.2-11.6 3.3-30.5 5.7-49.7 2.2-18 4.7-36.3 7-49.5"/></g>
<g clip-path="url(#b)"><path d="M254.4 118.2c0-5.8-4.2-10.5-9.7-11.4a7.2 7.2 0 00-5.6-11.7c2-.9 3.6-2.3 4.7-4 2.7-4.2 3.1-23.3-6.5-35.5-4-5.1-7.8-2-8 1.4-.8 10.5-.9 18.5-3.6 27.5a7.2 7.2 0 00-12.8-3.3 11.5 11.5 0 00-17.8-7.9 18.4 18.4 0 01-4.5 22 13 13 0 01-1.3 15.2c2.4.5 4.8 1 7.1 2 3.8 1.3 7.8 1.4 11.6 0 7.2-2.8 14.6-3 22-2.4 4 .4 7.9 1.2 12 1.9l-.1 6.6c0 .5-.2 1.2-.5 1.3-1.7.7-1.8 2.2-2.2 3.7l-2.3 7.6a8.8 8.8 0 01-7 6.1c-5 1-10 1-14.9-.2-1.5-.3-2.8-1-3.9-1.9 1.2 4.5 2 9.1 3.2 13.7 1 4 1.6 8 2 12 .4 6.3-3.6 11.6-8.8 11.6h-12c-5.2 0-9.3-5.3-8.8-11.6.4-4 1-8 2-12 1-4.2 1.9-8.5 3-12.6-1.8 1-4 1.5-6.3 1.6-3.7 0-7.5-.3-11.2-.7a7.7 7.7 0 01-3.7-1.5c3.1 18.4 7.1 51.2 12.5 100.9l.6 5.3.8 7.9c21.4.7 41.5 1.9 59.7 3.4L243 243l-4.4-41.2a606 606 0 00-7-48.7 11.5 11.5 0 0011.2-11.2h.4a7.2 7.2 0 004.4-13.2c4-1.8 6.8-5.8 6.8-10.5"/></g>
<path d="M180 249.6h.4a6946 6946 0 00-7.1-63.9l5.4 51.3 1.4 12.6M164.4 125c2.5 10.7 5.7 33.9 8.9 60.7a570.9 570.9 0 00-8.9-60.7M74.8 236.3l-1.4 13.4 1.4-13.4"/>
</svg>
<span>CrowdSec</span>
</a>
</div>
<div class="unique-id">Request ID: %[unique-id]</div>
</div>
</div>
</body>
</html>
Loading