Detectar un ataque DDoS: cómo reconocerlo sin margen de duda
No toda sobrecarga es un ataque. Con ss, los contadores de paquetes, los mensajes del kernel y los logs del servidor web distingues sin margen de duda un ataque DDoS de un pico de carga o de un fallo de software.
El servicio deja de responder, el indicador de carga está por las nubes y en el chat aparece la pregunta: ¿nos están atacando? Esa pregunta se puede medir. Incluso se puede medir bastante rápido, siempre que sepas qué cuatro cifras mirar, en qué orden, y qué contraprueba convierte una sospecha en un diagnóstico.
Este artículo trata únicamente del diagnóstico. No se trata de repeler un ataque, sino de distinguirlo sin margen de duda de un problema de carga, de un fallo de software o simplemente del éxito. Si quieres saber qué es técnicamente un ataque DDoS, lo encuentras en ¿Qué es un ataque DDoS?. Si quieres actuar una vez hecho el diagnóstico, sigue en Proteger el servidor frente a ataques DDoS.
Cuatro sospechosos, un solo síntoma
"El servidor va lento" no es un diagnóstico, sino un síntoma con al menos cuatro causas plausibles. Antes de teclear un solo comando deberías saber qué patrones quieres distinguir en realidad.
| Observación | Ataque | Avalancha real de visitantes | Fallo de software |
|---|---|---|---|
| Inicio | de golpe, en cuestión de segundos | subida a lo largo de minutos, a menudo con curva | justo después de un despliegue, un cronjob o una actualización |
| Carga de red entrante | alta o extrema, a menudo muchos paquetes pequeños | moderada, la saliente claramente mayor que la entrante | sin nada llamativo |
| Conexiones por IP de origen | muchísimas desde pocas IP, o muy pocas desde muchísimas IP | repartidas de forma uniforme, pocas por IP | normal |
| Referrer en el log | casi siempre vacío | portales de noticias, redes sociales, buscadores | normal |
| Respuesta a través de 127.0.0.1 | rápida (el problema es la red) | lenta (el problema es la aplicación) | lenta o con error |
| Tras reiniciar el servicio | la carga vuelve de inmediato | la carga vuelve, el cuadro de errores no cambia | el problema desaparece a menudo durante minutos |
El cuarto sospechoso falta en esta tabla porque no necesita una columna propia: las tareas programadas. Backups, reindexaciones, rotaciones de logs y actualizaciones de paquetes se ejecutan a horas previsibles. Un vistazo a systemctl list-timers y a las crontabs cuesta diez segundos y cierra un número sorprendente de sospechas de ataque.
Los primeros 60 segundos: cuatro cifras
Recoge cuatro valores en este orden. Lo revelador es la combinación, ningún valor por sí solo lo es.
cat /proc/loadavg
ss -s
cat /proc/net/dev
curl -o /dev/null -s -w '%{time_total}\n' http://127.0.0.1/
La interpretación:
- Carga alta, carga de red alta, muchas conexiones a medio abrir, pero 127.0.0.1 responde en milisegundos: el problema está delante de la aplicación, es decir, en la capa de red. Esa es la imagen clásica de un ataque.
- Carga alta, carga de red normal, 127.0.0.1 responde despacio: aplicación o base de datos. No es un ataque, sino trabajo para el desarrollador.
- Carga baja, carga de red alta: muy sospechoso. Una tormenta de paquetes que ni siquiera llega hasta la aplicación consume poca CPU y mucha línea.
- Todo bajo y aun así el servicio no responde desde fuera: mira más abajo, en el apartado sobre los casos sin rastro medible.
Sobre el valor de carga en sí conviene una precisión: /proc/loadavg cuenta también los procesos que esperan por entrada y salida. Un valor de 40 con cuatro núcleos puede ser tanto un ataque como un disco saturado. vmstat 1 5 separa ambas cosas con limpieza: la columna r muestra los procesos listos para ejecutarse, b los bloqueados y wa la proporción de tiempo de espera.
Contar conexiones: ss en lugar de netstat
Casi todas las guías que circulan por la red empiezan con netstat. En un sistema actual eso acaba así:
Command 'netstat' not found, but can be installed with:
apt install net-tools
netstat pertenece al paquete net-tools, que ya no viene incluido en las instalaciones estándar de Debian 12, Debian 13, Ubuntu 22.04 y Ubuntu 24.04. En la familia Red Hat (AlmaLinux, Rocky, RHEL, Oracle Linux) tampoco. Puedes instalarlo después, pero tiene más sentido usar ss de iproute2: está presente en prácticamente cualquier servidor, es bastante más rápido cuando hay muchas conexiones y ofrece exactamente la misma información.
ss -s
Para que funcionen todos los comandos que siguen necesitas un puñado de paquetes, y sus nombres cambian según la familia de distribución. Ese es el punto en el que una guía copiada se atasca con más frecuencia en AlmaLinux, ya en la primera línea:
| Herramienta | Debian y Ubuntu | AlmaLinux, Rocky, Oracle Linux |
|---|---|---|
| ss, nstat, ip | iproute2 | iproute |
| netstat | net-tools | net-tools |
| vmstat, free, top | procps | procps-ng |
| sar | sysstat | sysstat |
| dig | dnsutils | bind-utils |
| tcpdump | tcpdump | tcpdump |
En Debian y Ubuntu, por tanto, apt-get install -y iproute2 net-tools procps sysstat dnsutils tcpdump; en la familia Red Hat, dnf -y install iproute net-tools procps-ng sysstat bind-utils tcpdump. Ahí es mejor que no añadas curl a la lista: ya está instalado como curl-minimal, y el paquete completo entra en conflicto con él (curl-minimal ... conflicts with curl). Si aun así lo necesitas, te saca del paso dnf -y --allowerasing install curl.
La primera línea indica el número total de sockets, la línea TCP lo desglosa: estab, closed, orphaned, timewait. Un valor alto de timewait por sí solo no es un indicio de ataque, sino la consecuencia normal de muchas conexiones HTTP cortas.
Más interesante es el reparto entre los estados:
ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn
Lo llamativo es un bloque grande en SYN-RECV. Esas conexiones se iniciaron pero nunca se confirmaron, que es justo el patrón de un SYN flood. Se pueden contar directamente:
ss -Htn state syn-recv | wc -l
La trampa de las columnas con la que fracasan la mayoría de los one-liners
En cuanto le pasas a ss un filtro de estado, desaparece la columna de estado en la salida. El extremo remoto pasa entonces a la columna 4 en lugar de la 5. Justo por eso los one-liners copiados de los foros devuelven disparates con regularidad: cuentan puertos en lugar de direcciones IP, o imprimen líneas vacías. Regla para recordar: sin filtro, el extremo remoto es $5; con filtro, $4. La opción -H suprime además la línea de cabecera, de modo que wc -l da el número correcto sin corregir nada.
Conexiones por IP de origen, a prueba de IPv6:
ss -Htn state established | awk '{print $4}' | sed 's/:[^:]*$//' | sort | uniq -c | sort -rn | head -20
El sed recorta solo los dos puntos finales junto con el puerto. El enfoque extendido con cut -d: -f1 funciona con IPv4, pero parte las direcciones IPv6 después del primer bloque y deja el análisis sin ningún valor. Con IPv6 los corchetes se quedan puestos, cosa que no molesta al contar.
La interpretación exige mesura. Cien conexiones desde una sola IP pueden ser un ataque, pero igual de bien un NAT corporativo, un NAT de operador móvil o un reverse proxy delante de tu servidor. Si hay una red de distribución de contenidos o un balanceador de carga por delante, de todos modos solo verás sus direcciones y tendrás que recurrir a X-Forwarded-For en el log del servidor web.
Para UDP vale el mismo esquema con -u. Y para saber siquiera qué servicio escucha en qué puerto:
ss -tulnp
Medir bien la carga de red y la tasa de paquetes
El ancho de banda en megabits es la cifra que todo el mundo menciona. La más reveladora es la tasa de paquetes. Un ataque con 200.000 paquetes diminutos por segundo deja un servidor fuera de juego aunque el ancho de banda parezca inofensivo.
Primero el nombre de la interfaz, porque eth0 ni de lejos es la respuesta correcta en todas partes. Nombres habituales son ens3, enp1s0 o eth0:
ip -br link
Después, dos mediciones separadas por un segundo, sin ningún paquete adicional. El nombre de la interfaz se obtiene de la ruta por defecto en lugar de escribirlo a mano:
IF=$(ip -o route get 1.1.1.1 | awk '{print $5}')
A=$(cat /sys/class/net/$IF/statistics/rx_packets) || exit 1; sleep 1; B=$(cat /sys/class/net/$IF/statistics/rx_packets); echo "$((B-A)) paquetes/s entrantes en $IF"
Ese rodeo tiene un motivo muy concreto. Si escribes aquí eth0 de forma fija y la interfaz se llama en realidad ens3 o enp1s0, como ocurre en la mayoría de los servidores KVM, el comando imprime dos veces cat: /sys/class/net/eth0/statistics/rx_packets: No such file or directory, pero acto seguido informa tan tranquilo de 0 paquetes/s entrantes y termina con código de retorno 0. En un artículo sobre detección de ataques esa es la variante más peligrosa de todas: lees cero paquetes, das la señal de que todo está bien y el ataque sigue en marcha. El || exit 1 impide exactamente eso.
Lo mismo con rx_bytes da los bytes por segundo. A partir de ambos valores calculas el tamaño medio de paquete, y eso delata el tipo de ataque:
- por debajo de 100 bytes de media con una tasa de paquetes muy alta: SYN flood, ACK flood o UDP flood. El objetivo es el procesamiento de paquetes, no la línea.
- de 1200 a 1500 bytes con ancho de banda alto, puertos de origen 53, 123, 389 u 11211: ataque de reflexión y amplificación. Las IP de origen son servidores ajenos, no los atacantes.
- distribución de tamaños normal, peticiones HTTP limpias: capa de aplicación. Entonces decide el log del servidor web, no el contador de paquetes.
Más cómodo resulta con sar del paquete sysstat:
sar -n DEV 1 3
Punto delicado: la medición en vivo funciona nada más instalarlo, mientras que el análisis histórico con sar -f no. En Debian y Ubuntu la recogida de datos viene desactivada de fábrica, en /etc/default/sysstat pone ENABLED="false". Quien se da cuenta de esto solo cuando ya hay un ataque no tiene valores de referencia de anteayer. Ese es el motivo para activar este paquete de forma preventiva y no en plena emergencia.
Y la limitación más importante de todas: en el servidor solo mides lo que ha logrado pasar. Si hay un filtrado por delante, verás una fracción del volumen real o directamente nada. La cifra fiable está en el gráfico de tráfico del área de cliente, no en /proc/net/dev.
Leer los mensajes del kernel
El kernel registra situaciones de sobrecarga que quedan invisibles en la capa de aplicación. En Ubuntu y en las versiones actuales de Debian el búfer circular está bloqueado para los usuarios normales, y sin sudo aparece:
dmesg: read kernel buffer failed: Operation not permitted
Eso no es un fallo de funcionamiento, sino kernel.dmesg_restrict=1. Con permisos de root y marcas de tiempo:
dmesg -T | grep -Ei 'syn flood|conntrack|neighbour|drop'
El mensaje que la mayoría busca dice literalmente:
TCP: request_sock_TCP: Possible SYN flooding on port 443. Sending cookies. Check SNMP counters.
Hay dos variantes y una diferencia de formato que conviene conocer:
- Sending cookies significa que las SYN cookies están activas y que las conexiones se siguen atendiendo. Dropping request aparece en su lugar cuando
net.ipv4.tcp_syncookiesestá a 0. Entonces las peticiones se descartan, visitantes reales incluidos. - Los kernels antiguos indican solo el número de puerto, los más nuevos añaden la dirección de escucha con la forma
0.0.0.0:443. En Debian 12 verás por tanto la forma corta, en Debian 13 y Ubuntu 24.04 la larga. Quien haga grep del texto antiguo exacto no encontrará nada en los sistemas nuevos.
Importante para la honestidad del diagnóstico: ese mensaje no demuestra ningún ataque. Aparece también cuando una aplicación trabaja con un backlog de escucha demasiado pequeño y un pico de carga legítimo la desborda. Es un indicio que hay que respaldar con otras mediciones.
Los contadores correspondientes los da nstat:
nstat -az TcpExtSyncookiesSent
nstat -az TcpExtListenDrops
nstat -az TcpExtListenOverflows
Aquí también hay un punto delicado: nstat guarda un estado intermedio en cada llamada y la siguiente vez muestra solo la diferencia. Es intencionado e incluso práctico para medir, pero sorprende a cualquiera que de repente vea ceros en la segunda llamada. Con -a fuerzas valores absolutos, con -s evitas que ese estado se actualice.
Otros dos mensajes que aparecen con regularidad durante un ataque y que provocan pérdida de paquetes para los visitantes legítimos:
nf_conntrack: table full, dropping packet
neighbour: arp_cache: neighbor table overflow!
El nivel de llenado del seguimiento de conexiones lo compruebas con cat /proc/sys/net/netfilter/nf_conntrack_count en comparación con nf_conntrack_max. Ambos archivos solo existen si el módulo está cargado, es decir, si hay un firewall activo. En un sistema sin reglas faltan, y eso es normal.
Logs del servidor web: patrones en lugar de intuición
En Debian y Ubuntu los logs están en /var/log/nginx/access.log o en /var/log/apache2/access.log respectivamente. En la familia Red Hat la ruta de Apache es /var/log/httpd/access_log, sin punto antes de la terminación. De momento bastan tres análisis:
awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10
La distinción entre un ataque y una avalancha real sale de la relación entre esas tres listas:
- Relación entre peticiones e IP distintas. Diez mil peticiones desde 6.000 direcciones son público. Diez mil peticiones desde 30 direcciones no lo son.
- Peticiones posteriores. Un visitante real carga, después de la página HTML, hojas de estilo, scripts e imágenes. Si en la lista de rutas solo aparece una única URL y ni un solo archivo estático, ahí no había ningún navegador.
- Procedencia. Cuando el éxito viene de una campaña, el campo Referrer indica una fuente: un portal de noticias, una red social, un buscador. En un ataque ese campo suele estar vacío o relleno con una dirección inventada.
- Identificación del programa. Un único User-Agent, exactamente idéntico a lo largo de decenas de miles de peticiones, es una herramienta. También llaman la atención las identificaciones de versiones de navegador muy antiguas.
- Códigos de estado. Un predominio de 200 apunta a público, un muro de 404 o de 499 apunta a un sondeo automatizado o a clientes que cortan antes de recibir la respuesta.
Cuidado con los buscadores: una IP afirma con gusto en su User-Agent que es Googlebot. Eso solo se puede comprobar con una resolución inversa seguida de otra directa. La afirmación solo es cierta cuando el nombre inverso apunta a un dominio del proveedor y ese nombre vuelve a resolverse a la misma IP.
dig -x 66.249.66.1 +short
Para una dirección auténtica de Googlebot te devuelve un nombre del estilo crawl-66-249-66-1.googlebot.com., que después haces resolver de nuevo a la misma IP con dig +short crawl-66-249-66-1.googlebot.com. Si no vuelve nada en absoluto, puede deberse también a un servidor sin resolución DNS saliente, y entonces no prueba nada.
Y la frase más importante sobre los logs: un ataque en la capa de red no aparece en el log del servidor web. Un SYN flood nunca llega a la aplicación y no deja allí ni una sola línea. Un log vacío no refuta nada, solo acota la capa.
La contraprueba: así una sospecha se convierte en diagnóstico
Hasta aquí tienes indicios. Se vuelven sólidos gracias a contrapruebas, cada una de las cuales puede refutar exactamente una hipótesis.
- Parar el servicio. Detén el servidor web durante 30 segundos. Si la tasa de paquetes entrantes sigue igual de alta, el ataque está por debajo de la aplicación. Si se desploma, eran peticiones a tu aplicación, malintencionadas o no.
- Dentro contra fuera. Si
curlresponde a través de 127.0.0.1 en milisegundos mientras la consulta desde fuera acaba en un error de tiempo de espera, la aplicación está sana y el problema es la línea. - Segunda medición. Repite la medición de paquetes dos minutos después. Los ataques se mantienen o vuelven en oleadas. Un pico aislado no era más que un pico.
- Mirada desde fuera. Una prueba externa de accesibilidad desde varias ubicaciones separa "inaccesible para todos" de "inaccesible solo para ti". El segundo caso suele ser un problema de enrutamiento o de proveedor en el lado del observador, no un ataque.
- Comprobar la dirección. Compara
rx_packetscontx_packets. Si la tasa llamativa es la saliente, no te están atacando, es tu servidor el que ataca. Entonces está comprometido o se está usando como reflector, y el caso cambia de urgencia al instante.
Sabrás que el diagnóstico se sostiene cuando puedas decir en una frase qué protocolo llega a qué puerto y con qué tasa, si es entrante o saliente, y tengas dos mediciones independientes que muestren lo mismo. Todo lo que quede por debajo es una suposición.
Cuando no hay ningún rastro medible
Cuatro situaciones que generan confusión con regularidad:
- Ya no consigues entrar en el servidor. Con la línea saturada, SSH tampoco logra pasar. El acceso va entonces por la consola del área de cliente, que funciona con independencia de la conexión de red del sistema. Justo para eso está.
- El servicio se cayó y en el servidor no hay nada. Con un filtrado por delante eso es la regla, no la excepción. Si el ataque se intercepta en la red, el sistema solo ve un pequeño bache. La prueba está entonces en el gráfico de tráfico del área de cliente.
- El log se corta en mitad del incidente. Comprueba si entretanto se ejecutó una rotación de logs, el material más antiguo está al lado como
access.log.1oaccess.log.2.gz. Siaccess_logestá desactivado o en búfer en la configuración, simplemente faltan las últimas líneas. - El kernel calla. En sistemas basados en contenedores el búfer circular pertenece al sistema anfitrión y
dmesgno muestra allí nada propio. En un servidor root KVM con kernel propio esto no supone ningún problema.
Qué documentar antes de contactar con el proveedor
Un aviso del tipo "el servidor iba lento esta tarde" alarga la tramitación varias rondas. Un aviso con valores medidos se atiende de inmediato. Recoge los datos mientras el incidente sigue en curso, porque los contadores de /proc se ponen a cero al reiniciar.
mkdir -p /root/incidente && cd /root/incidente
date -u > 01-hora.txt
ss -s > 02-sockets.txt
ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn > 03-estados.txt
ss -Htn state established | awk '{print $4}' | sed 's/:[^:]*$//' | sort | uniq -c | sort -rn | head -50 > 04-top-ips.txt
ip -s link > 05-interfaces.txt
sar -n DEV 1 10 > 06-tasa-paquetes.txt
dmesg -T | tail -100 > 07-kernel.txt
nstat -az > 08-contadores.txt
Si la línea lo permite, una captura de paquetes corta también forma parte del conjunto. Limítala, porque una captura sin límite en una línea saturada llena el disco en minutos y agrava el problema:
tcpdump -D
tcpdump -ni "$IF" -s 96 -c 2000 -w /root/incidente/captura.pcap
tcpdump -D lista las interfaces disponibles, por si la variable $IF del apartado de medición anterior ya no está definida. Ambas llamadas requieren permisos de root.
En el ticket deben ir entonces:
- Momento de inicio y de fin, con zona horaria.
date -uevita cualquier discusión al respecto. - Dirección IP afectada y puerto afectado.
- Tasa de paquetes y ancho de banda medidos, indicando de forma expresa la dirección.
- Reparto por protocolos y, si se reconoce, los puertos de origen de los extremos remotos.
- Un puñado de direcciones de origen de ejemplo, con la advertencia de que los remitentes pueden estar falsificados.
- Un extracto del log del servidor web con el patrón de peticiones que se repite, bastan de tres a cinco líneas.
- Qué se cambió poco antes: un despliegue, un cambio de DNS, una campaña publicitaria, un puerto recién abierto.
- Resultado de la contraprueba: ¿estaba el servicio accesible a través de 127.0.0.1 mientras fallaba desde fuera?
Conclusiones erróneas frecuentes
- Tomar muchas conexiones TIME-WAIT como prueba de un ataque. Son la consecuencia normal de conexiones HTTP cortas y desaparecen solas.
- Tomar el mensaje de SYN flooding como prueba. Un backlog de escucha configurado demasiado pequeño también lo genera con un pico de carga legítimo.
- Bloquear una IP con muchas conexiones sin comprobar nada. Detrás de un NAT corporativo, un NAT móvil o un proxy por delante estarás dejando fuera a grupos enteros de clientes.
- Deducir un ataque a partir de un ancho de banda alto. Comprueba primero la dirección. Los picos salientes suelen ser un backup o una descarga popular.
- Deducir "no hay ataque" a partir de logs vacíos. Los ataques en la capa de red nunca llegan a la aplicación.
- Reiniciar con el incidente en curso. El reinicio borra todos los contadores que habrías necesitado para el aviso, y el ataque sigue después igual que antes.
Quien ha practicado esta secuencia una vez no necesita ni cinco minutos para llegar a un diagnóstico sólido. Y quien adjunta los valores medidos en el ticket se salta las preguntas de vuelta y llega directo a la solución. En la misma línea: la lista de comprobación para servidores root nuevos y Configurar Fail2ban para el seguimiento en la capa de aplicación.
Preguntas frecuentes
¿Cómo distingo un ataque DDoS de una avalancha real de visitantes?
¿Por qué netstat no está instalado en mi servidor?
¿El mensaje Possible SYN flooding on port 443 significa siempre un ataque?
¿Por qué no encuentro ningún rastro en el servidor aunque el servicio estuvo un rato inaccesible?
¿Cómo entro en el servidor si SSH deja de responder durante el incidente?
¿Por qué mi one-liner de ss copiado devuelve cifras erróneas?
¿Qué hago si la tasa de paquetes saliente es la llamativa?
2026 KernelHost GmbH. Todos los derechos reservados. Esta guía está protegida por derechos de autor. Su publicación en otros sitios web, aunque sea de forma parcial o modificada, no está permitida sin nuestro consentimiento por escrito. Las citas con indicación de la fuente y un enlace son muy bienvenidas.

