WAF y rate limiting: filtrar antes de que haya cómputo
Las fases de seguridad del borde se evalúan antes de que exista una invocación de tu Worker, y esa ordenación no es un detalle de implementación sino el eje económico de toda la defensa. Recorremos el orden real del tráfico, los conjuntos gestionados y su doble mecánica de firma y puntuación de anomalía, el despliegue en modo registro y las excepciones que domestican los falsos positivos, la anatomía completa de una regla de rate limiting con su característica de conteo y su ventana, y el limitador que vive dentro del Worker para cuando la clave que importa es el usuario y no la dirección.
Hasta ahora la pregunta ha sido quién. Esta lección cambia de eje y pregunta cuánto y de qué forma. Hay tráfico que no tiene identidad porque no la quiere tener: el escáner automático que prueba rutas de administración conocidas, la inyección que viaja en un parámetro, el bot que agota tu formulario de recuperación de contraseña, el cliente legítimo que se ha quedado en un bucle y te manda cuatrocientas peticiones por segundo sin querer. Ninguno de esos casos se resuelve con un proveedor de identidad, y todos comparten una propiedad que los hace tratables: se pueden reconocer en el borde, con información barata, antes de que tu código exista. Esta lección va de aprovechar esa propiedad hasta el final, y de entender por qué el orden en que se aplican los filtros importa más que la sofisticación de cada uno.
- Situar las fases de seguridad del borde respecto de la ejecución del Worker.
- Distinguir la mecánica de firma de la puntuación de anomalía en los conjuntos gestionados.
- Diseñar una regla de
rate limitingeligiendo característica, ventana, acción y mitigación. - Decidir cuándo limitar en el borde y cuándo hacerlo dentro con el binding del Worker.
El orden del tráfico
Una petición que llega a tu zona atraviesa una secuencia de etapas antes de convertirse en trabajo tuyo. Primero la mitigación de denegación de servicio en las capas bajas, que actúa sobre paquetes y no sabe nada de HTTP. Después la normalización del identificador, que resuelve las codificaciones raras para que las reglas posteriores no se dejen engañar por un mismo camino escrito de siete maneras. Luego tus reglas propias, después las de caudal, después los conjuntos gestionados, después la evaluación de bots, después Access, después la caché y, solo al final de todo eso, tu Worker.
flowchart LR A[paquete entrante] --> B[mitigacion de denegacion de servicio] B --> C[normalizacion del identificador] C --> D[reglas personalizadas] D --> E[reglas de caudal] E --> F[conjuntos gestionados] F --> G[evaluacion de bots] G --> H[Access] H --> I[cache] I --> J[Worker]
Esa cadena tiene una lectura técnica y otra económica, y la segunda es la que decide tu factura. Cada etapa es más barata que la siguiente y más tosca que la siguiente. Rechazar en la primera cuesta una fracción de microsegundo y no distingue nada; rechazar en la última cuesta arrancar un isolate, ejecutar tu código, quizá leer de D1 o gastar una neurona de inferencia, y distingue con toda la precisión de tu modelo de datos. La defensa bien construida no es la que pone el filtro más listo, sino la que coloca cada comprobación en la etapa más barata capaz de tomar esa decisión.
De ahí sale una regla práctica que conviene interiorizar antes de tocar un solo panel: lo que se pueda decidir con la ruta, el método, el país, el agente de usuario o la reputación de la dirección, se decide en el borde y nunca llega a tu código. Lo que necesite saber quién es el usuario o qué hay en tu base de datos, se decide dentro, asumiendo que ya pagaste por llegar hasta ahí.
Las reglas personalizadas son el instrumento de esa primera mitad, y su lenguaje de expresiones da acceso a los campos de la petición ya normalizados y a las señales que Cloudflare calcula por su cuenta, como la puntuación de bot o la reputación del origen. Una sola regla bien escrita elimina categorías enteras de ruido antes de que nada más se evalúe.
(http.request.uri.path contains "/wp-admin")
or (http.request.method eq "POST"
and http.request.uri.path eq "/api/registro"
and cf.bot_management.score lt 30)
Merece la pena detenerse en la fase de normalización, que parece un tecnicismo y evita una familia entera de evasiones. Un mismo camino se puede escribir con codificaciones porcentuales, dobles codificaciones, barras repetidas o segmentos relativos, y si cada regla tuviera que contemplar todas esas formas, la primera que se olvidara sería la puerta. Normalizar antes de evaluar convierte la comparación en algo decidible, y explica por qué el orden de las etapas no es negociable ni configurable a capricho.
Un aviso sobre workers.dev que enlaza con la primera lección: ese subdominio no pertenece a tu zona y por tanto no atraviesa ninguna de estas etapas. Un Worker publicado ahí no tiene reglas personalizadas, ni conjuntos gestionados, ni reglas de caudal. No es una limitación menor: es la diferencia entre tener toda esta cadena delante y no tener absolutamente nada.
Conjuntos gestionados: firma y anomalía
Los conjuntos gestionados del WAF no son un único mecanismo sino dos, con filosofías opuestas, y usarlos bien exige distinguirlas. El conjunto gestionado de Cloudflare es de alta señal: cada regla describe un patrón concreto, a menudo asociado a una vulnerabilidad publicada, y cuando dispara lo hace con poca ambigüedad. El conjunto central de OWASP funciona al revés: ninguna de sus reglas es concluyente por sí sola, cada coincidencia suma puntos a una puntuación de anomalía, y el bloqueo ocurre cuando la suma cruza un umbral que tú eliges.
| Mecanismo | Cómo decide | Palanca de ajuste |
|---|---|---|
| Conjunto gestionado | Firma concreta por regla | Desactivar reglas | cambiar acción |
Conjunto de OWASP |
Suma de puntos hasta un umbral | Nivel de paranoia | umbral |
| Puntuación de ataque | Modelo entrenado, no firmas | Umbral por clase de ataque |
| Credenciales expuestas | Contraste con filtraciones conocidas | Acción ante coincidencia |
El nivel de paranoia del conjunto de OWASP controla cuántas reglas participan en la suma: subirlo incorpora reglas más agresivas y más propensas a confundirse. El umbral controla cuánta suma hace falta para actuar. Ambas palancas mueven el mismo compromiso desde extremos distintos, y tocarlas a la vez sin medir es la forma más rápida de acabar con un sistema que bloquea a tus usuarios y deja pasar a los demás.
Junto a las firmas conviene conocer la puntuación de ataque, que no busca patrones conocidos sino que evalúa la petición con un modelo entrenado y devuelve una probabilidad de que se trate de un ataque, con desgloses por clase. Su virtud es que atrapa variantes que ninguna firma vio nunca; su límite es el de todo clasificador, que puede equivocarse sin poder explicarte por qué. Firma y puntuación se complementan justamente porque fallan de maneras distintas.
Un conjunto gestionado recién activado en modo bloqueo sobre una API que recibe cuerpos JSON grandes va a romper algo, casi con seguridad, y lo romperá para el cliente que peor se lo toma. Actívalo en modo registro, deja pasar tráfico real durante unos días, revisa qué reglas dispararon sobre peticiones legítimas y solo entonces cambia la acción.
La comprobación de credenciales expuestas merece una mención propia porque no protege de un ataque sino de una consecuencia. Contrasta el par de usuario y contraseña que viaja en la petición con conjuntos de credenciales ya filtradas, y te permite actuar sobre el intento de reutilización antes de que llegue a tu lógica de autenticación. En una aplicación protegida por Access esa capa importa poco, porque no hay contraseñas propias; en cualquier extremo que aún autentique por su cuenta, es de las medidas con mejor relación entre esfuerzo y daño evitado.
Cuando aparezca el falso positivo, la reacción correcta casi nunca es desactivar la regla en toda la zona. Existen excepciones acotadas por expresión, que permiten saltarse un conjunto, un grupo de reglas o una regla concreta únicamente en el camino donde molesta, y anulaciones que cambian su acción sin apagarla. La diferencia entre desactivar globalmente y acotar es la diferencia entre perder la protección y conservarla en el noventa y nueve por ciento de la superficie donde nadie se quejaba.
Anatomía de una regla de caudal
Una regla de rate limiting tiene más piezas de las que sugiere su nombre, y cada una responde a una pregunta distinta. La expresión de coincidencia decide a qué tráfico se aplica la regla. La característica de conteo decide qué se considera un mismo cliente, y es la decisión de diseño más importante. El periodo fija la ventana de observación. El umbral fija cuántas peticiones caben en esa ventana. La acción decide qué ocurre al superarlo, y el tiempo de mitigación decide cuánto dura el castigo.
Característica de conteo
Dirección de origen, valor de una cabecera, una cookie, un parámetro, el sistema autónomo, el país o la huella del cliente. Define quién es el mismo de antes.
Ventana y umbral
Diez segundos atrapan ráfagas; una hora atrapa cosecha lenta de datos. Casi nunca quieres una sola regla, sino dos con escalas distintas.
Acción
Bloquear es lo contundente. El desafío gestionado deja pasar al humano y frena al automatismo, que suele ser lo que de verdad querías.
Mitigación
Cuánto tiempo sigue castigado quien cruzó el umbral. Una mitigación larga con característica tosca convierte un incidente en una avería.
La característica merece pensarse con cuidado porque la dirección de origen, que es la elección por defecto, es a la vez demasiado gruesa y demasiado fina. Demasiado gruesa cuando hay traducción de direcciones detrás y una oficina entera comparte una sola: castigas a doscientas personas por lo que hizo una. Demasiado fina cuando el atacante dispone de un rango amplio y rota, con lo que ninguna dirección individual llega nunca al umbral. Contar por un valor que identifique la sesión o la cuenta, cuando ese valor viaje en una cabecera o una cookie, resuelve ambos extremos a la vez.
Hay una separación adicional que multiplica la expresividad de estas reglas y que muchos despliegues no usan: la expresión que decide a qué tráfico se aplica el límite no tiene por qué ser la misma que decide qué peticiones se cuentan. Puedes aplicar el límite a toda una API y contar únicamente los intentos que terminaron en un error de autenticación, con lo que el cliente correcto nunca se acerca al umbral y el que prueba credenciales lo alcanza en segundos. Esa asimetría entre lo que se limita y lo que se cuenta es la diferencia entre una regla que molesta a todos y una que solo afecta a quien se está portando mal.
Conviene saber también que los contadores del borde se mantienen de forma distribuida y se agregan, lo que significa que el límite se cumple de manera aproximada y no como una cota dura. Para el uso al que sirve una regla de caudal, que es frenar abuso, esa aproximación es irrelevante. Para cuotas de facturación, donde un cliente no debe poder consumir ni una unidad de más, no sirve, y hay que bajar a un mecanismo con estado.
Limitar dentro, cuando la clave es el usuario
La última capa vive ya en tu código, y existe porque hay una clave de conteo que el borde no puede conocer: el sujeto autenticado. Access resuelve la identidad, tu Worker la verifica, y solo entonces sabes que estas cuatrocientas peticiones son de la misma persona aunque vengan de siete direcciones distintas. Para eso Workers ofrece un binding de limitación declarado en el manifiesto.
[[ratelimits]]
name = "POR_USUARIO"
namespace_id = "1001"
simple = { limit = 60, period = 60 }
const yo = await identidad(request);
if (!yo) return new Response("no autorizado", { status: 403 });
const { success } = await env.POR_USUARIO.limit({ key: yo.sub });
if (!success) {
return new Response("demasiadas peticiones", {
status: 429,
headers: { "Retry-After": "60" },
});
}
Repara en el orden de las dos comprobaciones, porque está invertido respecto de lo que sugiere la intuición de eficiencia. Primero se verifica la identidad y después se consulta el límite, no al revés, y la razón es que sin identidad no existe la clave por la que se quiere contar. Ese es el precio de la precisión, y es también el motivo por el que esta capa va la última: es la única que sabe algo que ninguna anterior podía saber, y por eso es la única que puede permitirse ser la más cara.
Ese limitador tiene dos propiedades que hay que conocer para no llevarse una sorpresa. Es local al centro de datos que atiende la petición, de modo que un cliente repartido entre varias ubicaciones puede superar el límite nominal agregado; y no está pensado como cuota exacta sino como amortiguador de abuso, que es justo lo que necesitas después de la identidad. Cuando de verdad haga falta una cuenta global y exacta, la respuesta es un Durable Object por cuenta implementando un cubo de fichas, con el coste que ya conoces del nivel de objetos durables: una coordinación real, con su latencia real.
| Dónde limitar | Qué clave admite | Cuándo elegirlo |
|---|---|---|
| Regla del borde | Dirección | cabecera | cookie | país | Abuso anónimo, antes de gastar nada |
| Binding del Worker | Cualquier clave que calcules | Por usuario, tras verificar identidad |
Durable Object |
Cualquiera, con estado consistente | Cuotas exactas y facturables |
Aquí está la idea que convierte esta lección en algo más que una lista de paneles, y que se te va a aplicar en sistemas que no tienen nada que ver con Cloudflare. Todo ataque volumétrico es, en el fondo, una apuesta sobre una asimetría de coste: el atacante gana si generar la petición le sale más barato que atenderla te sale a ti. Cuando esa desigualdad se cumple, cada unidad de capacidad que añades financia una unidad más de ataque, y la escala te perjudica en vez de protegerte. Lo verdaderamente peligroso es que muchas defensas empeoran la desigualdad en lugar de arreglarla, porque son caras de ejecutar: un limitador que consulta una base de datos compartida por cada petición ha convertido una inundación de peticiones baratas en una inundación de escrituras caras, y ahora el atacante paga menos que antes por hacerte más daño que antes. La única salida es ordenar las comprobaciones por coste creciente y hacer que cada etapa descarte todo lo que pueda descartar con la información barata que ya tiene, de modo que la etapa cara solo vea lo que ninguna anterior supo resolver. Esto es exactamente la evaluación en cortocircuito de una condición booleana, exactamente lo que hace un planificador de consultas al empujar los filtros más selectivos hacia abajo, y exactamente por qué en un intercambiador se pone el filtro grueso antes del fino. Cuando lo miras así, la secuencia del borde deja de parecer una lista de productos que alguien quiere venderte y se revela como lo que es, un gradiente de precisión y de coste, y las decisiones de configuración se vuelven casi automáticas: cada comprobación baja hasta la etapa más barata capaz de tomarla, y ninguna sube más arriba de lo necesario. La consecuencia final es la que más cuesta aceptar, porque contradice el instinto de querer decidirlo todo con el máximo contexto disponible: la mejor defensa no es la que sabe más, es la que gasta menos en decir que no, y el buen ingeniero de seguridad se reconoce en que optimiza el coste del rechazo con la misma obsesión con la que otros optimizan el camino feliz.
- Enumera las diez rutas de tu aplicación y anota, para cada una, la etapa más barata capaz de decidir si una petición merece atención.
- Despliega el conjunto gestionado en modo registro y deja pasar tres días de tráfico real. Lista las reglas que dispararon sobre peticiones legítimas.
- Acota cada falso positivo con una excepción por expresión en vez de desactivar la regla. Compara la superficie protegida antes y después.
- Escribe dos reglas de caudal sobre el mismo camino con ventanas de diez segundos y de una hora. Explica qué ataque atrapa cada una y por qué ninguna sobra.
- Añade el binding de limitación por usuario tras verificar el
JWTy mide cuántas invocaciones de Worker te ahorra frente a limitar solo por dirección de origen.