Centinel AnalyticaCentinel Analytica
PlatformsWeb Server

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 and extract the module

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 pages
  • json.lua — JSON encoder/decoder used by the Lua hooks
  • spoe-centinel.conf — SPOE filter configuration
Configure haproxy.cfg
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.crt

The 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.

Set your secret key and reload
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-service Lua 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
TimeoutDefaultDescription
timeout processing500msMax wait for the SPOA response. If exceeded, request is allowed (fail-open). Increase if you see frequent timeouts.
timeout hello500msTLS + SPOP handshake timeout. Only matters on first connection — subsequent requests reuse the persistent connection.
timeout idle30sIdle 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_centinel

A 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

VariableDescription
CENTINEL_SECRET_KEYAPI key from the dashboard. Required.

HAProxy config directives

DirectivePurpose
lua-prepend-pathPath to Lua module directory
lua-loadLoads the Centinel Lua hooks at startup
filter spoeActivates the SPOE filter with spoe-centinel.conf
http-request lua.centinel-request-hookRoutes requests based on SPOA decision
http-response lua.centinel-response-hookApplies response headers/cookies on allow
use_backend centinel-blockRoutes blocked requests to the block service
backend spoe-centinelTLS+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.cfg

Any 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

On this page