Limitación de Tasa de API con Nginx y Redis

La limitación de tasa (rate limiting) es una medida de seguridad y disponibilidad esencial que protege las APIs contra abuso, ataques DDoS y clientes malintencionados que consumen recursos de forma desproporcionada. Nginx ofrece módulos integrados de limitación de tasa eficientes para casos simples, mientras que Redis permite implementar limitación distribuida y por cliente con mayor flexibilidad. Esta guía cubre la configuración del módulo limit_req de Nginx, la integración con Redis mediante OpenResty/Lua y la gestión de cuotas por cliente.

Requisitos Previos

  • Ubuntu 20.04+ o Rocky Linux 8+
  • Nginx 1.14+ instalado
  • Redis instalado y funcionando
  • Conocimientos básicos de configuración de Nginx
# Instalar Nginx y Redis
apt update && apt install -y nginx redis-server

# O para OpenResty (Nginx + módulo Lua)
# Ver sección de OpenResty para instalación

# Verificar que Nginx tiene los módulos necesarios
nginx -V 2>&1 | grep -o "with-http_limit_req_module\|with-http_auth_request_module"

Limitación de Tasa Básica con Nginx

Módulo limit_req de Nginx

# /etc/nginx/nginx.conf - Configuración global
http {
    # Definir zonas de memoria compartida para rate limiting
    # Formato: $variable zona:nombre:tamaño velocidad
    
    # Por IP del cliente (1 solicitud por segundo)
    limit_req_zone $binary_remote_addr zone=por_ip:10m rate=1r/s;
    
    # Por dirección IP para API (10 solicitudes por segundo)
    limit_req_zone $binary_remote_addr zone=api_general:10m rate=10r/s;
    
    # Por usuario autenticado (si tienes usuario en la variable)
    limit_req_zone $http_x_api_key zone=por_apikey:10m rate=100r/s;
    
    # Respuesta por defecto para solicitudes rechazadas
    limit_req_status 429;
    
    # Tamaño de log: 0=sin log, 1=solo primero, error/warn para debug
    limit_req_log_level warn;
}

Configuración por Endpoint

