Implement HAProxy WAF integration with ModSecurity 3 for advanced threat protection

Advanced 75 min Aug 19, 2026 56 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Deploy ModSecurity 3 as a Stream Processing Offload Agent in front of HAProxy to inspect HTTP traffic with the OWASP Core Rule Set. This tutorial covers SPOA compilation, SPOE filter configuration, detection versus blocking modes, and production tuning.

Prerequisites

  • A running HAProxy instance with root or sudo access
  • Basic familiarity with HAProxy configuration syntax
  • A test or staging environment for tuning WAF rules before production rollout
  • At least 2 CPU cores and 2GB RAM for compiling libmodsecurity

What this solves

HAProxy has no native request body inspection engine, so it cannot stop SQL injection, XSS or other layer 7 attacks on its own. This tutorial wires HAProxy to ModSecurity 3 via the Stream Processing Offload Engine (SPOE) so every request is inspected by the OWASP Core Rule Set (CRS) before it reaches your backends.

You will build the SPOA (Stream Processing Offload Agent) from source, connect it to HAProxy with SPOE filters, load the CRS, and tune detection versus blocking behavior for production traffic.

Warning: Enabling blocking mode without a tuning period will generate false positives against legitimate traffic. Always run in detection-only mode first and review logs before switching to blocking.

Step-by-step installation

Install build dependencies

The SPOA and libmodsecurity must be compiled from source on all four target distributions. Install the toolchain and libraries first.

sudo apt update
sudo apt install -y build-essential git autoconf automake libtool \
  pkgconf libpcre2-dev libpcre3-dev libxml2-dev libcurl4-openssl-dev \
  libyajl-dev doxygen libgeoip-dev liblua5.3-dev libssl-dev \
  zlib1g-dev libmaxminddb-dev cmake
sudo dnf groupinstall -y "Development Tools"
sudo dnf install -y epel-release
sudo dnf install -y git autoconf automake libtool pkgconf pcre2-devel \
  libxml2-devel libcurl-devel yajl-devel doxygen GeoIP-devel \
  lua-devel openssl-devel zlib-devel libmaxminddb-devel cmake

Build and install libmodsecurity 3

libmodsecurity is the standalone engine that evaluates CRS rules. It is separate from the old Apache/NGINX ModSecurity modules.

cd /usr/local/src
sudo git clone --depth 1 -b v3/master https://github.com/owasp-modsecurity/ModSecurity modsecurity
cd modsecurity
sudo git submodule init
sudo git submodule update
sudo ./build.sh
sudo ./configure
sudo make -j"$(nproc)"
sudo make install

This build takes 15-30 minutes depending on CPU. Verify the shared library was installed correctly.

ls -la /usr/local/modsecurity/lib/libmodsecurity.so*

Build the ModSecurity SPOA

The SPOA is the daemon that speaks the SPOE binary protocol to HAProxy and forwards requests to libmodsecurity for inspection.

cd /usr/local/src
sudo git clone https://github.com/haproxytech/spoa-modsecurity
cd spoa-modsecurity
sudo make MODSECURITY_INC=/usr/local/modsecurity/include \
  MODSECURITY_LIB=/usr/local/modsecurity/lib
sudo cp spoa /usr/local/bin/modsec-spoa
sudo mkdir -p /etc/modsecurity-spoa

Install HAProxy 2.8 or newer

SPOE support is mature from HAProxy 2.0 onward, but use 2.8 LTS or later for stability and performance fixes relevant to WAF workloads.

sudo apt install -y haproxy
haproxy -v
sudo dnf install -y haproxy
haproxy -v

If your distribution ships an older HAProxy, refer to Install and configure HAProxy for high availability load balancing for repository options that provide newer releases.

Download the OWASP Core Rule Set

The CRS provides the generic ruleset for SQLi, XSS, RCE, protocol violations and scanner detection. Pull the latest stable release.

cd /etc/modsecurity-spoa
sudo git clone -b v4.3/master https://github.com/coreruleset/coreruleset crs
sudo cp crs/crs-setup.conf.example crs/crs-setup.conf

Create the ModSecurity engine configuration

This file loads libmodsecurity's core directives, the CRS setup file and all CRS rule files in order.

