Hardening de Nginx en HestiaCP: cómo frenar los DDoS de capa 7 y acelerar los sitios con caché

Guía práctica para crear una plantilla Nginx personalizada en HestiaCP que mitiga los ataques DDoS de capa de aplicación (capa 7) y, a la vez, activa la caché de contenido dinámico para acelerar los sitios.

Hardening de Nginx en HestiaCP: cómo frenar los DDoS de capa 7 y acelerar los sitios con caché

Un ataque DDoS «clásico» busca saturar el ancho de banda o las conexiones de red del servidor (los llamados ataques de capa 3/4, a nivel de red y transporte). Pero existe otra categoría, a menudo más insidiosa porque necesita mucho menos ancho de banda para causar daño: los ataques de capa 7, a nivel de aplicación. Aquí el objetivo no es la conexión de red en sí, sino el servidor web y las aplicaciones que corren sobre él: miles de peticiones HTTP «legítimas» en su forma, pero enviadas tan rápido que agotan la CPU, la memoria y las conexiones a la base de datos, tumbando incluso un servidor cuya conexión a internet está lejos de estar saturada.

Quien gestiona sus propios sitios con HestiaCP, el panel de control de servidor de código abierto basado en Nginx (solo, o como proxy inverso delante de Apache), dispone de una herramienta potente pero poco conocida para defenderse de este tipo de ataques: las plantillas Nginx personalizadas. En esta guía veremos cómo crear una plantilla a medida que añade reglas de mitigación de DDoS de aplicación, y al mismo tiempo cómo aprovechar la caché de Nginx para acelerar el contenido dinámico (PHP, aplicaciones Node.js, contenedores Docker), reduciendo aún más la carga del servidor incluso cuando no hay ningún ataque en curso.

Cómo funcionan las plantillas Nginx en HestiaCP

HestiaCP no escribe «a mano» la configuración de cada sitio: para cada dominio aplica una plantilla, es decir, un archivo modelo con marcadores de posición (placeholders) que el panel sustituye por los valores reales del dominio (nombre de dominio, ruta de archivos, usuario del sistema, etc.) cada vez que el dominio se crea o se «reconstruye». Las plantillas predeterminadas se encuentran en:

/usr/local/hestia/data/templates/web/nginx/          # Nginx usado como proxy inverso delante de Apache
/usr/local/hestia/data/templates/web/nginx/php-fpm/  # Nginx con PHP-FPM (sin Apache)

Cada plantilla es en realidad un trío de archivos con el mismo nombre y extensiones distintas: .tpl (configuración para HTTP), .stpl (configuración para HTTPS) y .sh (un script opcional ejecutado al generarla). Por ejemplo, la plantilla «default» está formada por default.tpl, default.stpl y default.sh.

Regla de oro: nunca modificar las plantillas originales. Cada actualización de HestiaCP sobrescribe los archivos predeterminados, y cualquier personalización hecha directamente sobre ellos se perdería. El procedimiento correcto es copiarlas con un nuevo nombre y trabajar solo sobre la copia:

cd /usr/local/hestia/data/templates/web/nginx/php-fpm/
cp default.tpl hardened.tpl
cp default.stpl hardened.stpl
cp default.sh hardened.sh 2>/dev/null || true

(el ejemplo usa las plantillas para Nginx + PHP-FPM; si el sitio funciona detrás de Nginx + Apache, el mismo procedimiento se aplica en la carpeta nginx/ en lugar de nginx/php-fpm/).

Dentro de los archivos copiados se encuentran marcadores como %domain%, %docroot%, %user%, %home% y %backend_lsnr%: HestiaCP los sustituye automáticamente por los valores correctos para cada dominio que use la plantilla, por lo que deben dejarse intactos mientras se añaden las nuevas directivas alrededor.

Primera regla de hardening: limitar la frecuencia de peticiones (rate limiting)

