Skip to content
Open
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
28 changes: 0 additions & 28 deletions config/crowdsec-upstreamproxy.cfg

This file was deleted.

33 changes: 24 additions & 9 deletions config/crowdsec.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# /etc/haproxy/crowdsec.cfg
# SPOE configuration for CrowdSec HAProxy bouncer
# Used for both standard and upstream proxy deployments
# IP extraction is handled by HAProxy ACLs (see haproxy-upstreamproxy.cfg for upstream proxy setup)
[crowdsec]
spoe-agent crowdsec-agent
messages crowdsec-ip crowdsec-http
messages crowdsec-tcp
groups crowdsec-http-body crowdsec-http-no-body

option var-prefix crowdsec
option set-on-error error
Expand All @@ -11,13 +15,24 @@ spoe-agent crowdsec-agent
use-backend crowdsec-spoa
log global

## This message is used to customise the remediation from crowdsec-ip based on the host header
## src-ip is included as fallback in case crowdsec-ip message didn't fire
spoe-message crowdsec-http
args remediation=var(txn.crowdsec.remediation) crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) id=unique-id host=hdr(Host) method=method path=path query=query version=req.ver headers=req.hdrs body=req.body url=url ssl=ssl_fc src-ip=src src-port=src_port
event on-frontend-http-request

## This message should be the first to trigger in the chain
spoe-message crowdsec-ip
## TCP/IP level check - runs early to check IP remediation
## Uses event directive to trigger on each new client session (not sent as a group)
spoe-message crowdsec-tcp
args id=unique-id src-ip=src src-port=src_port
event on-client-session

## HTTP message with body - used when body size is within limit for AppSec
spoe-message crowdsec-http-body
args remediation=var(txn.crowdsec.remediation) crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) id=unique-id host=hdr(Host) method=method path=path query=query version=req.ver headers=req.hdrs body=req.body url=url ssl=ssl_fc src-ip=src src-port=src_port

## HTTP message without body - used when body is too large or not needed
spoe-message crowdsec-http-no-body
args remediation=var(txn.crowdsec.remediation) crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) id=unique-id host=hdr(Host) method=method path=path query=query version=req.ver headers=req.hdrs url=url ssl=ssl_fc src-ip=src src-port=src_port

## Group for HTTP message with body - used when body size is within limit for AppSec
spoe-group crowdsec-http-body
messages crowdsec-http-body

## Group for HTTP message without body - used when body is too large or not needed
spoe-group crowdsec-http-no-body
messages crowdsec-http-no-body
27 changes: 23 additions & 4 deletions config/haproxy-upstreamproxy.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,20 @@ 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
# Extract real client IP from proxy headers (runs before SPOE groups)
# This allows SPOE groups to use src-ip=src which will have the correct IP
# Priority: X-Real-IP > CF-Connecting-IP > X-Forwarded-For > direct src
http-request set-src hdr_ip(X-Real-IP) if { req.hdr(X-Real-IP) -m found }
http-request set-src hdr_ip(CF-Connecting-IP) if { req.hdr(CF-Connecting-IP) -m found } !{ req.hdr(X-Real-IP) -m found }
http-request set-src hdr_ip(X-Forwarded-For) if { req.hdr(X-Forwarded-For) -m found } !{ req.hdr(X-Real-IP) -m found } !{ req.hdr(CF-Connecting-IP) -m found }

filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg

# ACL for body size limit (100000 bytes = ~100KB) - adjust here to change limit globally
# Note spop protocol has limitations on the size of the message, so altering this value will not ensure the whole
# body is sent for processing but should be enough to prevent overwhelming the SPOA bouncer.
acl body_within_limit req.body_size -m int le 100000

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

Expand All @@ -41,6 +50,15 @@ frontend test
## Drop ban requests before http handler is called
# tcp-request content reject if { var(txn.crowdsec.remediation) -m str "ban" }

# Send HTTP group conditionally based on body size
# Only the HTTP handler is used in upstream proxy mode to ensure the real client IP (from headers) is checked.
# The TCP handler still runs via on-client-session event, but checks the proxy IP before http-request set-src
# extracts the real IP from headers, so HTTP handler always re-checks with the correct IP.
# Send group with body when body size <= limit (or no body present)
http-request send-spoe-group crowdsec crowdsec-http-body if body_within_limit || !{ req.body_size -m found }
# Send group without body when body exists and size > limit
http-request send-spoe-group crowdsec crowdsec-http-no-body if !body_within_limit { req.body_size -m found }

## 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
Expand All @@ -67,5 +85,6 @@ backend test_backend

backend crowdsec-spoa
mode tcp
balance roundrobin
timeout connect 2s
timeout server 60s
server s2 spoa:9000
18 changes: 16 additions & 2 deletions config/haproxy.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# https://www.haproxy.com/documentation/hapee/latest/onepage/#home
global
log stdout format raw local0
tune.bufsize 65536 # 64KB - increased for WAF body inspection
lua-prepend-path /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/?.lua
lua-load /usr/lib/crowdsec-haproxy-spoa-bouncer/lua/crowdsec.lua
setenv CROWDSEC_BAN_TEMPLATE_PATH /var/lib/crowdsec-haproxy-spoa-bouncer/html/ban.html
Expand All @@ -25,12 +26,24 @@ 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
# ACL for body size limit (100000 bytes = ~100KB) - adjust here to change limit globally
# Note spop protocol has limitations on the size of the message, so altering this value will not ensure the whole
# body is sent for processing but should be enough to prevent overwhelming the SPOA bouncer.
acl body_within_limit req.body_size -m int le 100000