SecRuleEngine DetectionOnly
SecRequestBodyAccess On
SecResponseBodyAccess Off
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
SecPcreMatchLimit 250000
SecPcreMatchLimitRecursion 250000
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogParts ABIJDEFHZ
SecAuditLogType Serial
SecAuditLog /var/log/modsecurity/audit.log
SecDebugLog /var/log/modsecurity/debug.log
SecDebugLogLevel 3

Include /etc/modsecurity-spoa/crs/crs-setup.conf
Include /etc/modsecurity-spoa/crs/rules/*.conf

SecRuleEngine is set to DetectionOnly deliberately. You will switch this to On only after reviewing logs from real traffic.

sudo mkdir -p /var/log/modsecurity
sudo chown -R haproxy:haproxy /var/log/modsecurity
sudo chmod 750 /var/log/modsecurity
Note: The audit log directory is owned by the haproxy user because the SPOA process should run as an unprivileged service account, not root.

Create the SPOA systemd service

Run the SPOA as a dedicated systemd unit so it starts on boot and restarts automatically on failure.

[Unit]
Description=ModSecurity SPOA for HAProxy
After=network.target

[Service]
Type=simple
User=haproxy
Group=haproxy
ExecStart=/usr/local/bin/modsec-spoa -n 4 -p 12345 -r /etc/modsecurity-spoa/modsecurity.conf
Restart=on-failure
RestartSec=3
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now modsec-spoa
sudo systemctl status modsec-spoa

Configure the SPOE filter definition

The SPOE config tells HAProxy which messages to send to the agent and which variables to expect back.

[modsecurity]
spoe-agent modsecurity-agent
    messages check-request
    option var-prefix modsec
    timeout hello 2s
    timeout idle 2m
    timeout processing 1s
    use-backend spoe-modsecurity
    log global

spoe-message check-request
    args unique-id method path query req.ver req.hdrs_bin req.body_size req.body
    event on-frontend-http-request

Configure HAProxy to use the SPOE filter

Add the SPOE backend, the filter directive on your frontend, and the ACL that blocks requests flagged by ModSecurity.

global
    log /dev/log local0
    maxconn 20000
    tune.ssl.default-dh-param 2048

defaults
    log global
    mode http
    option httplog
    timeout connect 5s
    timeout client 30s
    timeout server 30s

frontend fe_main
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
    filter spoe engine modsecurity config /etc/haproxy/modsecurity.spoe.conf

    http-request deny deny_status 403 if { var(txn.modsec.blocked) -m int gt 0 }

    default_backend be_app

backend spoe-modsecurity
    mode tcp
    balance roundrobin
    timeout connect 5s
    timeout server 2m
    server spoa1 127.0.0.1:12345

backend be_app
    balance roundrobin
    option httpchk GET /healthz
    server app1 203.0.113.10:8080 check
    server app2 203.0.113.11:8080 check

If you have not yet configured TLS on this HAProxy instance, follow Set up HAProxy SSL termination with Let's Encrypt certificates before exposing this frontend publicly.

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy

Tune CRS paranoia level and anomaly thresholds

Paranoia level controls how aggressively CRS matches suspicious patterns. Level 1 is the default; higher levels catch more but increase false positives.

SecAction \
    "id:900000,\
    phase:1,\
    nolog,\
    pass,\
    t:none,\
    setvar:tx.paranoia_level=1,\
    setvar:tx.inbound_anomaly_score_threshold=5,\
    setvar:tx.outbound_anomaly_score_threshold=4"

Start at paranoia level 1 with the default anomaly threshold of 5. Only raise the level after a tuning period shows an acceptable false-positive rate.

Add custom rule exceptions

Legitimate application behavior sometimes trips generic rules, such as a CMS admin panel that submits raw HTML in a form field. Exclude specific rule IDs for specific paths instead of disabling entire rule categories.

SecRule REQUEST_URI "@beginsWith /admin/content-editor" \
    "id:1000001,\
    phase:2,\
    pass,\
    nolog,\
    ctl:ruleRemoveById=941100-941999"

Place exclusion files after the CRS rule includes in modsecurity.conf so they can override earlier matches. Document every exclusion with a comment explaining why it exists and who approved it.

Verify your setup

Confirm the SPOA is listening and HAProxy is forwarding traffic to it.

sudo ss -tlnp | grep 12345
sudo journalctl -u modsec-spoa -n 50 --no-pager

Test detection with a known malicious payload. In DetectionOnly mode this should log the event but still return a 200 response.

curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/?id=1' OR '1'='1"
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/?exec=/bin/bash"

Check that the audit log recorded the anomaly.

sudo tail -n 20 /var/log/modsecurity/audit.log

After reviewing logs for false positives over a representative traffic period, switch to blocking mode and confirm attacks now return 403.

sudo sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/modsecurity-spoa/modsecurity.conf
sudo systemctl restart modsec-spoa
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/?id=1' OR '1'='1"

Logging, alerting and testing against attack payloads

Feed the ModSecurity audit log into your centralized logging pipeline rather than tailing it manually in production. If you already run the ELK stack, follow Set up ELK Stack for centralized ModSecurity log analysis and monitoring to parse the JSON audit log format and build dashboards for blocked requests by rule ID and source IP.

Run a broader payload sweep to validate CRS coverage before going live. Test SQL injection, XSS, path traversal and command injection patterns against a staging frontend running in blocking mode.

curl -s -o /dev/null -w "%{http_code}\n" "https://staging.example.com/search?q=<script>alert(1)</script>"
curl -s -o /dev/null -w "%{http_code}\n" "https://staging.example.com/file?path=../../etc/passwd"
curl -s -o /dev/null -w "%{http_code}\n" "https://staging.example.com/ping?host=127.0.0.1;cat /etc/shadow"

All three should return 403. If any return 200, check the anomaly score threshold and confirm the relevant CRS rule files loaded without syntax errors.

Performance and high availability considerations

Each SPOA process is single-threaded per worker, so scale the -n worker count to match available CPU cores on the SPOA host. Keep the SPOA on the same host as HAProxy or on a low-latency internal network, since every request now incurs an extra round trip.

Tuning areaRecommendation
SPOA workersSet -n to CPU core count minus 1, reserving a core for HAProxy
SPOE timeout processingKeep under 1s; requests exceeding this fail open or closed based on on-error setting
Request body limitCap SecRequestBodyLimit to your largest legitimate upload plus margin, not unlimited
Audit log volumeUse RelevantOnly and ship logs asynchronously to avoid disk I/O blocking inspection

For high availability, run HAProxy and the SPOA in an active-passive or active-active pair behind keepalived, and run a local SPOA instance per HAProxy node rather than a shared remote SPOA pool. This avoids a single SPOA becoming a bottleneck or single point of failure for every frontend. See Set up HAProxy high availability with keepalived clustering for automatic failover for the VRRP configuration.

Decide the SPOE failure mode explicitly. By default, if the SPOA is unreachable, HAProxy fails open and traffic passes uninspected. For security-critical services, consider failing closed by adding an ACL that denies traffic when the SPOE variable is unset, but test this carefully since it turns a WAF outage into a full service outage.

Common issues

SymptomCauseFix
All requests return 403 immediatelyAnomaly threshold too low or a broad rule exclusion is missingSet SecRuleEngine DetectionOnly, review /var/log/modsecurity/audit.log, raise threshold temporarily
HAProxy config check fails on filter spoeSPOE config file path is wrong or has a syntax errorRun sudo haproxy -c -f /etc/haproxy/haproxy.cfg and check the reported line number
modsec-spoa fails to startlibmodsecurity.so not found by the dynamic linkerRun sudo ldconfig /usr/local/modsecurity/lib and add that path to /etc/ld.so.conf.d/
Legitimate file uploads get blockedSecRequestBodyLimit too low or CRS rule 200002 triggering on multipart dataIncrease the body limit and add a scoped exclusion for the upload endpoint
High latency on every requestSPOA and HAProxy on different hosts with network round trip overheadColocate SPOA on the same host as HAProxy or on a sub-millisecond LAN link
Audit log grows unboundedSecAuditEngine set to On instead of RelevantOnlySet SecAuditEngine RelevantOnly and configure logrotate on the audit log path

Next steps

Running this in production?

Want this handled for you? Running this at scale adds a second layer of work: rule tuning as your application changes, false-positive triage, CRS version upgrades and failover drills for the SPOA tier. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Non vuoi gestirlo da solo?

Gestiamo l'infrastruttura di aziende che dipendono dall'uptime. Completamente gestita, con un referente fisso che conosce il tuo ambiente.

Avete un referente fisso che conosce il vostro ambiente

Rotterdam 04:50 · raggiungibile con un messaggio, senza modulo ticket