Cómo configurar un WAF con HAProxy

Guía completa para añadir protección de firewall de aplicaciones web a HAProxy usando coraza-spoa con el OWASP Core Rule Set, además de defensas nativas con ACL y stick-tables.

60-90 minutos intermediate 10 steps
Última actualización: Jul 18, 2026

HAProxy es el balanceador de carga de código abierto más usado del mundo, pero por sí solo no es un firewall de aplicaciones web. El HAProxy de código abierto no tiene ningún módulo WAF nativo; inspecciona y enruta el tráfico, termina TLS y aplica ACLs y límites de tasa, pero no ejecuta de forma nativa la detección de ataques a nivel de aplicación basada en firmas o reglas.

Para dotar al HAProxy de código abierto de una verdadera capacidad WAF, delegas la inspección de las peticiones en un motor externo mediante el Stream Processing Offload Protocol (SPOP), usando un Stream Processing Offload Agent (SPOA). La opción de código abierto más práctica es coraza-spoa, que ejecuta el motor OWASP Coraza y el OWASP Core Rule Set (CRS) como un SPOA al que HAProxy consulta en cada petición. HAProxy Enterprise ofrece una ruta distinta: un WAF integrado y potenciado con ML que se ejecuta en el mismo proceso, sin un agente aparte que gestionar.

Esta guía te lleva por ambas capas de defensa. Primero refuerzas HAProxy con ACLs y stick-tables nativas para el límite de tasa y el filtrado básico, y después despliegas coraza-spoa con el OWASP CRS para lograr una protección completa a nivel de aplicación. Al terminar tendrás un WAF funcional y ajustable delante de tus backends, sin coste de licencia.

Requisitos Previos

  • Se recomienda HAProxy 2.6+ (SPOE está disponible desde la 1.7, pero conviene usar una rama LTS actual)
  • Acceso root o sudo a tu servidor
  • Ubuntu 22.04+, Debian 12+ o RHEL/Rocky 9+ (otras distribuciones pueden variar)
  • Una aplicación backend en funcionamiento ya proxeada a través de HAProxy
  • Go 1.25+ y Git instalados (el go.mod de coraza-spoa requiere una cadena de herramientas de Go actual para compilar desde el código fuente)
  • Familiaridad básica con la configuración de HAProxy (frontends, backends, ACLs)

Guía Paso a Paso

1

Elige tu enfoque de WAF para HAProxy

El HAProxy de código abierto no incluye un módulo WAF. Existen tres opciones reales para añadir uno, y conviene elegir con criterio antes de instalar nada:

  • coraza-spoa (código abierto, la opción recomendada aquí): Ejecuta el motor OWASP Coraza y el OWASP Core Rule Set como un Stream Processing Offload Agent. HAProxy envía cada petición al agente por SPOP y actúa según el veredicto. Usa las mismas reglas SecLang que ModSecurity.
  • SPOA de ModSecurity (código abierto): Un SPOA más antiguo que integra el motor de ModSecurity. Funciona, pero recibe menos mantenimiento activo que Coraza y, en la práctica, queda desplazado por coraza-spoa en los despliegues nuevos.
  • WAF de HAProxy Enterprise (comercial): Un WAF integrado y potenciado con ML que se ejecuta en el mismo proceso que el balanceador, con un modo opcional de compatibilidad con el OWASP CRS. No hay agente aparte y la latencia añadida es casi nula, pero requiere una licencia Enterprise.

Sea cual sea el WAF que elijas, las ACLs y stick-tables nativas de HAProxy se encargan del límite de tasa y del filtrado grueso. Esta guía usa coraza-spoa porque es gratuito, de código abierto y ejecuta las mismas reglas del CRS que ModSecurity.

Consejo: Si ya usas HAProxy Enterprise, omite por completo los pasos del SPOA y habilita el WAF integrado mediante su propia configuración. Esta guía se centra en el HAProxy de código abierto, que no tiene WAF nativo.
2

