··
Las cinco recetas WAF Custom Rules que aplico en cada zona, el rate limit con la trampa de no poder usar ip.src en Free, el HSTS conservador, el TLS mínimo razonable, una Cache Rule de un año para los bundles hasheados de Vite, y por qué descarté cachear endpoints de Next.js 16 en Free.

Con Traefik y AOP, firewall por IPs y monitor del token funcionando, la mitad del valor de tener Cloudflare delante todavía está sin explotar. Esta entrega es sobre lo que hago en el lado de Cloudflare, las reglas WAF que filtran tráfico hostil antes de llegar al origen, el rate limiting de los endpoints sensibles, el hardening de la zona (SSL/TLS, HSTS, Bot Fight Mode) y un par de Cache Rules que reducen drásticamente el tráfico hacia el VPS para los proyectos con bundles hasheados.
Todo lo que cuento está dentro del plan Free. CF Pro elimina varias limitaciones y simplifica algunas reglas, pero el objetivo era ver hasta dónde se puede llegar sin pasar por caja. Spoiler, bastante lejos.
Antes de abrir el dashboard, las restricciones que marcan el diseño.
ip.src en la expresión, ni directo ni con not (ip.src in $lista). Esto significa que no puedes excluir tu propia IP del rate limit dentro de la regla. La salida es una Custom Rule de tipo Skip que se ejecuta antes y exime tu IP del resto de reglas.ip.src in $lista. La asimetría no es intuitiva pero es la que hay.Antes de tocar la zona, en Manage Account, Configurations, Lists, creé una IP List llamada trusted_ips con mi IP de casa. Es una lista a nivel cuenta, así que la puedo referenciar desde cualquier zona como $trusted_ips. Si cambia mi IP, edito un solo sitio y todas las zonas se actualizan.
Esta regla se ejecuta antes que ninguna otra y exime mi IP del resto del WAF y del rate limit. Sin esto, cuando hago pruebas contra mis propias APIs me autobloqueo a los pocos segundos.
ip.src in $trusted_ipsSkipLe dedico un slot pero a cambio puedo ser tan agresivo como quiera con el resto de reglas sin riesgo de morder mi propia mano cuando trabajo desde casa.
(ip.src.country in {"RU" "CN" "KP" "IR" "BY"}) and not (ip.src in $trusted_ips)Lista corta y conservadora, países desde los que objetivamente no espero tráfico legítimo en proyectos personales y desde los que recibo cantidades desproporcionadas de scanners. Managed Challenge en lugar de Block deja la puerta abierta a un visitante humano (que pueda resolver el reto) sin abrirla a bots automatizados. La cláusula not (ip.src in $trusted_ips) es por seguridad, aunque ya tengo Receta D que skip-ea, prefiero la doble red.
(starts_with(http.request.uri.path, "/wp-")) or
(http.request.uri.path contains "/.env") or
(http.request.uri.path contains "/.git/") or
(http.request.uri.path contains "/phpmyadmin") or
(http.request.uri.path eq "/xmlrpc.php") or
(http.request.uri.path contains "/.aws/") or
(http.request.uri.path contains "/.ssh/") or
(http.request.uri.path contains "/.DS_Store")Block.Ninguno de mis proyectos sirve estas rutas. Cualquier petición a estas rutas es siempre hostil, así que no tiene sentido contarlas para un rate limit. Block instantáneo es preferible a darle al atacante 5 intentos antes de cortar.
/admin con métodos de escrituraEsta receta usa la única Rate Limit Rule disponible en Free. Limita los intentos contra el endpoint de admin de mi blog y, de paso, contra rutas clásicas de scanner que no estaban en E (porque la receta E las bloquea ya, esto es duplicación intencional para zonas donde no aplique E).
(http.request.uri.path eq "/admin" and http.request.method in {"POST" "PUT"}) or
(starts_with(http.request.uri.path, "/wp-")) or
(http.request.uri.path contains "/.env") or
(http.request.uri.path contains "/phpmyadmin") or
(http.request.uri.path eq "/xmlrpc.php")El method in {"POST" "PUT"} evita que la navegación normal del panel admin (peticiones GET de la SPA) consuma el contador. Solo cuentan los intentos de login o de modificación.
En zonas con varios subproyectos (la mía con muchas APIs distintas) la receta B no es la mejor opción y prefiero un rate limit más genérico tipo starts_with(http.request.uri.path, "/api/"). Como el slot es uno, eliges según el perfil de la zona.
Esta es la decisión más delicada del hardening, porque HSTS es efectivamente irreversible. Si activas HSTS con includeSubDomains y un subdominio futuro tiene un fallo de TLS temporal, los visitantes que ya hayan cacheado el header no podrán entrar y no hay forma de saltar el error en el navegador. Y si añades preload, sales de la lista codificada en navegadores tardándote meses.
Mi configuración inicial es deliberadamente prudente.
En 1 a 2 meses sin incidentes, valoro ampliar a includeSubDomains. Preload solo lo encendería si tengo total certeza operacional. La regla mental es HSTS estricto se gana, no se elige.
Una de las cosas que cambia drásticamente la experiencia (y el ancho de banda hacia mi VPS) son las Cache Rules para los assets estáticos. Con bundles modernos hasheados (Vite, Webpack, Next.js) los nombres de fichero ya incluyen un hash de contenido, así que cualquier cambio cambia el nombre y no hay riesgo de servir contenido obsoleto. La conclusión natural es cache largo, sin remordimiento.
Mi origen (Express sirviendo la carpeta dist/ de Vite) enviaba Cache-Control: public, max-age=14400, 4 horas. Es razonable pero conservador, los assets podrían cachearse mucho más. Una sola Cache Rule cubre los proyectos Vite que hospedo.
(http.host in {"e2e.mi-zona.com" "otra-app.mi-zona.com"})
and starts_with(http.request.uri.path, "/assets/")Resultado, los /assets/index-HASH.js dejan de tocar mi VPS prácticamente nunca tras la primera petición. Y como el HASH cambia con cada deploy, no hay riesgo de servir versiones antiguas.
curl -sI https://<host>/path-a-un-estatico dos veces.cf-cache-status: HIT con un cache-control razonable, no toques.max-age corto y el archivo tiene hash en el nombre (foo-HASH.js), override TTL a 1 año es seguro.styles.css, app.js), no subas TTL. Cualquier deploy futuro serviría contenido obsoleto durante toda la ventana de cache.Probé cuatro caminos para cachear endpoints públicos de mi blog (Next.js 16 App Router). Ninguno dio HIT estable. La razón es que Next.js 16 inyecta automáticamente la cabecera Vary: rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch en todas las respuestas, y CF Free solo acepta Vary: Accept-Encoding como cacheable. Cualquier otra cosa convierte la respuesta en cf-cache-status: DYNAMIC, sin cache.
Los caminos probados y descartados.
Vary a Accept-Encoding. Next.js inyecta su Vary después, llegan dos cabeceras y CF se queda con la más estricta.Vary, parece aplicarse después de la decisión de cache.caches.default, el strip funciona pero el Cache API empieza a fallar y al final el Worker abre fallback abierto sin cache.Soluciones realistas, subir a CF Pro para Custom Cache Key que ignore Vary, meter un proxy delante de Next que reescriba antes de llegar a CF, o esperar a una versión de Next que permita opt-out del RSC Vary (no existe hoy). Lo dejé documentado y abandoné el camino. No todas las batallas merecen pelearse.
El orden de las operaciones evita ventanas de autobloqueo.
trusted_ips (nivel cuenta).trusted_ips (móvil con datos), un GET a /.env debe devolver 403.ip.src, no funciona en Free. Si lo intentas, error literal not entitled, the use of field ip.src is not allowed.Si en algún momento subo a Pro, las cosas que cambian.
ip.src in $trusted_ips directo en expresiones de Rate Limit (elimina la necesidad de la receta D)./admin de /api/ en reglas distintas.Vary RSC de Next.js.Con esto el lado Cloudflare está cubierto. Edge filtra, cachea lo que puede y endurece TLS. La última pieza de la serie es algo que no se ve hasta que pasa, un bug silencioso que aparece en cualquier app que dependa de la IP del cliente cuando metes Cloudflare delante de Traefik. Lo cuento en el post final.

Jose, autor del blog
QA Engineer. Escribo en voz alta sobre automatización, IA y arquitectura de software. Si algo te ha servido, escríbeme y cuéntamelo.
¿Qué te ha parecido? ¿Qué añadirías? Cada comentario afina la siguiente entrada.
Si esto te ha gustado

ScamDetector combina inteligencia artificial, búsqueda de reputación de teléfonos y escaneo de URLs para ayudarte a identificar estafas digitales. Sin registro, sin datos almacenados.

Repaso completo de las medidas de seguridad que puedes aplicar a un VPS Linux: desde CrowdSec y el firewall hasta el hardening del kernel, pasando por SSH, Docker y las actualizaciones automáticas.

Nuestros posts viven en una base de datos SQLite. Si alguien accede a ella, puede cambiar cualquier artículo sin dejar rastro. Construimos un verificador externo con hashes SHA-256 y firma Ed25519 que vigila la integridad desde un segundo servidor.