El rate limit que nunca se aplicó: cuando tu proxy le miente a tu app sobre la IP del cliente
Cómo detectar que un proxy está ocultando la IP real del cliente y deja inutilizado un rate limiter basado en IP.
Mantener servicios detrás de un reverse proxy simplifica el deploy, pero también agrega una frontera que conviene hacer explícita: la aplicación ya no recibe la conexión directamente del cliente. Este caso muestra por qué un rate limiter configurado puede no estar limitando nada.
El síntoma
El problema apareció al revisar logs de requests que debían venir de clientes distintos. El endpoint tenía rate limiting habilitado y las respuestas de la aplicación parecían normales, pero la dirección registrada para cada request era siempre la misma. El contador no estaba agrupando clientes: estaba agrupando al proxy.
Un rate limiter “configurado” no es lo mismo que un rate limiter que funciona.
La pista: la IP interna repetida
Al comparar varias requests consecutivas, la IP observada por la aplicación era una dirección interna de la red del proxy. Desde la perspectiva del limiter, todo el tráfico externo provenía del mismo origen. Eso puede producir dos fallos opuestos: bloquear a todos cuando se alcanza el límite, o confiar en una clave que no representa al cliente y volver inútil la protección.
Por qué pasa esto
Cuando una aplicación corre detrás de un reverse proxy, el proxy es quien recibe la conexión TCP
real. La aplicación solo ve la conexión que le llega desde ese proxy. Para conservar la IP del
cliente, el proxy debe reenviarla en un header estándar como X-Forwarded-For o X-Real-IP, y la
aplicación debe estar configurada para confiar en ese header únicamente desde proxies conocidos.
Si falta cualquiera de las dos partes, todos los requests pueden verse como una sola IP. Confiar indiscriminadamente en headers enviados por el cliente tampoco es una solución: permitiría falsear la identidad usada por el rate limiter.
El fix
En una aplicación Express detrás de un único proxy confiable, la configuración puede verse así:
app.set('trust proxy', 1)
app.use((req, res, next) => {
console.log('IP real detectada:', req.ip)
next()
})El código puede copiarse al portapapeles.El número importa: 1 expresa que hay un salto de proxy confiable. La configuración exacta debe
ajustarse a la topología real; no conviene usar true como atajo si existen proxies intermedios o
si la aplicación puede recibir tráfico directamente. Después del cambio, comprobé el valor de
req.ip con requests desde orígenes distintos y confirmé que el limiter usaba esa misma clave.
La lección
Si corrés detrás de Coolify, Nginx, Cloudflare, Traefik o cualquier otro proxy, verificá explícitamente qué IP está viendo tu aplicación. Probá la cadena completa —proxy, headers, framework y limiter— en vez de asumir que una opción habilitada implica que la protección está activa. Un log sencillo puede revelar meses de una defensa que nunca estuvo funcionando.