Añade protección básica con ACLs y stick-tables

Antes de añadir un motor WAF, usa las funciones nativas de HAProxy para el límite de tasa y el filtrado básico de peticiones. Las stick-tables registran el estado por cliente; las ACLs te permiten bloquear o limitar en función de ese estado. Añade esto a tu frontend en /etc/haproxy/haproxy.cfg:

frontend web
    mode http
    bind :80

    # Buffer the request body so the WAF can inspect POST and JSON payloads later
    option http-buffer-request

    # Track request rate per source IP over a 10s window
    stick-table type ip size 100k expire 30s store http_req_rate(10s)
    http-request track-sc0 src

    # Deny clients making more than 100 requests per 10 seconds
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }

    # Basic path-based filtering example
    acl block_path path_beg /.git /.env /wp-login.php
    http-request deny deny_status 403 if block_path

    default_backend web-backend

Recarga y confirma que la configuración es válida:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Consejo: Las stick-tables y las ACLs son rápidas y se ejecutan en el mismo proceso, pero no sustituyen a un WAF. No entienden las cargas de inyección SQL ni de XSS; eso es lo que aporta el motor Coraza en los siguientes pasos.
3

Instala coraza-spoa

coraza-spoa es un programa en Go que ejecuta el motor WAF Coraza y lo expone a HAProxy mediante SPOP. Compílalo desde el código fuente:

# Clone the SPOA
cd /opt
sudo git clone https://github.com/corazawaf/coraza-spoa.git
cd coraza-spoa

# Build the binary with Mage (there is no Makefile; requires Go 1.25+ per go.mod)
sudo go run mage.go build

# Install the compiled binary onto your PATH (it is written to build/)
sudo cp build/coraza-spoa /usr/local/bin/

Crea un directorio para alojar la configuración del agente y las reglas:

sudo mkdir -p /etc/coraza-spoa/rules
Advertencia: coraza-spoa es un proyecto en estado preview y su formato de configuración ha cambiado entre versiones. Verifica siempre las claves de configuración exactas contra la versión que hayas compilado, ya que los campos mostrados aquí pueden diferir en versiones más nuevas o más antiguas.
4

Descarga y configura el OWASP Core Rule Set

Coraza ejecuta las reglas estándar del OWASP CRS, las mismas reglas SecLang que usa ModSecurity. Descarga el CRS y su configuración base:

cd /etc/coraza-spoa/rules
sudo git clone https://github.com/coreruleset/coreruleset.git
cd coreruleset

# Create the CRS setup file from the example
sudo cp crs-setup.conf.example crs-setup.conf

También necesitas una configuración base de Coraza, que establece el modo del motor de reglas y el tratamiento del cuerpo de la petición. Crea /etc/coraza-spoa/rules/coraza.conf con las directivas principales:

# Start in detection mode until you have tuned false positives
SecRuleEngine DetectionOnly

SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyLimitAction Reject

SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml application/json

SecAuditEngine RelevantOnly
SecAuditLogParts ABIJDEFHZ
SecAuditLog /var/log/coraza-spoa/audit.log
Consejo: Mantén SecRuleEngine en DetectionOnly al principio. Registra los bloqueos potenciales sin denegar tráfico, de modo que puedas identificar los falsos positivos antes de aplicar el bloqueo.
5

Configura el agente coraza-spoa

El agente necesita su propia configuración que le indique qué reglas cargar y dónde escuchar a HAProxy. Crea /etc/coraza-spoa/config.yaml:

# coraza-spoa agent configuration
bind: 127.0.0.1:9000
log_level: info

# Must match the name of an application defined in the list below
default_application: default

