WAF en Cloudflare para una web estática. Dos reglas, API y pruebas
Esta web es un sitio estático generado con Hugo. No tiene base de datos, no tiene PHP, no tiene WordPress. Y aun así, si miro los logs de Cloudflare, veo peticiones a /wp-admin, /wp-login.php y /xmlrpc.php cada pocos minutos. Bots que recorren internet buscando instalaciones de WordPress vulnerables sin importarles lo que hay realmente en el servidor.
La respuesta obvia es bloquearlo. Y ya que tengo el dominio en Cloudflare con proxy activo, el WAF está a un par de llamadas API de distancia.
Este artículo documenta exactamente lo que hice: qué reglas creé, cómo las apliqué sin tocar el panel web y cómo las verifiqué.
Por qué una web estática necesita WAF
Una web Hugo no tiene WordPress, así que un ataque a /wp-admin no va a funcionar nunca. Pero eso no significa que el tráfico sea inofensivo:
- Consume ancho de banda y cuota de peticiones
- Contamina los logs haciendo más difícil detectar actividad legítima
- Algunos bots no buscan solo WordPress — escanean puertos, enumeran rutas y lanzan payloads genéricos
El WAF de Cloudflare actúa antes de que la petición llegue al origen. Para un sitio en GitHub Pages, eso significa que el bloqueo ocurre en el edge de Cloudflare, sin que GitHub vea nada.
La API de Cloudflare: Rulesets
Cloudflare tiene dos APIs para WAF. La antigua (Firewall Rules) está deprecada. La nueva es la Rulesets API, que trabaja con fases (http_request_firewall_custom para reglas personalizadas).
La operación principal es un PUT al endpoint de la fase, que reemplaza el ruleset completo:
PUT /client/v4/zones/{zone_id}/rulesets/phases/http_request_firewall_custom/entrypoint
Importante: no existe PATCH por regla individual en el plan gratuito. Cada vez que modificas, envías el ruleset entero. Hay que incluir todas las reglas existentes en el body.
Regla 1 — Honeypot WordPress
La primera regla bloquea cualquier petición a rutas típicas de WordPress:
{
"description": "YYYY-MM-DD — honeypot WordPress: bloquea bots que buscan rutas WP en sitio estático",
"expression": "(http.request.uri.path contains \"/wp-admin\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/wp-content\") or (http.request.uri.path contains \"/wp-includes\") or (http.request.uri.path contains \"/xmlrpc.php\")",
"action": "block",
"enabled": true
}
La lógica es simple: si el sitio es estático y no tiene WordPress, cualquier petición a estas rutas viene de un bot. No hay falsos positivos posibles.
Regla 2 — Bloqueo de User Agents de escaneo
La segunda regla filtra por User Agent. Las herramientas de auditoría y pentesting suelen identificarse en el User Agent —es parte de su diseño, para que aparezcan en los logs del objetivo.
{
"description": "YYYY-MM-DD — bloqueo User Agents de herramientas de escaneo y reconocimiento",
"expression": "(lower(http.user_agent) contains \"nikto\") or (lower(http.user_agent) contains \"sqlmap\") or (lower(http.user_agent) contains \"nmap\") or (lower(http.user_agent) contains \"masscan\") or (lower(http.user_agent) contains \"zgrab\") or (lower(http.user_agent) contains \"nuclei\") or (lower(http.user_agent) contains \"dirbuster\") or (lower(http.user_agent) contains \"gobuster\") or (lower(http.user_agent) contains \"wfuzz\") or (lower(http.user_agent) contains \"acunetix\") or (lower(http.user_agent) contains \"nessus\") or (lower(http.user_agent) contains \"openvas\") or (lower(http.user_agent) contains \"burpsuite\") or (lower(http.user_agent) contains \"havij\") or (lower(http.user_agent) contains \"hydra\") or (lower(http.user_agent) contains \"metasploit\")",
"action": "block",
"enabled": true
}
lower() normaliza el User Agent antes de comparar, así cubre variantes en mayúsculas.
¿Bloquear por UA es suficiente? No como única defensa —un atacante serio puede cambiar el UA. Pero filtra el ruido automatizado que no se molesta en camuflarse, que es la mayoría.
Aplicarlo con curl
El script completo para crear ambas reglas desde cero. Necesitas tu Zone ID (lo encuentras en el dashboard de Cloudflare, columna derecha de la zona) y un API Token con permisos Zone > WAF > Edit:
#!/bin/bash
CF_API_TOKEN="tu_api_token"
ZONE_ID="tu_zone_id"
curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_request_firewall_custom/entrypoint" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rules": [
{
"description": "YYYY-MM-DD — honeypot WordPress: bloquea bots que buscan rutas WP en sitio estático",
"expression": "(http.request.uri.path contains \"/wp-admin\") or (http.request.uri.path contains \"/wp-login\") or (http.request.uri.path contains \"/wp-content\") or (http.request.uri.path contains \"/wp-includes\") or (http.request.uri.path contains \"/xmlrpc.php\")",
"action": "block",
"enabled": true
},
{
"description": "YYYY-MM-DD — bloqueo User Agents de herramientas de escaneo y reconocimiento",
"expression": "(lower(http.user_agent) contains \"nikto\") or (lower(http.user_agent) contains \"sqlmap\") or (lower(http.user_agent) contains \"nmap\") or (lower(http.user_agent) contains \"masscan\") or (lower(http.user_agent) contains \"zgrab\") or (lower(http.user_agent) contains \"nuclei\") or (lower(http.user_agent) contains \"dirbuster\") or (lower(http.user_agent) contains \"gobuster\") or (lower(http.user_agent) contains \"wfuzz\") or (lower(http.user_agent) contains \"acunetix\") or (lower(http.user_agent) contains \"nessus\") or (lower(http.user_agent) contains \"openvas\") or (lower(http.user_agent) contains \"burpsuite\") or (lower(http.user_agent) contains \"havij\") or (lower(http.user_agent) contains \"hydra\") or (lower(http.user_agent) contains \"metasploit\")",
"action": "block",
"enabled": true
}
]
}' | jq '.success, .result.rules[] | {id, description, enabled}'
La respuesta incluye los IDs asignados por Cloudflare a cada regla —hay que guardarlos si luego quieres modificar reglas individuales incluyéndolas por ID en el PUT.
Pruebas
Con las reglas activas, verifiqué cada caso:
# Regla 1 — rutas WordPress
curl -o /dev/null -s -w "%{http_code}" https://jaimealberto.io/wp-admin
# → 403 ✅
curl -o /dev/null -s -w "%{http_code}" https://jaimealberto.io/wp-login.php
# → 403 ✅
# Regla 2 — User Agents de escaneo
curl -o /dev/null -s -w "%{http_code}" -A "nikto/2.1.6" https://jaimealberto.io/
# → 403 ✅
curl -o /dev/null -s -w "%{http_code}" -A "sqlmap/1.7.8#stable" https://jaimealberto.io/
# → 403 ✅
# Tráfico legítimo — no debe bloquearse
curl -o /dev/null -s -w "%{http_code}" https://jaimealberto.io/
# → 301 ✅ (redirect HTTP→HTTPS, comportamiento normal)
Los cinco tests pasaron correctamente.
Consultar las reglas activas
Para ver el estado del ruleset en cualquier momento:
CF_API_TOKEN="tu_api_token"
ZONE_ID="tu_zone_id"
curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/rulesets/phases/http_request_firewall_custom/entrypoint" \
-H "Authorization: Bearer $CF_API_TOKEN" | jq '.result.rules[] | {id, description, enabled}'
Regla opcional — WPScan
Si tienes un WordPress real (no es mi caso), tiene sentido añadir una regla específica para WPScan, el escáner de vulnerabilidades WordPress más usado. Su User Agent es inconfundible:
WPScan v3.8.25 (https://wpscan.com/wordpress-security-scanner)
La expresión para bloquearlo sería:
{
"description": "YYYY-MM-DD — bloqueo WPScan",
"expression": "lower(http.user_agent) contains \"wpscan\"",
"action": "block",
"enabled": true
}
No la incluí en mis reglas porque no sirvo WordPress y bloquear WPScan en un sitio estático es redundante con la Regla 1 (si buscas /wp-admin ya eres un bot). Pero si tienes WordPress, esta regla va antes que las demás: bloquea la herramienta de reconocimiento antes de que enumere plugins y temas con vulnerabilidades conocidas.
Para añadirla, incluye el objeto JSON anterior en el array rules del PUT junto a las reglas existentes.
Qué queda pendiente
Estas dos reglas cubren el ruido más habitual. Hay cosas más avanzadas que me interesan para más adelante:
- Cloudflare Tunnel: exponer servicios del homelab con autenticación Zero Trust, sin abrir puertos en el router
- Rate limiting: limitar peticiones por IP para mitigar fuerza bruta en formularios
- Managed Rules: el conjunto de reglas mantenidas por Cloudflare (disponibles en plan Pro)
Por ahora, con dos reglas y cinco minutos de API, el ruido más evidente está fuera.