El módulo ngx_http_limit_req_module, incluido por defecto en Nginx, permite contar las peticiones por dirección IP (o cualquier otra variable) y rechazar las que superen un umbral, devolviendo un error HTTP 429 Too Many Requests. Es la defensa más sencilla y eficaz contra las inundaciones de aplicación: un bot que dispara cientos de peticiones por segundo contra la página de inicio de sesión o el buscador interno del sitio queda bloqueado antes de poder consumir recursos en el backend PHP.

La zona de conteo solo debe declararse una vez, de forma global, dentro del bloque http de la configuración principal de Nginx (/etc/nginx/nginx.conf, o un archivo separado incluido desde ella, por ejemplo /etc/nginx/conf.d/ddos-mitigation.conf):

# 10 MB de memoria compartida bastan para rastrear unas 160.000 IP
limit_req_zone $binary_remote_addr zone=req_zone_general:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=req_zone_login:10m rate=5r/m;

limit_conn_zone $binary_remote_addr zone=conn_zone:10m;

Aquí se crean dos zonas de rate limiting distintas: una genérica (req_zone_general, 20 peticiones por segundo por IP) para todo el sitio, y otra mucho más estricta (req_zone_login, 5 peticiones por minuto) aplicada solo a la página de inicio de sesión, objetivo típico tanto de ataques de fuerza bruta como de DDoS. También se añadió una zona para limit_conn, que limita no la frecuencia sino el número de conexiones simultáneas por IP, útil contra los intentos de saturar el pool de conexiones del backend.

En la plantilla personalizada (hardened.tpl y hardened.stpl), dentro del bloque server { ... }, se aplican las zonas recién definidas:

server {
    listen      %ip%:%web_port%;
    server_name %domain_idn% %alias_idn%;
    root        %docroot%;

    # Limita las conexiones simultáneas por IP individual en todo el sitio
    limit_conn conn_zone 20;

    location / {
        # Límite general: permite breves picos (burst) sin bloquear
        # de inmediato a un usuario que hace clic rápidamente
        limit_req zone=req_zone_general burst=40 nodelay;
        limit_req_status 429;
        try_files $uri $uri/ /index.php?$args;
    }

    location = /wp-login.php {
        limit_req zone=req_zone_login burst=5 nodelay;
        limit_req_status 429;
        include /home/%user%/conf/web/%domain%/nginx.hosting.conf*;
    }

    # ... el resto de la plantilla (gestión de PHP-FPM, páginas de error, etc.)
    # permanece igual que en la plantilla «default» original.
}

Una breve explicación de los parámetros usados, para quien no trabaja con Nginx todos los días:

  • rate=20r/s: la velocidad «sostenida» permitida, en este caso 20 peticiones por segundo por cada IP.
  • burst=40: una especie de «depósito» de peticiones extra que una IP puede acumular y gastar de golpe, para no penalizar a un usuario real que navega rápido entre varias páginas (cada imagen, cada archivo CSS o JS de una página genera su propia petición).
  • nodelay: sin esta opción, Nginx pondría en cola (retrasando) las peticiones que superan el umbral; con nodelay, las peticiones que exceden el burst se rechazan de inmediato con un error 429, evitando acumular conexiones en espera que a su vez consumirían recursos.
  • limit_req_status 429: fija explícitamente el código de error devuelto (por defecto sería 503, pero 429 es semánticamente más correcto y lo reconocen mejor las CDN y las herramientas de monitorización).

Segunda regla de hardening: cortar las conexiones lentas (anti-Slowloris)

Otra familia de ataques de capa 7, conocida como Slowloris, no envía muchas peticiones sino muy pocas, pero mantiene cada conexión abierta el mayor tiempo posible enviando los datos en fragmentos mínimos: de este modo agota el número máximo de conexiones simultáneas que el servidor puede gestionar, sin necesidad de generar nunca un tráfico elevado. El remedio consiste en reducir los tiempos máximos que Nginx concede a un cliente para completar el envío de su petición:

client_body_timeout   10s;
client_header_timeout 10s;
keepalive_timeout     15s;
send_timeout          10s;

# Limita también los tamaños, para evitar subidas anómalas usadas como vector de inundación
client_max_body_size  10m;
large_client_header_buffers 4 8k;

Estos valores (muy inferiores a los 60 segundos predeterminados) también deben incluirse dentro del bloque server { ... } de la plantilla personalizada, o en el bloque global http { ... } si se quieren aplicar a todos los dominios del servidor. Cuidado con exagerar: tiempos demasiado agresivos pueden interrumpir subidas legítimas de archivos grandes desde conexiones lentas o móviles; 10 segundos suelen ser un buen compromiso para sitios normales.

Optimizar la caché de contenido dinámico

Mitigar los DDoS de aplicación no consiste solo en «bloquear a los malos»: también significa hacer que responder a una petición resulte muy barato, de modo que incluso un volumen alto de tráfico (malicioso, o simplemente un pico repentino de popularidad) no ponga al servidor de rodillas. La caché de Nginx para contenido dinámico sirve exactamente para eso: las páginas generadas por PHP (o por una aplicación Node.js detrás de un proxy) se guardan temporalmente en disco y se devuelven idénticas a las peticiones siguientes, sin tener que volver a ejecutar el código de la aplicación y las consultas a la base de datos cada vez.

HestiaCP distingue dos casos, según la pila elegida para el dominio:

Nginx + PHP-FPM: caché FastCGI

Para los dominios que usan Nginx con PHP-FPM (sin Apache), se utiliza la caché FastCGI. En la configuración global (el mismo archivo /etc/nginx/conf.d/ddos-mitigation.conf usado antes, o nginx.conf) se define la caché:

fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=FASTCGICACHE:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";

En la plantilla personalizada, en el bloque que gestiona PHP-FPM, hay que añadir la línea de activación después de la línea include /etc/nginx/fastcgi_params; ya presente en la plantilla original:

location ~ \.php$ {
    try_files $uri =404;
    fastcgi_pass   %backend_lsnr%;
    fastcgi_index  index.php;
    include        /etc/nginx/fastcgi_params;

    # Activa la caché para este dominio (el archivo lo crea HestiaCP
    # cuando la caché FastCGI se activa desde el panel para el dominio)
    include /home/%user%/conf/web/%domain%/nginx.fastcgi_cache.conf*;

    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Sin embargo, no todas las páginas deben almacenarse en caché: un carrito de compra, un área personal tras iniciar sesión, o un endpoint que genera contenido distinto para cada usuario no deben servir a un visitante la página guardada para otro. Esas rutas se excluyen fijando la variable $no_cache:

set $no_cache 0;
if ($request_uri ~* "/(carrito|checkout|wp-admin|my-account)") {
    set $no_cache 1;
}
if ($http_cookie ~* "wordpress_logged_in") {
    set $no_cache 1;
}

Una vez creada la plantilla, la caché FastCGI para un dominio específico se activa desde la página de edición del dominio en el panel de HestiaCP (en la sección dedicada a la plantilla Nginx/backend), seleccionando la plantilla recién creada. Para vaciar la caché en cualquier momento (por ejemplo, tras actualizar el sitio), basta usar el botón «Purge Nginx Cache» en la misma página, o desde la línea de comandos:

v-purge-nginx-cache usuario dominio.tld

Nginx + Apache: caché proxy

Cuando, en cambio, el dominio usa Nginx como proxy inverso delante de Apache (una configuración muy común en HestiaCP para admitir .htaccess y mod_php), la caché FastCGI no está disponible: en su lugar se usa la caché proxy de Nginx, que funciona de manera conceptualmente idéntica pero se aplica al tráfico proxificado (también funciona bien para aplicaciones Node.js o contenedores Docker detrás del proxy). Las plantillas personalizadas para este fin siguen una convención de nombre dedicada: caching-nombrepersonalizado.tpl, caching-nombrepersonalizado.stpl y caching-nombrepersonalizado.sh, creadas siempre copiando la plantilla de partida equivalente que ofrece HestiaCP en la carpeta nginx/ (no nginx/php-fpm/), y luego seleccionables como «plantilla de caché» dedicada desde la página del dominio.

Una advertencia sobre los WebSockets

Tanto la plantilla estándar como las de caché proxy incluyen por defecto la directiva proxy_hide_header Upgrade;, añadida originalmente para evitar un problema de compatibilidad con Safari. Sin embargo, esta línea también bloquea las conexiones WebSocket (usadas, por ejemplo, por chats en tiempo real, notificaciones push del navegador o algunos paneles de administración): si la aplicación alojada las necesita, hay que eliminarla de la plantilla personalizada.

Aplicar la plantilla y comprobar que funciona

Tras guardar los cambios en los archivos .tpl/.stpl, hay que seleccionar la nueva plantilla para el dominio deseado: en el panel de HestiaCP se abre la página de edición del dominio (Web > nombre de dominio > Editar), se despliegan las opciones avanzadas y se elige la plantilla recién creada (hardened, en nuestro ejemplo) en el menú desplegable de la plantilla Nginx/Proxy. Como alternativa, desde la línea de comandos se puede forzar la reconstrucción de la configuración para todos los dominios de un usuario con:

v-rebuild-user nombredeusuario

Antes de poner todo en producción, conviene validar la sintaxis y recargar Nginx manualmente:

nginx -t && systemctl reload nginx

Para comprobar que el rate limiting funciona realmente, se puede simular un pequeño flujo de peticiones repetidas desde la propia máquina (sin confundirlo con una verdadera prueba de carga, que debe realizarse con cautela y solo sobre sistemas de los que se es propietario):

for i in $(seq 1 80); do
  curl -o /dev/null -s -w "%{http_code}\n" https://dominio.tld/
done | sort | uniq -c

Si la configuración es correcta, las primeras peticiones (dentro del burst configurado) devolverán 200, mientras que las siguientes, una vez superado el umbral, responderán 429. Esa es la señal de que la zona de rate limiting está haciendo su trabajo.

Algunas advertencias antes de activarlo todo en producción

El rate limiting basado en la dirección IP tiene una limitación intrínseca que conviene tener en cuenta: muchos usuarios que navegan desde redes corporativas, universidades o ciertas redes móviles comparten la misma IP pública mediante NAT, y umbrales demasiado bajos corren el riesgo de bloquear a usuarios legítimos junto con posibles bots. Por eso es recomendable empezar con valores prudentes (como los 20 r/s del ejemplo), monitorizar los registros de errores 429 durante unos días, y solo después endurecer los umbrales donde sea necesario. También es buena práctica excluir del rate limiting las direcciones IP de las propias herramientas de monitorización (comprobadores de disponibilidad, CDN, servicios de copia de seguridad), usando una directiva geo o un simple bloque map antes de la zona de limit_req, para evitar falsas alarmas.

Por último, el rate limiting a nivel de aplicación descrito en esta guía constituye una primera línea de defensa eficaz contra los DDoS de capa 7, pero no sustituye una protección dedicada a nivel de red (capa 3/4) frente a ataques volumétricos muy grandes: para esos casos siguen siendo indispensables servicios de mitigación aguas arriba del servidor, como una CDN con protección anti-DDoS o el filtrado que ofrecen algunos proveedores de hosting y de conectividad.

Fuentes oficiales

Compartir

¿Cuál es tu reacción?

Me gusta Me gusta 0
No me gusta No me gusta 0
Me encanta Me encanta 0
Divertido Divertido 0
Enfadado Enfadado 0
Triste Triste 0
¡Vaya! ¡Vaya! 0