WAF
El WAF de ExyonQ es un motor nativo en Rust, integrado en el proceso exyonq serve. Inspecciona ruta, query, cabeceras y cuerpo con detectores de firma y añade una puerta de abuso por IP. No es un complemento externo ni depende de Pingora.
Modos
waf.mode decide qué se hace con una firma que coincide:
| Modo | Detectores | Rechazo en el host |
|---|---|---|
disabled (o enabled = false) | No se compilan | No |
monitor | Se ejecutan; Block se degrada a Log | No |
block | Se ejecutan; Block es efectivo | Sí, cuando el host aplica la decisión |
La puerta de abuso es independiente del modo de firma: con mode = "monitor" y [waf.abuse] mode = "rate_limit" se siguen devolviendo 429. Un reto superado nunca anula un bloqueo de firma.
[waf]
| Campo | Tipo | Defecto | Notas |
|---|---|---|---|
enabled | bool | true | false desactiva detectores |
mode | string | "monitor" | monitor, block, disabled |
engine | string | "native" | Único valor admitido |
max_inspection_body_bytes | entero | 1048576 | Presupuesto de inspección de cuerpo; debe ser > 0 |
on_inspection_limit | string | "log_and_allow" | allow_uninspected, block, log_and_allow |
fail_policy | string | "closed_for_invalid_rules" | Único valor admitido |
Reglas incluidas
| ID | Familia | Qué detecta |
|---|---|---|
EXY-SQL-1001 | SQLi | UNION … SELECT |
EXY-SQL-1002 | SQLi | Tautología numérica (OR 1=1) |
EXY-SQL-1003 | SQLi | Funciones de retardo (SLEEP, BENCHMARK, WAITFOR DELAY) |
EXY-XSS-1001 | XSS | Etiqueta <script |
EXY-XSS-1002 | XSS | Manejadores onload, onerror, onclick, onmouseover |
EXY-XSS-1003 | XSS | Esquema javascript: |
EXY-PATH-1001 | Path traversal | ../, ..\, %2e%2e%2f |
EXY-PATH-1002 | Rutas sensibles | /etc/passwd, .htaccess, .env, id_rsa, /proc/self |
EXY-CMD-1001 | Ejecución de comandos | Encadenado con punto y coma o tubería, backticks, $(…) |
EXY-CMD-1002 | Ejecución de comandos | Rutas de intérprete (/bin/sh, cmd.exe, powershell) |
EXY-BOT-1001 | Bot | sqlmap, nikto, nmap, masscan, burpsuite, acunetix |
EXY-BOT-1002 | Bot | dirbuster, gobuster, wpscan, nessus |
EXY-HDR-1001 | Cabeceras | User-Agent ausente o vacío |
EXY-HDR-1002 | Cabeceras | Valor de cabecera mayor de 8 KiB |
EXY-IP-1001 | IP | IP de cliente en lista negra |
EXY-IP-1002 | IP | IP fuera de la lista blanca, cuando hay lista blanca |
EXY-LIMIT-BODY-001 | Límite de inspección | Presupuesto de cuerpo agotado en modo block |
Los bots legítimos conocidos se perdonan: googlebot, bingbot, slurp, duckduckbot, facebookexternalhit, twitterbot. Antes de comparar se normaliza la URL (incluida la doble codificación) y el cuerpo se lee como UTF-8 con pérdida, de modo que un cuerpo binario no se salta en silencio.
Activación por detector
[waf.ruleset.builtins] ajusta familia por familia. Si ningún ruleset suministra builtins, se usa el paquete por defecto: SQL, XSS, path y comandos en block; cabeceras y bot en log.
| Clave | Valores | Defecto efectivo |
|---|---|---|
sql_injection | off, log, block | block |
xss | off, log, block | block |
path_traversal | off, log, block | block |
command_injection | off, log, block | block |
header_anomaly | off, log, block | log |
bot_ua | off, log, block | log |
ip_filter | tabla enabled, whitelist, blacklist | desactivado |
[waf]
enabled = true
mode = "block"
engine = "native"
max_inspection_body_bytes = 1048576
on_inspection_limit = "log_and_allow"
fail_policy = "closed_for_invalid_rules"
[[waf.ruleset]]
id = "exyonq-core"
enabled = true
[waf.ruleset.builtins]
sql_injection = "block"
xss = "block"
path_traversal = "block"
command_injection = "block"
header_anomaly = "log"
bot_ua = "block"
Exclusiones
Una exclusión calla reglas concretas, no apaga el WAF. Es la herramienta para casos como el editor de bloques de WordPress, que envía HTML y Markdown en el cuerpo de un POST y dispararía XSS o CMD sobre contenido legítimo.
| Campo | Tipo | Defecto | Notas |
|---|---|---|---|
host | string | ausente | Si se define, debe coincidir |
path_prefix | string | ausente | Casa la ruta exacta o prefijo + "/" + resto; /api no casa /api2 |
method | string | ausente | Si se define, debe coincidir |
rule_ids | lista | obligatoria | Lista de IDs a suprimir; vacía no casa nada |
[[waf.exclusion]]
host = "blog.exyonq.org"
path_prefix = "/wp-admin"
method = "POST"
rule_ids = ["EXY-SQL-1001", "EXY-XSS-1001", "EXY-CMD-1001"]
rule_idsvacío no significa «todas las reglas». En configuración se rechaza; en tiempo de ejecución una lista vacía no coincide con nada. Una exclusión nunca es un bypass genérico.
Límite de inspección
El cuerpo se inspecciona hasta max_inspection_body_bytes (1 MiB por defecto). Al agotarse el presupuesto, on_inspection_limit decide:
| Valor | Efecto |
|---|---|
allow_uninspected | Sigue sin tratar el truncamiento como bloqueo |
block | Emite EXY-LIMIT-BODY-001 como bloqueo cuando la política lo permite |
log_and_allow | Deja evidencia y permite la petición |
La truncación se registra: nunca se reporta como «inspeccionado completo».
fail_policy
Solo existe closed_for_invalid_rules. Si el WAF está habilitado y la compilación del snapshot falla (regla inválida, regex roto, configuración incoherente), el arranque o la recarga fallan cerrados y no se publica una generación sin protección.
Abuso y rate limit
[waf.abuse] es una tabla única (singleton), no una lista: una sola política por proceso, indexada por IP.
| Campo | Tipo | Defecto | Notas |
|---|---|---|---|
mode | string | "off" | off, rate_limit, challenge |
requests_per_second | entero | 10 | ≥ 1 cuando el modo no es off |
burst | entero | 20 | ≥ 1 cuando el modo no es off |
[waf.abuse]
mode = "rate_limit"
requests_per_second = 20
burst = 40
Con rate_limit, una petición denegada devuelve 429 con Retry-After en segundos (mínimo 1). Con challenge, el host devuelve 403 con una prueba de trabajo HMAC-SHA256 y cookie de concesión (exyonq_waf_grant, TTL 300 s); la verificación es un POST /.exyonq/waf-challenge, que sigue pasando por las firmas.
Snapshot fijo por generación
Las reglas se compilan a un snapshot inmutable. Al arrancar se compila el snapshot de la generación 1; cada recarga compila un candidato en la fase de preparación y solo lo intercambia si la publicación sale bien. Un fallo de compilación deja vivo el snapshot anterior, y las peticiones en vuelo conservan el suyo.
Las variables de entorno mandan sobre el IR cuando están definidas: EXYONQ_WAF_MODE, EXYONQ_WAF_ENFORCE (solo con mode = "block"), EXYONQ_WAF_ABUSE, EXYONQ_WAF_ABUSE_RPS, EXYONQ_WAF_ABUSE_BURST y EXYONQ_WAF_CHALLENGE_KEY_FILE (32 bytes exactos). Un valor desconocido en EXYONQ_WAF_ABUSE falla cerrado.
Verificar con curl
Con mode = "block", estas peticiones se rechazan con 403 (comprobado en un despliegue real con la configuración de ejemplo):
curl -s -o /dev/null -w '%{http_code}\n' 'https://exyonq.dev/?q=1%20UNION%20SELECT%20password%20FROM%20users'
curl -s -o /dev/null -w '%{http_code}\n' 'https://exyonq.dev/?q=%3Cscript%3Ealert(1)%3C/script%3E'
curl -s -o /dev/null -w '%{http_code}\n' 'https://exyonq.dev/../../etc/passwd'
curl -s -o /dev/null -w '%{http_code}\n' -A 'sqlmap/1.7' 'https://exyonq.dev/'
Las cuatro devuelven 403 y el cuerpo blocked by waf. Una petición limpia devuelve 200. Si obtienes un 200 en una carga que debería bloquear, revisa el mode, el host de la petición y si hay una exclusión que cubra esa ruta y ese método.
Verificar en el journal
Cada coincidencia deja una línea warn con el objetivo exyonq_waf:
WARN exyonq_waf: waf violation phase=RequestHeaders rule_id=EXY-SQL-1001 action=Block truncated=false field=Some("query") enforce=true sql injection pattern
Campos útiles: phase (RequestHeaders, RequestBodyComplete…), rule_id, action, truncated, field (path, query, header:user-agent), enforce. La línea de acceso asociada lleva status=403 y outcome="forbidden".
journalctl -u exyonq-sites | grep 'waf violation' | tail -20
journalctl -u exyonq-sites | grep -c 'rule_id=EXY-BOT-1001'
Con [modules.metrics] habilitado, el WAF publica exyonq_waf_inspections_total, exyonq_waf_decisions_total y exyonq_waf_violations_total (por rule_id).
Límites conocidos
- No hay importación de ModSecurity CRS, ni motor de puntuación por anomalías, ni detectores de fase de respuesta.
- No hay proveedor externo de CAPTCHA.
- El abuso es una sola política global por proceso.
- Existen reglas personalizadas por ruleset (
[[waf.ruleset.rule]]conpattern,target,actionyphases), pero las acciones válidas sonallow,logyblock:challengeyrate_limitestán reservadas a la puerta de abuso.