## If you don't want to render any content, you can use the following line
# 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" }

# Send HTTP group conditionally based on body size
# TCP handler already checked IP (triggered on client session), HTTP handler will use that remediation
# Send group with body when body size <= limit (or no body present)
http-request send-spoe-group crowdsec crowdsec-http-body if body_within_limit || !{ req.body_size -m found }
# Send group without body when body exists and size > limit
http-request send-spoe-group crowdsec crowdsec-http-no-body if !body_within_limit { req.body_size -m found }

## 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
Expand All @@ -57,5 +70,6 @@ backend test_backend

backend crowdsec-spoa
mode tcp
balance roundrobin
timeout connect 2s
timeout server 60s
server s2 spoa:9000
4 changes: 2 additions & 2 deletions docker-compose.proxy-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ services:
image: haproxy:2.9.7-alpine
volumes:
- ./config/haproxy-upstreamproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
- ./config/crowdsec-upstreamproxy.cfg:/etc/haproxy/crowdsec.cfg
- ./config/crowdsec.cfg:/etc/haproxy/crowdsec.cfg
- sockets:/run/
- templates:/var/lib/crowdsec-haproxy-spoa-bouncer/html/
- lua:/usr/lib/crowdsec-haproxy-spoa-bouncer/lua/
Expand All @@ -51,7 +51,7 @@ services:
volumes:
- ./config/nginx-proxy.conf:/etc/nginx/nginx.conf:ro
ports:
- "8080:80"
- "9090:80"
depends_on:
- haproxy
networks:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ require (
github.com/crowdsecurity/crowdsec v1.7.3
github.com/crowdsecurity/go-cs-bouncer v0.0.19
github.com/crowdsecurity/go-cs-lib v0.0.23
github.com/dropmorepackets/haproxy-go v0.0.7
github.com/gaissmai/bart v0.25.0
github.com/google/uuid v1.6.0
github.com/negasus/haproxy-spoe-go v1.0.7
github.com/oschwald/geoip2-golang/v2 v2.0.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dropmorepackets/haproxy-go v0.0.7 h1:atXkB0MSRBZrAgpq+Vj/E4KysQ4CiI0O5QGUr+HvfTw=
github.com/dropmorepackets/haproxy-go v0.0.7/go.mod h1:4a2AmmVjvg2zPNdizGZrMN8ZSUpj90U43VlcdbOIBnU=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/expr-lang/expr v1.17.5 h1:i1WrMvcdLF249nSNlpQZN1S6NXuW9WaOfF5tPi3aw3k=
Expand Down Expand Up @@ -79,8 +81,6 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/negasus/haproxy-spoe-go v1.0.7 h1:OhRY0zapeHudrRqoblI9DjIolJjWI0s/TO6kT/va0ao=
github.com/negasus/haproxy-spoe-go v1.0.7/go.mod h1:ZrBizxtx2EeLN37Jkg9w9g32a1AFCJizA8vg46PaAp4=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oschwald/geoip2-golang/v2 v2.0.0 h1:1GZ7MsQsbIKeOXMDV2MqBVfV8NuCIqWatomkS67LwQo=
Expand Down
6 changes: 3 additions & 3 deletions internal/remediation/ban/root.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package ban

import (
"github.com/negasus/haproxy-spoe-go/action"
"github.com/dropmorepackets/haproxy-go/pkg/encoding"
log "github.com/sirupsen/logrus"
)

Expand All @@ -19,6 +19,6 @@ func (b *Ban) InitLogger(logger *log.Entry) {
b.logger = logger.WithField("module", "ban")
}

func (b *Ban) InjectKeyValues(actions *action.Actions) {
actions.SetVar(action.ScopeTransaction, "contact_us_url", b.ContactUsURL)
func (b *Ban) InjectKeyValues(writer *encoding.ActionWriter) {
_ = writer.SetString(encoding.VarScopeTransaction, "contact_us_url", b.ContactUsURL)
}
10 changes: 5 additions & 5 deletions internal/remediation/captcha/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

"github.com/crowdsecurity/crowdsec-spoa/internal/cookie"
"github.com/crowdsecurity/crowdsec-spoa/internal/remediation"
"github.com/negasus/haproxy-spoe-go/action"
"github.com/dropmorepackets/haproxy-go/pkg/encoding"
log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -94,16 +94,16 @@ func (c *Captcha) getTimeout() int {
}

// Inject key values injects the captcha provider key values into the HAProxy transaction
func (c *Captcha) InjectKeyValues(actions *action.Actions) error {
func (c *Captcha) InjectKeyValues(writer *encoding.ActionWriter) error {

// We check if the captcha configuration is valid for the front-end
if err := c.IsFrontEndValid(); err != nil {
return err
}

actions.SetVar(action.ScopeTransaction, "captcha_site_key", c.SiteKey)
actions.SetVar(action.ScopeTransaction, "captcha_frontend_key", providers[c.Provider].key)
actions.SetVar(action.ScopeTransaction, "captcha_frontend_js", providers[c.Provider].js)
_ = writer.SetString(encoding.VarScopeTransaction, "captcha_site_key", c.SiteKey)
_ = writer.SetString(encoding.VarScopeTransaction, "captcha_frontend_key", providers[c.Provider].key)
_ = writer.SetString(encoding.VarScopeTransaction, "captcha_frontend_js", providers[c.Provider].js)

return nil
}
Expand Down
Loading