HAProxy
Bot protection for HAProxy 2.4+ using SPOE and Lua hooks.
Overview
Centinel integrates with HAProxy via the SPOE (Stream Processing Offload Engine) filter. HAProxy sends request metadata to the Centinel SPOA over SPOP, receives a decision, and Lua hooks enforce it — routing blocked or challenged requests to the built-in block service before they reach your backend.
Prerequisites
- Secret key from your dashboard
- HAProxy 2.4+ compiled with Lua 5.3 and SPOE support
Install
Download centinel-haproxy.zip and extract the three files to your HAProxy configuration directory:
curl -O https://docs.centinelanalytica.com/downloads/centinel-haproxy.zip
unzip centinel-haproxy.zip -d /etc/haproxy/lua/
mv /etc/haproxy/lua/spoe-centinel.conf /etc/haproxy/The zip contains:
centinel-spoe.lua— Lua hooks for request routing, response headers, and block pagesjson.lua— JSON encoder/decoder used by the Lua hooksspoe-centinel.conf— SPOE filter configuration
global
lua-prepend-path /etc/haproxy/lua/?.lua
lua-load /etc/haproxy/lua/centinel-spoe.lua
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http-in
bind *:80
filter spoe engine centinel config /etc/haproxy/spoe-centinel.conf
http-request lua.centinel-request-hook
http-response lua.centinel-response-hook
use_backend centinel-block if { var(txn.centinel_status) -m str blocked }
default_backend servers
backend servers
server app1 127.0.0.1:8080 check
backend centinel-block
http-request use-service lua.centinel-block-service
backend spoe-centinel
mode tcp
server spoa validator.centinelanalytica.com:9000 ssl sni str(validator.centinelanalytica.com) verify required verifyhost validator.centinelanalytica.com ca-file /etc/ssl/certs/ca-certificates.crtThe defaults block is required
Centinel's hooks are HTTP-mode rules. Without mode http, HAProxy discards every http-request and http-response line, reports the config as valid, starts normally, and enforces nothing. Run haproxy -c -f /etc/haproxy/haproxy.cfg and read the warnings: a line saying rules were "ignored for frontend" means protection is off.
Fail-open by design
If the SPOA is unreachable or doesn't respond within timeout processing (500ms default), HAProxy lets the request through. A down SPOA never blocks traffic.
verifyhost and sni are not optional. The secret key travels on this connection as a SPOE
argument, and verify required on its own checks the certificate chain without checking the
hostname.
export CENTINEL_SECRET_KEY="sk_live_your_key_here"
haproxy -f /etc/haproxy/haproxy.cfg -sf $(cat /var/run/haproxy.pid)For systemd:
[Service]
Environment="CENTINEL_SECRET_KEY=sk_live_your_key_here"Configure
How it works
HAProxy's SPOE filter sends request metadata to the hosted SPOA over SPOP. The SPOA validates the request and returns a decision. Lua hooks then route the request:
- allow / not_matched — request goes to your backend. Response headers and cookies from the validator get applied on the way out.
- block / redirect — request hits the
centinel-block-serviceLua applet, which serves the block page or challenge HTML.
Static assets (images, fonts, CSS, JS, media files) are excluded by an ACL in spoe-centinel.conf and never reach the SPOA.
HAProxy does not send a SPOE message that is larger than one frame (16,380 bytes by default).
It lets that request through without a check. The Lua hooks cap each argument so that the
message always fits: 2 KiB for the URL and the referrer, 1 KiB for the _centinel cookie and
for each header value, and 6 KiB for all headers together. Keep your tune.bufsize at 16384 or higher.
The cap removes the bytes after the limit, and the validator does not know that it removed them.
Policy rules on the URL, the query or the cookie see only the first 2 KiB of the URL and the first
1 KiB of the Cookie header. A bot can put a query parameter or a cookie after the limit. A rule
for that parameter or cookie then does not match.
Timeouts are configured in spoe-centinel.conf:
spoe-agent centinel-agent
timeout hello 500ms
timeout idle 30s
timeout processing 500ms| Timeout | Default | Description |
|---|---|---|
timeout processing | 500ms | Max wait for the SPOA response. If exceeded, request is allowed (fail-open). Increase if you see frequent timeouts. |
timeout hello | 500ms | TLS + SPOP handshake timeout. Only matters on first connection — subsequent requests reuse the persistent connection. |
timeout idle | 30s | Idle connection timeout before HAProxy drops the SPOA connection. |
Static files are excluded by the static_file ACL in the spoe-message section of spoe-centinel.conf. These requests skip the SPOE pipeline entirely.
Default exclusions: .avi, .avif, .bmp, .css, .eot, .flac, .flv, .gif, .gz, .ico, .jpeg, .jpg, .js, .less, .map, .mka, .mkv, .mov, .mp3, .mp4, .mpeg, .mpg, .ogg, .ogm, .opus, .otf, .png, .svg, .svgz, .swf, .ttf, .wav, .webm, .webp, .woff, .woff2.
Edit the path_reg ACL in spoe-centinel.conf to add or remove extensions.
Path exclusions belong in spoe-centinel.conf, next to the static_file ACL. Add your own ACL in
the spoe-message section and extend the event condition:
[centinel]
spoe-message check-centinel
args secret_key=str(...) url=... method=... ip=... cookie=... referrer=... headers=...
acl static_file path_reg \.(css|js|png|jpg|woff2)$
acl no_centinel path_beg /health /metrics /internal/
event on-frontend-http-request unless static_file or no_centinelA frontend ACL is not enough
Adding unless no_centinel to the http-request hooks in haproxy.cfg only skips enforcement. HAProxy filters take no conditions, so the SPOE event still fires and the request is still sent to the SPOA and on to /validate. Excluding a path from the event, as above, is what actually keeps it out of the pipeline.
Configuration reference
Environment variables
| Variable | Description |
|---|---|
CENTINEL_SECRET_KEY | API key from the dashboard. Required. |
HAProxy config directives
| Directive | Purpose |
|---|---|
lua-prepend-path | Path to Lua module directory |
lua-load | Loads the Centinel Lua hooks at startup |
filter spoe | Activates the SPOE filter with spoe-centinel.conf |
http-request lua.centinel-request-hook | Routes requests based on SPOA decision |
http-response lua.centinel-response-hook | Applies response headers/cookies on allow |
use_backend centinel-block | Routes blocked requests to the block service |
backend spoe-centinel | TLS+TCP backend pointing to the hosted SPOA |
Verify
Check the config first, and read the warnings rather than only the exit status:
haproxy -c -f /etc/haproxy/haproxy.cfgAny warning that http-request or http-response rules were "ignored for frontend" means the
frontend is in TCP mode and Centinel is doing nothing. Add the defaults block from Install.
Then reload and send a request that should be denied:
curl -s -o /dev/null -w '%{http_code}\n' -A 'python-requests/2.31' http://localhost/If everything is allowed, the usual causes are a missing mode http, a failed SPOA TLS handshake, or
timeout processing being exceeded, which fails open by design.
Changelog
- v1.2.0 — Bounded SPOE message size
- v1.1.0 — TLS for SPOA connection
- v1.0.0 — Initial release