# applications is a YAML list; each entry is an object with a name
applications:
  - name: default
    directives: |
      Include /etc/coraza-spoa/rules/coraza.conf
      Include /etc/coraza-spoa/rules/coreruleset/crs-setup.conf
      Include /etc/coraza-spoa/rules/coreruleset/rules/*.conf
    # Enable response-phase inspection so the coraza-res message and the
    # http-response deny rule actually run (defaults to false)
    response_check: true
    transaction_ttl_ms: 60000
    log_level: info
    log_file: /var/log/coraza-spoa/coraza.log

Crea el directorio de logs:

sudo mkdir -p /var/log/coraza-spoa

Comprueba que el agente arranca y carga las reglas sin errores:

sudo coraza-spoa -config /etc/coraza-spoa/config.yaml
Advertencia: Si el agente no arranca, casi siempre se trata de un problema de ruta o de sintaxis en las reglas. Lee la salida de arranque; indica el archivo y la línea de la primera regla que no pudo interpretar.
6

Configura el filtro SPOE de HAProxy

HAProxy se comunica con el agente a través de un archivo de configuración del Stream Processing Offload Engine (SPOE). Este define qué datos de la petición se envían a Coraza y en qué variables se escribe el veredicto. Crea /etc/haproxy/coraza.cfg:

[coraza]
spoe-agent coraza-agent
    messages    coraza-res
    groups      coraza-req
    option      var-prefix      coraza
    option      set-on-error    error
    timeout     hello           2s
    timeout     idle            2m
    timeout     processing      500ms
    use-backend coraza-spoa
    log         global

spoe-message coraza-req
    args app=var(txn.coraza.app) src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body exportRuleIDs=bool(false)

spoe-message coraza-res
    args app=var(txn.coraza.app) id=var(txn.coraza.id) version=res.ver status=status headers=res.hdrs body=res.body exportRuleIDs=bool(false) detect-only=bool(false)
    event on-http-response

spoe-group coraza-req
    messages coraza-req

La petición se envía mediante un spoe-group que el frontend dispara con send-spoe-group; el mensaje de respuesta se dispara automáticamente en el evento on-http-response. El orden de los argumentos coincide exactamente con el del ejemplo oficial coraza.cfg, y es obligatorio que estén en ese orden.

Después define un backend que apunte al agente para que HAProxy sepa dónde encontrarlo. Añade esto a /etc/haproxy/haproxy.cfg:

backend coraza-spoa
    mode tcp
    option spop-check
    server coraza 127.0.0.1:9000 check
Consejo: La opción var-prefix (coraza) determina los nombres de las variables que establece el agente, por ejemplo txn.coraza.action. Mantenla coherente entre este archivo y las ACLs del siguiente paso.
7

Integra la decisión del WAF en tu frontend

Ahora asocia el filtro SPOE a tu frontend y actúa según el veredicto que devuelve Coraza. Actualiza el frontend del Paso 2 en /etc/haproxy/haproxy.cfg:

frontend web
    mode http
    bind :80

    # Buffer the request body so Coraza can inspect POST and JSON payloads
    option http-buffer-request

    # Native rate limiting (from Step 2)
    stick-table type ip size 100k expire 30s store http_req_rate(10s)
    http-request track-sc0 src
    http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }

    # Select which coraza-spoa application this frontend uses
    http-request set-var(txn.coraza.app) str(default)

    # Attach the Coraza WAF filter and send the request to the agent
    filter spoe engine coraza config /etc/haproxy/coraza.cfg
    http-request send-spoe-group coraza coraza-req

    # Deny requests and responses Coraza flags as attacks
    http-request deny deny_status 403 if { var(txn.coraza.action) -m str deny }
    http-response deny deny_status 403 if { var(txn.coraza.action) -m str deny }

    default_backend web-backend

La línea option http-buffer-request no es opcional si te preocupan los ataques en el cuerpo POST. Sin ella, HAProxy no rellena de forma fiable la muestra req.body antes de enviar la petición al agente, por lo que las cargas de SQLi o XSS transportadas en cuerpos de petición de formularios o JSON nunca se envían a Coraza y quedan sin inspeccionar, aunque SecRequestBodyAccess esté en On. La inspección del cuerpo de la petición está limitada por tune.bufsize (16 KB por defecto): HAProxy solo puede almacenar en búfer alrededor de un bufsize, así que los cuerpos mayores que eso no se envían por completo a Coraza. Si necesitas inspeccionar cuerpos de formulario o JSON más grandes, aumenta tune.bufsize con moderación (por ejemplo a 64 KB) en lugar de igualarlo a un SecRequestBodyLimit de varios megabytes, lo que multiplicaría la memoria por conexión y arriesgaría una condición de falta de memoria.

Valida la configuración completa antes de recargar:

sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Advertencia: Mientras Coraza se ejecute en modo DetectionOnly, la variable txn.coraza.action no se establecerá a deny, por lo que estas reglas todavía no bloquearán nada. Es lo esperado; cambiarás al modo de bloqueo después del ajuste.
8

Ejecuta coraza-spoa como servicio

Ejecuta el agente bajo systemd para que arranque en el inicio del sistema y se reinicie ante un fallo. Crea /etc/systemd/system/coraza-spoa.service:

[Unit]
Description=Coraza SPOA WAF agent for HAProxy
After=network.target

[Service]
ExecStart=/usr/local/bin/coraza-spoa -config /etc/coraza-spoa/config.yaml
Restart=on-failure
User=haproxy
Group=haproxy

[Install]
WantedBy=multi-user.target

Habilita y arranca el agente, y después recarga HAProxy:

sudo systemctl daemon-reload
sudo systemctl enable --now coraza-spoa
sudo systemctl reload haproxy

# Confirm the agent is running and listening
sudo systemctl status coraza-spoa
Consejo: Arranca el agente antes de recargar HAProxy. Con la opción set-on-error error configurada, HAProxy falla en modo abierto si el agente no está accesible; revisa tus logs para que una caída del agente no desactive la protección de forma silenciosa.
9

Prueba el WAF

Establece temporalmente SecRuleEngine On en /etc/coraza-spoa/rules/coraza.conf, reinicia el agente y envía cargas de ataque conocidas. Deberían bloquearse con un 403:

sudo systemctl restart coraza-spoa

# SQL injection attempt (should be blocked)
curl -I 'http://your-server.com/?id=1%20OR%201=1'

# XSS attempt (should be blocked)
curl -I 'http://your-server.com/?q=<script>alert(1)</script>'

# Request-body SQL injection attempt (should be blocked with http-buffer-request enabled)
curl -I -X POST -d 'id=1 OR 1=1' 'http://your-server.com/login'

# A normal request (should pass with 200)
curl -I 'http://your-server.com/'

Confirma que los bloqueos aparecen en el log de auditoría de Coraza:

sudo tail -f /var/log/coraza-spoa/audit.log

Deberías ver entradas con los IDs de las reglas del CRS coincidentes y la puntuación de anomalía que activó la denegación.

Advertencia: Si los ataques no se bloquean, verifica que SecRuleEngine esté en On, que el agente se reinició correctamente y que el var-prefix de coraza.cfg coincide con el nombre de la variable en tu ACL http-request deny. Si solo se cuelan los ataques en el cuerpo de la petición, confirma que option http-buffer-request está definido en el frontend.
10

Ajusta los falsos positivos

Ejecuta en modo DetectionOnly durante una o dos semanas y revisa el log de auditoría en busca de peticiones legítimas que se habrían bloqueado. Ajusta usando los mecanismos estándar del CRS en un archivo personalizado que se cargue después de las reglas, por ejemplo /etc/coraza-spoa/rules/custom-exclusions.conf:

# Disable a specific rule that misfires
SecRuleRemoveById 942100

# Disable a rule only for a specific path
SecRule REQUEST_URI "@beginsWith /api/upload" \
  "id:1000,phase:1,pass,nolog,ctl:ruleRemoveById=920420"

También puedes bajar el nivel de paranoia del CRS en crs-setup.conf para reducir los falsos positivos (1 = menos falsos positivos, 4 = más estricto):

SecAction "id:900000,phase:1,pass,t:none,nolog,\
  setvar:tx.blocking_paranoia_level=1"

Una vez que el log esté limpio, establece SecRuleEngine On y reinicia el agente para empezar a aplicar el bloqueo.

Consejo: Añade el include de tu archivo de exclusiones personalizado después de las reglas del CRS en config.yaml para que tus anulaciones tengan prioridad. Empieza en el nivel de paranoia 1 y súbelo gradualmente a medida que ajustas.

Conclusión y Próximos Pasos

Tu instancia de HAProxy cuenta ahora con protección por capas: ACLs y stick-tables nativas para el límite de tasa y el filtrado grueso, más un WAF completo a nivel de aplicación mediante coraza-spoa ejecutando el OWASP Core Rule Set. Esto le da al HAProxy de código abierto la capacidad WAF que le falta de forma nativa, sin coste de licencia.

Próximos pasos:

  • Monitoriza /var/log/coraza-spoa/audit.log en busca de ataques bloqueados y falsos positivos
  • Tras una o dos semanas en modo DetectionOnly, cambia SecRuleEngine a On
  • Configura la rotación de logs para los logs de auditoría y del agente
  • Envía los logs de auditoría de Coraza a tu SIEM (ELK, Splunk, Grafana) para correlacionarlos
  • Programa actualizaciones periódicas del OWASP CRS y vuelve a probar tras cada actualización
  • Si el rendimiento en el mismo proceso y la detección basada en ML te importan más que la transparencia de las reglas, evalúa el WAF integrado de HAProxy Enterprise como alternativa al enfoque SPOA

Solución de Problemas

HAProxy se recarga pero el WAF nunca bloquea nada

Comprueba que el agente se está ejecutando (systemctl status coraza-spoa) y que SecRuleEngine está en On y no en DetectionOnly. En modo DetectionOnly, Coraza registra las coincidencias pero nunca establece el veredicto de denegación, así que HAProxy no tiene nada sobre lo que actuar.

Los ataques por cadena de consulta se bloquean, pero los del cuerpo POST no

Esto significa que los cuerpos de las peticiones no están llegando a Coraza. Añade option http-buffer-request al frontend para que HAProxy almacene en búfer y rellene req.body antes de enviar la petición al agente. Sin ello, SecRequestBodyAccess On no tiene datos que inspeccionar. Ten en cuenta que el tamaño del cuerpo almacenado en búfer está limitado por tune.bufsize (16 KB por defecto); auméntalo con moderación si necesitas inspeccionar cuerpos más grandes.

La variable txn.coraza.action nunca se establece

El var-prefix de /etc/haproxy/coraza.cfg debe coincidir con el nombre de la variable en tu ACL http-request deny. Si var-prefix es coraza, la variable es txn.coraza.action. Un desajuste hace que la ACL nunca coincida de forma silenciosa.

Todo el tráfico se deniega tras habilitar el bloqueo

Normalmente es una avalancha de falsos positivos de un CRS sin ajustar con un nivel de paranoia alto. Vuelve a poner SecRuleEngine en DetectionOnly, baja el nivel de paranoia a 1 y revisa el log de auditoría para localizar los IDs de las reglas problemáticas antes de volver a habilitarlo.

HAProxy deja pasar el tráfico cuando el agente está caído (fail-open)

Con option set-on-error error, HAProxy deja pasar el tráfico si no puede alcanzar el agente. Esto evita que una caída del agente deje tu sitio fuera de servicio, pero también implica que una caída sin monitorizar desactiva la protección. Configura alertas sobre la salud del servicio coraza-spoa y sobre los contadores de errores SPOE en las estadísticas de HAProxy.

El agente no arranca por un error de análisis de reglas

Lee la salida de arranque; indica el archivo y la línea de la primera regla que no se pudo interpretar. Las causas habituales son una ruta de Include ausente, un checkout del CRS incompleto o una versión de coraza-spoa cuyas claves de configuración difieren de las de esta guía.

Latencia alta tras habilitar el WAF

Cada petición hace ahora un ida y vuelta al agente. Mantén coraza-spoa en el mismo host que HAProxy (127.0.0.1), ajusta más el timeout de processing y considera bajar SecRequestBodyLimit o reducir la inspección del cuerpo de la respuesta si no la necesitas.

Preguntas Frecuentes

¿El HAProxy de código abierto incluye un WAF integrado?

No. El HAProxy de código abierto es un balanceador de carga y proxy inverso; no tiene ningún módulo WAF nativo. Ofrece filtrado basado en ACLs y límite de tasa con stick-tables, pero no detección de ataques a nivel de aplicación. Para obtener un WAF, o bien delegas la inspección en un motor externo mediante SPOP (por ejemplo, coraza-spoa), o bien usas el comercial HAProxy Enterprise, que tiene un WAF integrado.

¿Qué es un SPOA y por qué HAProxy lo necesita para tener un WAF?

SPOA significa Stream Processing Offload Agent. Es un programa externo con el que HAProxy se comunica mediante el Stream Processing Offload Protocol (SPOP) para delegar trabajo que no puede hacer por sí mismo. Como el HAProxy de código abierto no tiene un motor WAF, un SPOA como coraza-spoa ejecuta el WAF en un proceso aparte; HAProxy envía cada petición al agente para su inspección y aplica el veredicto que devuelve.

¿Puedo usar el OWASP Core Rule Set con HAProxy?

Sí. coraza-spoa ejecuta el motor OWASP Coraza, que es totalmente compatible con el lenguaje de reglas SecLang de ModSecurity y con el OWASP CRS. Tus reglas del CRS y cualquier regla SecLang personalizada funcionan igual que lo harían bajo ModSecurity. HAProxy Enterprise también ofrece un modo opcional de compatibilidad con el CRS que ejecuta las reglas del CRS a través de su motor potenciado con ML.

¿Debería usar coraza-spoa o el SPOA de ModSecurity?

Para los despliegues nuevos, coraza-spoa es la mejor opción. Ejecuta el motor OWASP Coraza, en desarrollo activo, en Go puro, usa las mismas reglas del CRS y es más fácil de compilar y contenerizar. El SPOA de ModSecurity, más antiguo, integra el motor de ModSecurity y, en la práctica, ha quedado desplazado. Ten en cuenta que coraza-spoa sigue en estado preview, así que valida los formatos de configuración con la versión que instales.

¿Cómo hago límite de tasa en HAProxy sin un WAF?

Usa stick-tables y ACLs, que son funciones nativas de HAProxy. Define una stick-table que registre una métrica como http_req_rate por IP de origen, rastrea las conexiones con http-request track-sc0 src y después deniega las peticiones que superen un umbral. Esto se ejecuta en el mismo proceso, sin agente externo, y es tu primera capa de defensa, complementando la inspección más profunda que aporta un WAF.

¿El WAF añade latencia a HAProxy?

El enfoque SPOA añade un ida y vuelta de HAProxy al agente en cada petición, por lo que hay una sobrecarga medible frente al procesamiento nativo. Mantén coraza-spoa en el mismo host (127.0.0.1) y ajusta el timeout de processing para minimizarla. El WAF integrado de HAProxy Enterprise evita esto al ejecutarse en el mismo proceso, que es su principal ventaja de rendimiento frente a la ruta SPOA de código abierto.

¿Qué ocurre si el agente coraza-spoa se cae?

Depende de tu gestión de errores de SPOE. Con option set-on-error error, HAProxy falla en modo abierto y deja pasar el tráfico cuando el agente no está accesible, lo que mantiene tu sitio en servicio pero desactiva la protección WAF durante la caída. Por eso, monitoriza el servicio coraza-spoa y los contadores de errores SPOE de HAProxy, y configura alertas sobre la salud del agente para que un fallo silencioso no te deje desprotegido.