Primero, asegúrate de que de verdad es un ataque
El error más caro durante una caída es tratar el problema equivocado. "La web va lenta" es un síntoma que comparten una inundación genuina, un despliegue que metió una consulta sin índice, un cron que empezó a volcar la base de datos en el mismo minuto de cada hora, un crawler que descubrió tu búsqueda facetada, un enlace que llegó a la portada de algún sitio grande, y un disco que se llenó. Todos ellos son indistinguibles desde un navegador, y las respuestas son mutuamente excluyentes: no quieres estar limitando la tasa a clientes reales porque una migración se olvidó de un índice.
Tres preguntas los separan en menos de un minuto. ¿El volumen de tráfico es genuinamente anómalo, o el tráfico es normal y el servidor va lento? Una inundación se manifiesta como un cambio brusco en los paquetes o las peticiones por segundo; un mal despliegue se manifiesta como un volumen de peticiones normal y un tiempo de respuesta hundido. ¿Ha cambiado algo por tu parte en la última hora? Revisa el log de despliegues antes que el firewall —los incidentes autoinfligidos superan a los ataques por un margen amplio en infraestructuras pequeñas. ¿La carga está repartida por todo el sitio, o concentrada en una sola ruta? Las inundaciones reales suelen ser indiscriminadas o apuntar a la portada; un endpoint caro siendo machacado por un centenar de clientes se parece más a un abuso que a un DDoS, y tiene un arreglo mucho más barato.
Responde a eso, y sigue adelante. El resto de esta guía asume que la respuesta fue: el volumen es anómalo, no ha cambiado nada por tu parte, y la máquina se está ahogando.
SP·02El triaje de cinco minutos
Solo hay dos modos de fallo que importan, y necesitan respuestas opuestas. O la tubería está llena —los paquetes llegan más rápido de lo que tu uplink o tu kernel pueden procesarlos, y tu servidor está perdiendo tráfico antes de que ninguno de tus programas lo vea— o la tubería va bien y la aplicación está agotada, porque llegan peticiones bien formadas más rápido de lo que puede responderlas. Confundirlos malgasta la hora: ajustar nginx no hace nada contra un enlace saturado, y comprar más ancho de banda no hace nada contra una inundación de peticiones.
Distínguelos comparando dos números. Mira los contadores de la interfaz y el reparto de CPU al mismo tiempo. Si los bytes de rx están clavados cerca de la velocidad de tu puerto, si los contadores de dropped u overrun están subiendo, y si el tiempo se va en interrupciones de software en lugar de en tu aplicación, la inundación está en la capa 3 o 4 y es un problema de capacidad. Si el ancho de banda es normal pero el pool de workers está saturado, las conexiones se están encolando, y el log de acceso está lleno de peticiones que parecen legítimas, es capa 7 y es un problema de filtrado.
Después, clasifica el caso de capa 3/4 un paso más, porque los subtipos se comportan de forma distinta. Una inundación SYN se ve como decenas de miles de sockets semiabiertos en SYN-RECV; el kernel lo maneja bien en cuanto están activadas las syncookies. Una inundación UDP o de amplificación se ve como un volumen entrante enorme en puertos en los que ni siquiera escuchas —reflexiones de DNS, NTP, memcached, CLDAP— y nada de lo que ejecutes puede ayudar, porque el daño ya está hecho para cuando los paquetes llegan a tu NIC. Una inundación de fragmentación o de paquetes en bruto se ve como un número alto de paquetes por segundo con un ancho de banda modesto, lo cual ahoga la CPU en lugar del enlace. Anota cuál de ellas tienes antes de tocar un archivo de configuración.
Lo que no puedes arreglar desde dentro de la máquina
Esta es la parte que se saltan la mayoría de los artículos, y es la parte que decide si tu hora es productiva o no. Una regla de firewall en el objetivo no salva un uplink saturado. Tu DROP de iptables se ejecuta en la máquina que está al final de la tubería —el paquete ya ha cruzado el enlace de tránsito, ya ha consumido el ancho de banda que estás pagando, y ya ha desplazado el paquete de un usuario real. Descartarlo localmente protege a tu aplicación de malgastar ciclos, que ya es algo, y no protege tu ancho de banda absolutamente nada.
El techo honesto para un único servidor es, más o menos, el menor de dos números: la velocidad del puerto al que está conectado, y los paquetes por segundo que su CPU puede clasificar. Un puerto de 1 Gbps está lleno a 1 Gbps sin importar lo elegante que sea tu conjunto de reglas, y una inundación modesta de paquetes pequeños puede agotar un par de núcleos en la gestión de interrupciones mucho antes de que la cifra de ancho de banda parezca alarmante. Pasado ese punto, lo único que ayuda es un dispositivo más arriba en la cadena que tenga más capacidad que el ataque y descarte el tráfico antes de que llegue siquiera a tu enlace. Eso es lo que es el scrubbing, y es la razón por la que cada máquina de nuestra flota está detrás de 1.5 Tbps de mitigación permanente en lugar de un firewall más grande.
El corolario importa tanto como lo anterior: si no tienes protección upstream, la respuesta de tu proveedor ante un ataque volumétrico grande es aplicarle un null-route a tu dirección, porque la alternativa es degradar a todos los demás clientes de ese enlace. Eso no es maldad, es aritmética —y significa que el atacante gana haciendo que salgas caro, no rompiendo nada. Saber de antemano si la mitigación está incluida en tu plan o es un extra de pago que nunca activaste es una comprobación de cinco minutos que merece la pena hacer hoy, no durante el incidente. La nuestra viene incluida en todos los planes, que es el único arreglo que ayuda a las 3 de la madrugada.
SP·04Capa 7: la inundación que parece tráfico
Una inundación en la capa de aplicación es más difícil porque cada petición individual es legítima. Una inundación HTTP bien construida completa el handshake TCP, negocia el TLS, envía un GET / válido con un user-agent creíble, y lee la respuesta. Ningún paquete en ella está malformado. Lo que te mata es la aritmética: al atacante enviar una petición casi no le cuesta nada, y a ti te cuesta una consulta a la base de datos, el renderizado de una plantilla y cien milisegundos de un worker que ahora no está sirviendo a nadie más.
Las señales están en tu propio log de acceso, y suelen ser evidentes en cuanto las buscas en lugar de solo mirarlas. Cadenas de consulta que evitan la caché —miles de peticiones a /?1234567, cada una una URL única que derrota cualquier caché que tengas— son la firma más común con diferencia. Una distribución de user-agents sin cola larga: el tráfico real es una mezcla desordenada de cientos de versiones de navegador, y una inundación suele ser tres cadenas repetidas un millón de veces, o una que ningún usuario real utiliza. Un campo de referrer idéntico en todas partes. Peticiones que se saltan tus recursos estáticos por completo —un navegador real pide el CSS, las fuentes y las imágenes después del HTML; un cliente de inundación pide el HTML y se va. Y una distribución de origen demasiado plana: una botnet repartida entre diez mil direcciones residenciales que envían cada una dos peticiones por segundo parece popularidad hasta que te das cuenta de que la tasa por dirección es sospechosamente uniforme.
Luego está la variante que casi no necesita tráfico en absoluto: el ataque lento. Unos pocos cientos de conexiones que se abren, envían una cabecera cada veinte segundos y nunca terminan ocuparán todos los workers que tengas mientras tu gráfica de ancho de banda se mantiene plana. El arreglo no es un límite de tasa —la tasa de peticiones es minúscula—, son timeouts agresivos de cabecera y de cuerpo, y por eso aparecen en el primer bloque de configuración de más abajo en lugar de como una ocurrencia tardía.
SP·05Cuatro controles, en orden de efecto por minuto
Bajo presión, haz primero lo que tenga más impacto. El orden de abajo no es arbitrario; es aproximadamente descendente según cuánta carga quita cada uno por minuto de tu atención.
- Sirve algo barato. Una micro-caché de treinta segundos delante de tu aplicación convierte mil peticiones idénticas por segundo en una sola petición al origen y 999 lecturas de memoria. Es la palanca individual más grande en casi cualquier inundación HTTP, cuesta un bloque de directivas, y para tráfico anónimo es casi siempre seguro. Añade
proxy_cache_lockpara que un fallo de caché no mande una estampida al backend. - Limita la concurrencia y acorta los timeouts.
limit_connpor dirección más unos timeouts ajustados de cabecera, cuerpo y keepalive mata los ataques lentos de raíz e impide que un solo cliente acapare tu pool de workers. Este es el control que menos les cuesta a los usuarios reales. - Limita la tasa, con un burst.
limit_reqcon un burst sensato es preciso pero más lento de ajustar, y es el que genera falsos positivos si lo configuras desde el pánico en lugar de desde tu propio baseline. Necesitas conocer tus peticiones por segundo normales por cliente antes de poder elegir un número —por eso el post-mortem al final de esta guía importa más de lo que parece. - Bloquea, de forma precisa y a regañadientes. Descartar redes específicas funciona cuando las fuentes están concentradas y no hace nada cuando no lo están. Además envejece mal: cada bloqueo que añades durante un incidente es un cliente al que quizá estés rechazando en silencio dentro de tres meses. Usa un set con un timeout para que las reglas caduquen solas.
Fíjate en lo que no está en la lista: banear direcciones IP una a una a mano, reiniciar el servidor web repetidamente, y desactivar el firewall para "ver si ayuda". Lo primero es demasiado lento para importar contra una fuente distribuida, lo segundo tira todas las conexiones calientes que tenías, y lo tercero es cómo un incidente se convierte en un compromiso.
SP·06No lo combatas desde la máquina que aloja tus datos
Cada control de los anteriores vale más cuando se ejecuta en algún sitio que no sea la máquina que aloja tu base de datos. Si tu edge es un nodo aparte, la inundación termina en una máquina cuyo único trabajo es terminar inundaciones: cachea, limita la tasa y descarta con la dirección real del cliente en la mano, y el origen solo llega a ver el resto, pequeño y filtrado, a través de un túnel privado. Cuando el edge se cae, lo reemplazas en 15 min y no pierdes nada, porque no hay nada en él. Cuando el origen se cae, tienes una caída y una restauración.
Esa separación también cierra el bypass que convierte a la mayoría de la mitigación en decorativa. Si el origen todavía tiene un listener público, un atacante que encuentre su dirección —a través del DNS pasivo, una entrada de transparencia de certificados, un registro MX o una vista previa de enlace— puede apuntar más allá de todos los controles que configuraste y golpear la aplicación directamente. Esto es lo bastante común como para tratarlo como el estado por defecto de cualquier sitio "protegido" hasta que se demuestre lo contrario. Construir la versión que aguanta es una guía en sí misma: un proxy inverso offshore sin ningún listener público en el origen en absoluto.
Una advertencia sobre el punto álgido de un incidente: esto es arquitectura, no primeros auxilios. Levantar un edge, mover el DNS y reconstruir un túnel mientras estás bajo ataque es un trabajo de dos horas hecho mal bajo presión, y el cambio de DNS por sí solo no hará efecto durante todo el tiempo que diga tu TTL. Si lo tienes, úsalo. Si no lo tienes, pasa la hora con los controles que tengas, y constrúyelo en la semana tranquila de después —que es exactamente cuando nadie lo hace.
SP·07El día en que eres el reflector, no el objetivo
Hay una segunda versión de este incidente en la que tu servidor no es la víctima y nadie te lo dice. Un resolver abierto, un daemon NTP expuesto, un memcached sin autenticación en una interfaz pública, un respondedor SSDP o CLDAP dentro de un contenedor —cada uno de estos responde a una pequeña petición falsificada con una respuesta mucho más grande, dirigida a otra persona. Desde tu lado, los síntomas están invertidos: el ancho de banda saliente es alto, el entrante es modesto, tu aplicación va bien, y la primera señal real es un aviso de abuso o un puerto suspendido.
La comprobación lleva un minuto y pertenece al mismo runbook, porque es el mismo comando que ya ejecutaste durante el triaje. ss -tulpn no debería listar nada vinculado a una dirección pública que tú no hayas puesto ahí deliberadamente, y los servicios UDP merecen sospecha especial porque son los que amplifican. Un resolver recursivo debe estar vinculado a localhost o a una dirección del túnel; memcached y Redis no deben ser alcanzables desde internet bajo ningún concepto; y cualquier contenedor que publique un puerto con -p 0.0.0.0: acaba de abrir un agujero en el firewall que configuraste, porque Docker escribe sus propias reglas por delante de las tuyas. Eso último sorprende a la gente absolutamente siempre.
La misma forma cubre las inundaciones salientes desde una máquina que ya ha sido comprometida, que es la otra razón por la que un proveedor de repente le aplica un null-route a una dirección. Si tu gráfica de salida está alta y tu aplicación está inactiva, deja de leer configuración y empieza a revisar procesos —eso es una intrusión, no un problema de capacidad, y la respuesta es reconstruir la máquina desde una copia de seguridad fiable en lugar de intentar filtrarla.
SP·08Cuando termina: el post-mortem y el kit permanente
Los ataques paran. Normalmente el atacante se aburre, a veces la mitigación lo vuelve inútil, y de vez en cuando era una suscripción de booter de duración fija que simplemente caducó. La tentación en ese momento es dejarlo todo exactamente como está e irte a dormir, que es como un límite de tasa temporal se convierte en un 429 permanente, olvidado y silencioso para todo un país nueve meses después. Dedica veinte minutos a cerrar el ciclo mientras lo tienes fresco.
Merece la pena producir tres artefactos. Un baseline: tus peticiones por segundo normales, tu tasa normal por cliente, tu ancho de banda normal en hora punta. Sin esos números, cada límite que pongas durante el siguiente incidente es una suposición, y la mitad de ellas se equivocarán en la dirección que perjudica a los clientes. Una lista de reversión: todo lo que cambiaste, con una fecha y un motivo, para que la configuración de emergencia no se convierta calladamente en la permanente. Un runbook que tenga cuatro comandos de largo y viva en algún sitio al que puedas llegar cuando el sitio esté caído —no en el servidor, y no solo en tu propia cabeza.
Después cierra los dos huecos estructurales, porque aquí es donde encaja esta capa dentro de la pila. La mitigación es lo que absorbe el volumen, un edge es lo que mantiene la inundación lejos de tus datos, y la máquina que hay detrás de ellos todavía tiene que estar correctamente endurecida y tener copias de seguridad fuera de sitio y restaurables, porque el incidente después de este puede que no sea en absoluto una inundación. Un servidor que sobrevive a un DDoS y pierde su disco un mes después nunca fue resiliente —tuvo suerte, dos veces.
SP·09Paso a paso
-
01
Confírmalo antes de cambiar nada
Consigue una imagen honesta de la máquina antes de tocar un archivo de configuración. Estás buscando un cambio brusco en paquetes o peticiones, y dónde se está yendo el tiempo —una aplicación privada de CPU por interrupciones de software es un incidente muy distinto de una aplicación esperando a una base de datos.
# is the box alive, and where is the time going? uptime # load average against your core count vmstat 1 5 # 'in' and 'cs' high, 'id' near zero = packet work mpstat -P ALL 1 3 # %soft pinned on one core = interrupt saturation # is the pipe full, or just busy? ip -s link show eth0 # rx bytes, and the errors/dropped counters ethtool eth0 | grep -i speed # requests per second from your own log, minute by minute tail -n 20000 /var/log/nginx/access.log \ | awk -F'[][]' '{print $2}' | cut -d: -f2-4 | uniq -c | tail -5Antes de concluir que es un ataque, revisa tu propio log de despliegues y tu tabla de cron. Las caídas autoinfligidas son más frecuentes que las inundaciones en un único VPS, y se ven igual desde fuera.
-
02
Mide la forma del tráfico en sesenta segundos
Ahora clasifícalo. Tres preguntas: cuántas fuentes distintas, qué protocolo y qué estado, y —si es HTTP— qué rutas y qué agentes. Las respuestas deciden a qué control recurres, y tardan cosa de un minuto en recopilarse.
# top source addresses on the wire right now timeout 20 tcpdump -nn -i eth0 -c 20000 2>/dev/null \ | awk '{print $3}' | rev | cut -d. -f2- | rev \ | sort | uniq -c | sort -rn | head -20 # TCP state census: a wall of SYN-RECV is a SYN flood ss -tan | awk '{print $1}' | sort | uniq -c | sort -rn # layer 7: talkers, paths, agents over the last 50k requests L=/var/log/nginx/access.log tail -n 50000 $L | awk '{print $1}' | sort | uniq -c | sort -rn | head -20 tail -n 50000 $L | awk '{print $7}' | sort | uniq -c | sort -rn | head -20 tail -n 50000 $L | cut -d'"' -f6 | sort | uniq -c | sort -rn | head -10Lee el resultado contrastándolo con las señales: cadenas de consulta únicas en una sola ruta, una lista de user-agents sin cola larga, ninguna petición de tus recursos estáticos, o una tasa por dirección extrañamente uniforme. Si el ancho de banda es alto en puertos en los que no escuchas, para aquí —eso es una inundación volumétrica y el paso seis es el único paso que importa.
-
03
Sirve algo barato, y limita las conexiones
Primero lo de mayor impacto. Una micro-caché de treinta segundos colapsa una inundación de peticiones anónimas idénticas en una sola petición al origen, y unos timeouts ajustados matan los ataques lentos que un límite de tasa no puede ver. Pon los dos en el bloque
http, y después recarga en lugar de reiniciar para conservar tus conexiones calientes.# /etc/nginx/nginx.conf — http block limit_conn_zone $binary_remote_addr zone=perip:10m; limit_req_zone $binary_remote_addr zone=flood:20m rate=10r/s; client_header_timeout 10s; client_body_timeout 10s; send_timeout 10s; keepalive_timeout 20s; reset_timedout_connection on; proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=hot:64m max_size=2g inactive=10m use_temp_path=off;# the server block — cap concurrency, serve the cached copy limit_conn perip 20; location / { proxy_cache hot; proxy_cache_valid 200 301 302 30s; proxy_cache_lock on; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; add_header X-Cache $upstream_cache_status; proxy_pass http://127.0.0.1:8080; }nginx -t && systemctl reload nginx curl -sI https://example.com/ | grep -i x-cache # want: HIT on the second call
-
04
Limita la tasa del cliente real, no la de tu propio proxy
Si algo se sienta delante de nginx, todas las peticiones llegan desde una sola dirección y un límite por dirección le pondrá el freno al proxy en lugar de al atacante —o directamente lo baneará y tirará el sitio abajo justo mientras lo estás defendiendo. Confía en la cabecera reenviada solo desde la dirección del proxy, nunca desde internet, y luego aplica el límite.
# /etc/nginx/conf.d/realip.conf — the tunnel or edge address only set_real_ip_from 10.66.0.1; real_ip_header X-Forwarded-For; real_ip_recursive off;
# burst absorbs bursty humans; nodelay keeps the page fast for them location / { limit_req zone=flood burst=20 nodelay; limit_req_status 429; } # the expensive paths get a much tighter bucket of their own location ~ ^/(search|login|register|api/) { limit_req zone=flood burst=5; limit_req_status 429; }Después observa lo que acabas de hacer:
tail -f /var/log/nginx/error.log | grep limiting. Si las direcciones que se están limitando parecen tus clientes, la tasa es demasiado baja —súbela. Un límite que bloquea a usuarios reales es una caída que te has provocado tú mismo. -
05
Bloquea de forma precisa, y dale una caducidad a cada bloqueo
Solo merece la pena hacerlo cuando el paso dos mostró fuentes concentradas. Usa un set, no mil reglas —una búsqueda en
ipsetes de tiempo constante, una cadena larga deiptablesse recorre en cada paquete y se convierte en su propia denegación de servicio. Dale un timeout a cada entrada para que la emergencia de hoy no se convierta en la lista de bloqueo silenciosa del año que viene.ipset create flood hash:net timeout 3600 -exist iptables -I INPUT -m set --match-set flood src -j DROP # feed it from the census: /24s you actually verified, not guesses for n in 203.0.113.0/24 198.51.100.0/24; do ipset add flood $n -exist; done # SYN flood: let the kernel do the part it is good at sysctl -w net.ipv4.tcp_syncookies=1 sysctl -w net.ipv4.tcp_max_syn_backlog=8192 sysctl -w net.core.somaxconn=8192 ipset list flood | head -20 # keep a copy of this for the post-mortem
Resiste la tentación de geobloquear un país entero a menos que puedas nombrar a los clientes que estás dejando fuera. Y nunca hagas
ufw disablepara probar una teoría: una máquina sin firewall bajo ataque activo es cómo un incidente de ancho de banda se convierte en una brecha. -
06
Escala a la capa que de verdad puede absorberlo
Si los contadores de la interfaz dicen que el enlace está saturado, o los drops están subiendo mientras tu CPU está inactiva, has llegado al techo de lo que puedes hacer en el servidor. Confirma esa lectura, y luego escala en lugar de seguir ajustando.
# drops in the stack itself — second column is 'dropped' awk '{print strtonum("0x" $2)}' /proc/net/softnet_stat | paste -sd+ | bc # interface-level loss against link speed ip -s link show eth0 | sed -n '3,6p'Con el scrubbing upstream en su sitio, normalmente no hay nada que hacer: la detección está siempre activa y la inundación se descarta en la red antes de llegar a tu puerto —en nuestra flota eso son 1.5 Tbps de capacidad por delante de cada plan, así que el incidente muchas veces solo se ve como una gráfica después del hecho. Si el ataque es una inundación HTTP bien construida en lugar de una volumétrica, ahí es donde entra un blindaje L7, porque las inundaciones de peticiones son indistinguibles de los usuarios a nivel de paquete y hay que juzgarlas más arriba. Si no tienes ninguna mitigación en absoluto, tus opciones realistas son moverte detrás de un edge que sí la tenga, o esperar —y planear la primera antes del próximo ataque.
-
07
Cierra el ciclo: verifica, revierte y luego anótalo
Verifica desde fuera de la máquina, no desde una shell en ella. Después deshaz las medidas de emergencia de forma deliberada, conserva las que ya eran buena idea desde el principio, y anota los números para que el próximo incidente empiece desde un baseline en lugar de desde una suposición.
# from somewhere else entirely: is the site healthy for a normal user? curl -s -o /dev/null -w 'code=%{http_code} ttfb=%{time_starttransfer}s\n' \ https://example.com/ # did you leave a limit that is biting real people? grep -c 'limiting requests' /var/log/nginx/error.log # what is still blocked, and when does it expire? ipset list flood | head -30Conserva la caché, los timeouts y las syncookies —esas son mejoras permanentes. Revierte los límites de tasa agresivos a tu baseline medido más un margen, y deja que las entradas de ipset caduquen solas. Después escribe los cuatro comandos de los pasos uno y dos en un runbook guardado en algún sitio que no sea este servidor, junto a tus peticiones por segundo normales y tu ancho de banda de pico normal. El próximo ataque es un incidente más corto solo porque esos dos números existen.


