Skip to content

Commit 348d362

Browse files
docs: add CDN configuration guide for HAProxy SPOA with firewall recommendations
- Document how to configure SPOA when HAProxy is behind a CDN - Use X-Real-IP header extraction instead of direct client IP - Add critical firewall security note about trusted proxy validation - Include guidance on X-Forwarded-For with left-to-right (1) and right-to-left (-1) IP extraction - Provide CDN-specific header names and HAProxy functions reference
1 parent 3c52dfe commit 348d362

File tree

1 file changed

+131
-0
lines changed

1 file changed

+131
-0
lines changed

crowdsec-docs/unversioned/bouncers/haproxy_spoa.mdx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,137 @@ recaptcha
225225
turnstile
226226
```
227227

228+
#### HAProxy Behind a CDN
229+
230+
When HAProxy is deployed behind an upstream Content Delivery Network (CDN), the source IP seen by HAProxy will be the CDN's edge server IP, not the real client IP. To properly evaluate and apply security rules based on the actual client IP, you need to configure the SPOA to extract the real IP from the CDN-provided header.
231+
232+
:::info
233+
234+
Most CDNs add an `X-Real-IP` or `X-Forwarded-For` header to the request to pass the original client IP. Ensure your CDN is configured to add this header, and adjust the examples below if your CDN uses a different header name.
235+
236+
:::
237+
238+
##### Configuration Changes
239+
240+
When HAProxy is behind a CDN, modify your `/etc/haproxy/crowdsec.cfg` to:
241+
242+
1. **Use only the `crowdsec-http` message** (the `crowdsec-ip` message will capture the CDN edge IP, which is not useful)
243+
2. **Extract the real client IP** from the CDN header using `req.hdr_ip()` to convert it to HAProxy's IP type
244+
3. **Pass the real IP to the bouncer** via the SPOE message
245+
246+
<details>
247+
248+
<summary>`/etc/haproxy/crowdsec.cfg` (CDN Configuration)</summary>
249+
250+
```haproxy
251+
# /etc/haproxy/spoe/crowdsec.cfg
252+
# SPOE section for CDN deployments
253+
# - Uses a single message: crowdsec-http
254+
# - Extracts real client IP from X-Real-IP header (adjust if needed)
255+
# - Falls back to IP remediation if 'remediation' var is not set
256+
257+
[crowdsec]
258+
259+
spoe-agent crowdsec-agent
260+
messages crowdsec-http
261+
option var-prefix crowdsec
262+
option set-on-error error
263+
timeout hello 100ms
264+
timeout idle 30s
265+
timeout processing 500ms
266+
use-backend crowdsec-spoa
267+
log global
268+
269+
# This message extracts the real IP via X-Real-IP and includes all arguments.
270+
# IMPORTANT: req.hdr_ip() returns an IP type (required by SPOE protocol).
271+
# If 'remediation' isn't provided by HAProxy, the bouncer will check IP remediation.
272+
spoe-message crowdsec-http
273+
args remediation=var(txn.crowdsec.remediation) \
274+
crowdsec_captcha_cookie=req.cook(crowdsec_captcha_cookie) \
275+
id=unique-id host=hdr(Host) method=method path=path query=query \
276+
version=req.ver headers=req.hdrs body=req.body url=url ssl=ssl_fc \
277+
src-ip=req.hdr_ip(x-real-ip) src-port=src_port
278+
event on-frontend-http-request
279+
```
280+
281+
</details>
282+
283+
##### Key Changes Explained
284+
285+
- **Single message**: Only `crowdsec-http` is used. The `crowdsec-ip` message would run at `on-client-session` and capture the CDN's IP, not the real client IP, so it's omitted.
286+
- **IP extraction**: The `req.hdr_ip(x-real-ip)` function extracts the IP from the `X-Real-IP` header and converts it to HAProxy's IP type, which is required by the SPOE protocol.
287+
- **Header name**: If your CDN uses a different header (e.g., `X-Forwarded-For`, `CF-Connecting-IP` for Cloudflare), adjust the header name accordingly. For Cloudflare specifically, use `req.hdr_ip(cf-connecting-ip)`.
288+
289+
:::warning Firewall Rules for Trusted Proxies
290+
291+
Since your SPOA bouncer now relies on the `X-Real-IP` header to determine the client IP, **it is critical to ensure that only your trusted upstream CDN proxy can connect to your HAProxy server**.
292+
293+
If you do not properly firewall your HAProxy port, an attacker could connect directly and spoof the `X-Real-IP` header, bypassing your security rules.
294+
295+
**Ensure your firewall is configured to only allow connections to your HAProxy port (typically 80/443) from your upstream CDN provider's IP ranges.** Always verify your CDN provider's current IP ranges and keep your firewall rules up to date.
296+
297+
:::
298+
299+
##### HAProxy Configuration
300+
301+
Your `/etc/haproxy/haproxy.cfg` frontend configuration remains mostly the same, but ensure the CDN header is being passed through:
302+
303+
```haproxy
304+
frontend http-in
305+
bind *:80
306+
307+
# Ensure the CDN header is preserved (may already be done by your CDN)
308+
# You can optionally add debugging with set-header
309+
# http-request set-header X-Real-IP %[req.hdr(X-Real-IP)]
310+
311+
filter spoe engine crowdsec config /etc/haproxy/crowdsec.cfg
312+
http-request set-header X-CrowdSec-Remediation %[var(txn.crowdsec.remediation)]
313+
314+
## Handle 302 redirect for successful captcha validation (native HAProxy redirect)
315+
http-request redirect code 302 location %[var(txn.crowdsec.redirect)] if { var(txn.crowdsec.remediation) -m str "allow" } { var(txn.crowdsec.redirect) -m found }
316+
317+
## Call lua script only for ban and captcha remediations (performance optimization)
318+
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "captcha" }
319+
http-request lua.crowdsec_handle if { var(txn.crowdsec.remediation) -m str "ban" }
320+
321+
## Handle captcha cookie management via HAProxy (new approach)
322+
## Set captcha cookie when SPOA provides captcha_status (pending or valid)
323+
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 }
324+
## Clear captcha cookie when cookie exists but no captcha_status (Allow decision)
325+
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 }
326+
327+
use_backend <whatever>
328+
329+
backend crowdsec-spoa
330+
mode tcp
331+
server s1 127.0.0.1:9000
332+
```
333+
334+
##### Common CDN Headers
335+
336+
| CDN Provider | Header Name | HAProxy Function |
337+
|--------------|------------|------------------|
338+
| Generic / Most CDNs | `X-Real-IP` | `req.hdr_ip(x-real-ip)` |
339+
| Cloudflare | `CF-Connecting-IP` | `req.hdr_ip(cf-connecting-ip)` |
340+
| AWS CloudFront | `CloudFront-Viewer-Address` | `req.hdr_ip(cloudfront-viewer-address)` |
341+
| Akamai | `True-Client-IP` | `req.hdr_ip(true-client-ip)` |
342+
| Azure CDN | `X-Forwarded-For` | `req.hdr_ip(x-forwarded-for)` |
343+
344+
:::tip
345+
346+
If your CDN uses `X-Forwarded-For` with multiple IPs (comma-separated), you'll need to extract the correct IP. For example:
347+
348+
```haproxy
349+
src-ip=req.hdr_ip(x-forwarded-for,1)
350+
```
351+
352+
This tells HAProxy to use the first IP from the comma-separated list. If your CDN appends IPs from right to left (instead of left to right), you can use `-1` to extract the rightmost IP:
353+
354+
```haproxy
355+
src-ip=req.hdr_ip(x-forwarded-for,-1)
356+
```
357+
358+
:::
228359

229360
### Prometheus Metrics
230361

0 commit comments

Comments
 (0)