# /etc/nginx/sites-available/api-con-ratelimit
server {
    listen 443 ssl http2;
    server_name api.midominio.com;

    # SSL
    ssl_certificate /etc/letsencrypt/live/api.midominio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.midominio.com/privkey.pem;

    # Header para comunicar el límite al cliente
    add_header X-RateLimit-Limit 100;
    add_header Retry-After 60;

    # Ruta de login - límite estricto para prevenir fuerza bruta
    location /api/auth/login {
        # 5 solicitudes por minuto por IP, sin burst
        limit_req zone=por_ip burst=5 nodelay;
        
        proxy_pass http://backend;
    }

    # Ruta de registro - límite muy estricto
    location /api/auth/register {
        limit_req zone=por_ip burst=2 nodelay;
        proxy_pass http://backend;
    }

    # API general - más permisivo con burst
    location /api/ {
        # 10r/s con burst de 20 (permite picos cortos)
        limit_req zone=api_general burst=20 nodelay;
        
        proxy_pass http://backend;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # Webhook externo - sin rate limit
    location /api/webhooks/ {
        proxy_pass http://backend;
    }

    # Respuesta personalizada para 429
    error_page 429 /errors/429.json;
    location = /errors/429.json {
        internal;
        default_type application/json;
        return 429 '{"error":"Too Many Requests","message":"Has superado el límite de solicitudes. Inténtalo más tarde.","retry_after":60}';
    }
}

Configuración Avanzada de limit_req

# Configuración con múltiples capas de límites
http {
    # Zona global por IP
    limit_req_zone $binary_remote_addr zone=global:10m rate=100r/s;
    
    # Zona para endpoints sensibles
    limit_req_zone $binary_remote_addr zone=sensible:10m rate=5r/m;
    
    # Zona por combinación de IP y endpoint
    limit_req_zone $binary_remote_addr$uri zone=ip_endpoint:20m rate=10r/s;
    
    server {
        # Aplicar límite global a toda la API
        limit_req zone=global burst=50 nodelay;
        
        location /api/search {
            # Búsquedas: límite más permisivo con delay
            # 'nodelay' aplica la tasa sin retraso, rechazando el exceso
            # Sin 'nodelay': las solicitudes del burst se retrasan
            limit_req zone=global burst=100;  # Con delay
        }
        
        location /api/export {
            # Exportaciones costosas: muy restrictivo, sin burst
            limit_req zone=sensible nodelay;
        }
    }
}

Limitación por API Key

# Extraer la API key del header y usar como zona de limitación
http {
    # Mapa para normalizar la clave de API (evitar bypass con mayúsculas)
    map $http_x_api_key $api_key_normalized {
        default             "";
        "~^[a-zA-Z0-9_-]+$" $http_x_api_key;
    }
    
    # Zona por API key (100 req/s por clave)
    limit_req_zone $api_key_normalized zone=por_apikey:20m rate=100r/s;
    
    # Zona por IP como fallback
    limit_req_zone $binary_remote_addr zone=sin_auth:10m rate=10r/s;
    
    server {
        location /api/v1/ {
            # Si tiene API key, usar zona por apikey
            # Si no tiene, usar zona por IP (más restrictiva)
            set $ratelimit_zone "sin_auth";
            if ($http_x_api_key) {
                set $ratelimit_zone "por_apikey";
            }
            
            limit_req zone=sin_auth burst=5 nodelay;
            
            proxy_pass http://backend;
        }
    }
}

Rate Limiting Distribuido con Redis

Para múltiples servidores Nginx, el rate limiting nativo (en memoria) no es efectivo porque cada servidor lleva su propio contador. Redis permite un contador centralizado compartido:

# Instalar y configurar Redis para rate limiting
apt install redis-server

# Configurar Redis para ser accesible desde Nginx (si está en el mismo servidor)
# En /etc/redis/redis.conf
# bind 127.0.0.1
# maxmemory 256mb
# maxmemory-policy allkeys-lru

systemctl enable --now redis-server

# Verificar conexión a Redis
redis-cli ping
# PONG

Script Lua para Redis Rate Limiting

-- /etc/nginx/lua/ratelimit.lua
-- Rate limiting distribuido con Redis usando el algoritmo Token Bucket

local redis = require "resty.redis"
local red = redis:new()

-- Configuración de conexión a Redis
red:set_timeout(1000)  -- 1 segundo de timeout

local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
    ngx.log(ngx.ERR, "Error al conectar con Redis: ", err)
    -- Fallar de forma abierta (permitir la solicitud si Redis no está disponible)
    return
end

-- Obtener la clave del cliente (API key o IP)
local api_key = ngx.var.http_x_api_key
local client_key

if api_key and api_key ~= "" then
    client_key = "rl:apikey:" .. api_key
else
    client_key = "rl:ip:" .. ngx.var.binary_remote_addr
end

-- Parámetros del rate limit
local limit = tonumber(ngx.var.rate_limit) or 100  -- solicitudes por ventana
local window = tonumber(ngx.var.rate_window) or 60  -- ventana en segundos

-- Algoritmo de ventana deslizante
local now = ngx.time()
local window_start = now - window

-- Eliminar registros fuera de la ventana y contar los actuales
red:init_pipeline()
red:zremrangebyscore(client_key, 0, window_start)
red:zcard(client_key)
red:zadd(client_key, now, now .. ":" .. math.random(1000000))
red:expire(client_key, window + 1)
local results, err = red:commit_pipeline()

if err then
    ngx.log(ngx.ERR, "Error en pipeline Redis: ", err)
    return
end

local current_count = tonumber(results[2]) or 0

-- Añadir headers informativos
ngx.header["X-RateLimit-Limit"] = limit
ngx.header["X-RateLimit-Remaining"] = math.max(0, limit - current_count - 1)
ngx.header["X-RateLimit-Reset"] = now + window

-- Verificar si se supera el límite
if current_count >= limit then
    ngx.header["Retry-After"] = window
    ngx.status = 429
    ngx.header["Content-Type"] = "application/json"
    ngx.say('{"error":"Too Many Requests","message":"Límite de tasa superado","retry_after":' .. window .. '}')
    ngx.exit(429)
end

-- Devolver la conexión al pool
local ok, err = red:set_keepalive(10000, 100)
if not ok then
    ngx.log(ngx.WARN, "Error al devolver conexión Redis al pool: ", err)
end

OpenResty con Lua para Lógica Avanzada

# Instalar OpenResty (Nginx + LuaJIT + módulos adicionales)
apt install -y software-properties-common
add-apt-repository -y "ppa:openresty/ppa"
apt update && apt install -y openresty

# Instalar módulo Redis para OpenResty
opm get ledgetech/lua-resty-redis-connector
# O instalar lua-resty-redis
luarocks install lua-resty-redis

# Crear la configuración de OpenResty con rate limiting Lua
cat > /etc/openresty/nginx.conf << 'EOF'
worker_processes auto;
error_log /var/log/openresty/error.log warn;
pid /run/openresty.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    lua_shared_dict rate_limit_cache 10m;  # Caché local para reducir hits a Redis

    server {
        listen 80;
        server_name api.midominio.com;

        # Variables de configuración del rate limit
        set $rate_limit 100;    # solicitudes permitidas
        set $rate_window 60;    # ventana en segundos

        location /api/ {
            # Ejecutar el script Lua antes de procesar la solicitud
            access_by_lua_file /etc/openresty/lua/ratelimit.lua;

            proxy_pass http://backend;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }

        # Límites diferentes por endpoint
        location /api/auth/ {
            set $rate_limit 10;
            set $rate_window 60;
            access_by_lua_file /etc/openresty/lua/ratelimit.lua;
            proxy_pass http://backend;
        }
    }
}
EOF

systemctl enable --now openresty

Monitoreo y Alertas

# Monitorear solicitudes rechazadas por rate limiting en Nginx
tail -f /var/log/nginx/access.log | grep " 429 "

# Ver estadísticas de rate limiting
awk '$9 == 429 {count++} END {print "Solicitudes 429:", count}' /var/log/nginx/access.log

# Script de monitoreo con alertas
cat > /usr/local/bin/ratelimit-monitor.sh << 'EOF'
#!/bin/bash
# Monitoreo de rate limiting en Nginx
LOG="/var/log/nginx/access.log"
UMBRAL=100  # Alertar si más de 100 solicitudes 429 en el último minuto

HACE_UN_MINUTO=$(date -d "1 minute ago" "+%d/%b/%Y:%H:%M" 2>/dev/null || \
    date -v -1M "+%d/%b/%Y:%H:%M")

COUNT=$(awk -v tiempo="$HACE_UN_MINUTO" '$4 > "[" tiempo && $9 == "429" {count++} END {print count+0}' "$LOG")

if [ "$COUNT" -gt "$UMBRAL" ]; then
    echo "ALERTA: $COUNT solicitudes 429 en el último minuto (umbral: $UMBRAL)"
    # Enviar alerta
    logger -t rate-limit "ALERTA: $COUNT solicitudes 429 en el último minuto"
fi

# Ver las IPs con más solicitudes 429 (posibles atacantes)
echo "Top IPs con 429:"
awk '$9 == "429" {print $1}' "$LOG" | sort | uniq -c | sort -rn | head -10
EOF
chmod +x /usr/local/bin/ratelimit-monitor.sh
echo "*/5 * * * * root /usr/local/bin/ratelimit-monitor.sh" > /etc/cron.d/ratelimit-monitor

# Monitorear Redis para el rate limiting distribuido
redis-cli info stats | grep "keyspace_hits\|keyspace_misses"
redis-cli monitor  # Ver todos los comandos en tiempo real (debug)
redis-cli --scan --pattern "rl:*" | wc -l  # Número de claves de rate limit activas

Solución de Problemas

Clientes legítimos reciben 429:

# Ver qué zona está generando el 429
tail -100 /var/log/nginx/error.log | grep "limiting requests"
# Aumentar el burst para absorber picos normales
# limit_req zone=api_general burst=50 nodelay;  # De 20 a 50

El rate limiting no funciona en todos los servidores (configuración distribuida):

# Verificar conectividad a Redis desde todos los servidores
redis-cli -h redis-server ping
# Verificar que los contadores se acumulan correctamente
redis-cli -h redis-server keys "rl:*" | head -5
redis-cli -h redis-server get "rl:ip:X.X.X.X"

Nginx reporta "could not allocate shared memory zone":

# El tamaño de la zona compartida es insuficiente
# Aumentar el tamaño en la directiva limit_req_zone
# 1MB almacena ~16000 estados de IPs
# Para servidores con tráfico alto, usar 50m o más

Las respuestas 429 no incluyen los headers correctos:

# Nginx envía los headers antes de la ubicación de error
# Mover los add_header al bloque location correcto
# O usar always en el add_header:
# add_header X-RateLimit-Limit 100 always;

Conclusión

La limitación de tasa con Nginx y Redis proporciona una defensa eficaz contra el abuso de APIs, con la flexibilidad de aplicar diferentes límites por endpoint, por API key o por IP. El módulo limit_req de Nginx es suficiente para la mayoría de casos de uso en un solo servidor, mientras que Redis permite escalar a múltiples instancias con contadores compartidos. La clave para una implementación efectiva es calibrar cuidadosamente los valores de rate y burst basándose en el tráfico legítimo esperado, añadiendo headers informativos para que los clientes puedan adaptar su comportamiento.