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.

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.1.0 — TLS for SPOA connection
  • v1.0.0 — Initial release

On this page