Marcio Cunha

Cómo proteger APIs contra fuerza bruta usando Redis y Lua

Aprende a implementar un limitador de tasa atómico y distribuido en tus APIs utilizando scripts de Lua en Redis para bloquear ataques de fuerza bruta con eficiencia.

Marcio Cunha•4 min
También disponible en:PortuguêsEnglish
Resumen
  • Los scripts de Lua se ejecutan de forma atómica en Redis, evitando condiciones de carrera en entornos de alta concurrencia.
  • El control de solicitudes descentralizado protege servicios distribuidos sin sobrecargar bases de datos relacionales.
  • El conteo preciso por IP o token impide intentos automatizados de adivinar contraseñas en endpoints críticos.
  • La separación clara entre la lógica de expiración e incremento garantiza un bajo consumo de memoria en caché.
  • Los atributos de alto rendimiento hacen que este enfoque sea ideal para microservicios bajo tráfico intenso.

El desafío de controlar accesos en APIs bajo ataque

Cuando exponemos una API en internet, la mayor pesadilla no es solo el tráfico legítimo elevado, sino los intentos maliciosos de adivinar contraseñas. Los ataques de fuerza bruta saturan los servidores y consumen recursos preciosos intentando descifrar credenciales. En la práctica, esto significa que necesitamos una barrera en la puerta de entrada que logre contar peticiones por usuario o IP en tiempo real. Si dejamos este conteo en manos de bases de datos tradicionales, la lentitud generada por las consultas frecuentes puede derribar el mismo sistema que intentamos defender.

Por qué las soluciones ingenuas fallan en entornos distribuidos

Muchos desarrolladores intentan resolver el control de acceso guardando el número de intentos en variables dentro de la memoria de la propia aplicación o en tablas simples. El problema surge cuando escalamos la API a múltiples servidores ejecutándose en paralelo detrás de un balanceador de carga. Cada máquina pasa a ver solo una fracción de los accesos, permitiendo que un atacante multiplique los intentos reales sin superar el límite individual. Para solucionar esto, necesitamos un repositorio centralizado de alta velocidad, que es exactamente donde entra la arquitectura de cómo implementar CQRS asíncrono en APIs de alto tráfico para gestionar flujos masivos de datos.

El papel de Redis y el peligro de las condiciones de carrera

Redis es una base de datos en memoria extremadamente rápida, famosa por almacenar estructuras basadas en clave y valor. En escenarios comunes, verificamos un contador con el comando GET, sumamos un valor en el código de la aplicación y luego lo guardamos de vuelta con SET. Sin embargo, en entornos altamente concurrentes, dos peticiones paralelas pueden leer el mismo valor inicial al mismo tiempo, resultando en un conteo erróneo. Este fenómeno se conoce como condición de carrera. En la práctica, el sistema deja pasar más solicitudes de las debidas porque la lectura y la escritura no ocurrieron en un único paso indivisible.

Garantizando atomicidad con scripts Lua en Redis

Para eliminar cualquier fallo de sincronización, Redis permite ejecutar scripts escritos en el lenguaje Lua directamente dentro del propio servidor de base de datos. Cuando enviamos un script Lua a Redis, este ejecuta todo el bloque de código de forma estrictamente atómica. Ningún otro comando puede entrometerse o alterar los datos mientras el script se está ejecutando. Esto significa que podemos verificar el límite actual, sumar un nuevo intento y definir el tiempo de expiración en microsegundos, garantizando precisión absoluta incluso cuando cientos de usuarios intentan iniciar sesión simultáneamente.

Implementando el limitador de tasa en la práctica

A continuación tenemos un ejemplo clásico de un script Lua que implementa un limitador de tasa por ventana deslizante o conteo con expiración. El código verifica si la clave existe, incrementa el valor y define un tiempo de retención si es el primer intento de ese período. Para entender mejor cómo estructurar aplicaciones robustas que manejan este tipo de lógica y dependencias externas, vale la pena revisar cómo aplicar Clean Architecture en APIs Go de alto rendimiento para mantener el código limpio y testeable.

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])

local current = redis.call('get', key)
if current and tonumber(current) >= limit then
    return 0
end

current = redis.call('incr', key)
if tonumber(current) == 1 then
    redis.call('expire', key, expire_time)
end

return current

Manejando fallos y manteniendo la resiliencia del sistema

Aunque Redis es extremadamente estable, las redes pueden fallar y los servidores pueden reiniciarse de forma inesperada. Si la caché se cae, tu API no debe dejar de funcionar por completo solo porque el limitador quedó inaccesible. En la práctica, aplicamos el concepto de fallo abierto o cerrado según la criticidad del endpoint de inicio de sesión. En escenarios de alta seguridad, podemos optar por bloquear temporalmente o registrar una alerta crítica, de forma similar a los principios que empleamos al proteger microservicios del efecto cascada con circuit breaker para evitar que un componente rompa toda la aplicación.

Consideraciones finales sobre seguridad y rendimiento

Proteger tus rutas de autenticación contra ataques automatizados es un pilar indispensable para mantener la integridad de cualquier sistema moderno. El uso combinado de Redis con scripts Lua resuelve el problema clásico de la concurrencia distribuida sin sacrificar la velocidad de respuesta que los usuarios esperan. Al centralizar esta lógica en la caché, ahorras recursos vitales de la base de datos principal y garantizas una defensa sólida contra atacantes persistentes.