Ataque de fuerza bruta SSH: Cómo identificarlo y proteger tu servidor

Tiempo de lectura: 8.46 minutos

Póster del artículo Ataque de fuerza bruta SSH: Cómo identificarlo y proteger tu servidor

Hoy estuve revisando el droplet de un cliente, que estuvo offline por unos minutos.

En un primer momento pensé que se trataba de un error por falta de memoria, ya que el número de usuarios incrementó y algunas queries podrían estar tomando algo de tiempo.

Pero tras revisar las estadísticas de la base de datos, no encontré registros de slow queries o problemas de performance.

Revisando las métricas se observa un incremento en el uso del CPU:

Uso de CPU

Pero no en el uso de memoria ni uso de disco

Uso de memoria y disco

Como ves, el incidente ocurrió entre las 01:10 PM y 01:16 PM.

Empezando con el análisis

A fin de empezar a investigar el incremento en el uso del CPU:

  • Revisamos a qué hora ocurrió el spike
  • Y ejecutamos el siguiente comando para revisar dicho intervalo
journalctl --since "2025-11-16 13:10" --until "2025-11-16 13:16"

journalctl es la herramienta para leer los logs gestionados por systemd.

Con ella podemos ver:

  • logs del sistema
  • logs de servicios
  • logs del kernel
  • logs de autenticación
  • y filtrar por fecha, usuario, etcétera.

En este caso, tras revisar los logs, nos encontramos con intentos de fuerza bruta SSH contra el servidor.

El ataque no fue exitoso.

Pero el alto nivel de tráfico causó un incremento significativo en el uso del CPU.

Confirmando ataque de fuerza bruta

El siguiente comando:

sudo grep 'Failed password' /var/log/auth.log | wc -l

Nos indica cuántas veces aparece la cadena Failed password en /var/log/auth.log.

El resultado fue:

Intentos de login fallidos

Esto nos indica que hubo 2301 intentos fallidos de contraseña (intentos de login SSH fallidos).

Actividad de autenticación

El archivo /var/log/auth.log es donde Linux registra toda la actividad de autenticación.

Incluye, por ejemplo:

  • Logins exitosos por SSH.
  • Intentos fallidos (Failed password).
  • Eventos relacionados a PAM (el sistema que valida contraseñas).
  • Bloqueos o rechazos de login.

Rotación de Logs

No contiene el total histórico.

Si el sistema ya rotó logs, los intentos previos estarán en auth.log.1 o en archivos comprimidos, como auth.log.2.gz.

Para saber el intervalo exacto que abarca auth.log actualmente, podemos ver la primera y la última línea:

head -n 1 /var/log/auth.log
tail -n 1 /var/log/auth.log

Así podremos ver la fecha del primer y último log escritos en dicho archivo.

Intentos en archivos de log previos

Siguiendo la misma idea podemos ver la cantida de intentos en el archivo completo de la última rotación:

Intentos de login en auth.log.1

Los logs más antiguos se encuentran comprimidos.

Si contabilizamos el total en ellos:

zgrep 'Failed password' /var/log/auth.log.*.gz | wc -l

En este caso obtenemos 45128 como respuesta.

Frecuencia de la rotación

¿Cada cuánto rota?

Depende del archivo de configuración:

cat /etc/logrotate.d/rsyslog

Para auth.log suele ser:

rotate 4
weekly
compress

Significa:

  • weekly → rota cada semana.
  • rotate 4 → guarda hasta 4 copias (4 semanas).
  • compress → versiones viejas en .gz.

Un cron ejecuta logrotate diariamente, y éste se encarga de rotar, así mismo de comprimir y renombrar, según las reglas establecidas.

Total de intentos

Ubuntu normalmente mantiene estos archivos:

  • auth.log → archivo actual
  • auth.log.1 → el rotado más reciente
  • auth.log.2.gz, auth.log.3.gz, etcétera → anteriores y comprimidos

Dicho de otra forma, nos corresponde sumar:

  • auth.log → 2301
  • auth.log.1 → 14502
  • auth.log.*.gz → 45128

Sin embargo, este el total histórico de intentos fallidos registrados.

Es decir, no contabiliza los archivos de log que ya no están en el sistema por antigüedad.

Ataque de fuerza bruta SSH

¿Por qué se considera fuerza bruta?

Porque:

  • Miles de intentos fallidos = miles de pruebas de usuario/contraseña.
  • Vienen de IPs aleatorias de Internet.

Esto es fuerza bruta automatizada hecha por botnets, no por un humano.

¿Es algo grave o normal?

Es normal en servidores públicos.

Todos los VPS en el mundo reciben estos ataques las 24 horas, especialmente en Ubuntu/Debian porque es fácil identificar que tienen SSH abierto.

No representa un hackeo, solo que:

  • Robots ven la IP.
  • Detectan que el puerto 22 responde.
  • Empiezan a probar miles de combinaciones.

¿Es peligroso?

Depende de nuestra configuración:

Configuración Riesgo
Contraseñas débiles + puerto 22 abierto Alto
Contraseñas fuertes Bajo
fail2ban activo Muy bajo
Login (solo con llaves SSH) Casi nulo

¿Qué probaron?

¿Cómo saber si realmente probaron contraseñas distintas?

Podemos ver los usuarios que probaron:

sudo grep 'Failed password' /var/log/auth.log | awk '{print $(NF-5)}' | sort | uniq -c
Usuarios con los que intentaron hacer login vía SSH

Y ver las IPs:

sudo grep 'Failed password' /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | head
IPs desde las que intentaron hacer login vía SSH

Fail2ban

Qué hace Fail2ban

Fail2ban observa archivos de log (como /var/log/auth.log) y actúa ante fallos repetidos.

Por ejemplo:

Si una IP prueba 5 contraseñas incorrectas → Fail2ban agrega una regla de firewall para bloquear a dicha IP por X minutos.

Configuración básica (por defecto):

  • 5 fallos → bloquea 10 minutos
  • bloqueos a nivel de firewall (iptables/ufw)
  • reduce el uso de CPU porque sshd no se ejecuta en absoluto para las IPs bloqueadas

Fail2ban no protege definitivamente SSH; pero reduce el volumen de intentos de fuerza bruta.

Ver estado actual

Con este comando podemos ver qué prisiones están activas:

sudo fail2ban-client status

Por ejemplo, podemos obtener el siguiente el resultado:

Status
|- Number of jail:      1
`- Jail list:   sshd

Esto significa que se está protegiendo sshd.

Y podemos ver detalles específicos para el jail de sshd:

sudo fail2ban-client status sshd
Estado del jail sshd

Interpretación:

  • Currently banned: 6 significa que 6 IPs se encuentran bloqueadas de forma activa ahora mismo (su tiempo de ban no ha expirado aún).
  • Total banned: 2170 es un contador que incrementa por cada IP que ha sido baneada al menos una vez (incluso si un ban expira o el servidor de reinicia).

Cómo configurar Fail2ban

Fail2ban carga su configuración en este orden:

  • /etc/fail2ban/jail.conf
  • /etc/fail2ban/jail.d/*.conf
  • /etc/fail2ban/jail.local

Por lo que se recomienda editar (o crear si no existe) el archivo jail.local.

Así no modificamos los valores por defecto, y sólo sobreescribimos de forma puntual.

Ejemplo de configuración:

[sshd]
enabled = true
maxretry = 3
bantime = 1h

Luego de guardar cambios, no olvides reiniciar el servicio:

sudo systemctl restart fail2ban

Parámetros de configuración

Revisemos el significa de cada línea, para la siguiente configuración:

[sshd]
enabled = true
maxretry = 3
bantime = 1h
findtime = 10m
  • [sshd].- Este bloque controla la protección para ataques a SSH.
  • enabled = true.- Activa el jail.
  • maxretry = 3.- Número máximo de intentos fallidos permitidos dentro del findtime.
  • findtime = 10m.- Ventana de tiempo donde se cuentan los intentos fallidos.
  • bantime = 1h.- Tiempo que dura el bloqueo.

Ejemplo:

  • Si hay 3 intentos fallidos en 10 minutos → ban.
  • Si hay 3 intentos fallidos en 3 horas → no ban.

Baneo indefinido o permanente

¿Se puede banear permanentemente a un reincidente?

Sí. Hay dos formas.

Ban permanente

Si quieres aplicar ban permanente e inmediato:

bantime = -1

Con esta configuración, todos quedan baneados para siempre.

Ban incremental

Fail2ban soporta bantime escalonado.

Cada vez que la IP reincide, su castigo aumenta.

Esto se logra con:

bantime = 10m
bantime.increment = true
bantime.rndtime = 0
bantime.factor = 4
bantime.maxtime = -1

¿Qué significa?

  • bantime.increment = true → habilita escalamiento
  • bantime.factor = 4 → multiplica el tiempo por 4 en cada reincidencia
  • bantime.maxtime = -1 → sin límite
  • bantime.rndtime = 0 → tiempo aleatorio que se suma o resta al bantime

Resultado (con factor 4):

  • 1er ban → 10 min
  • 2do ban → 40 min
  • 3er ban → 160 min
  • 4to ban → ≈ 10.6h
  • 5to ban → 42h
  • 6to ban → sigue incrementando sin límite

Así un atacante reincidente queda fuera para siempre.

Recomendación

Lo recomendable es no compartir estas credenciales con muchos usuarios, incluso para proyectos con grandes equipos de desarrollo.

Debido a que la conexión SSH no es muy frecuente, y se espera que sólo tú como administrador tengas acceso, puedes optar por una estrategia como la siguiente.

[sshd]
enabled = true

# 2 fallas en 1 hora → ban
maxretry = 2
findtime = 1h

# Tiempo de ban → permanente
bantime = -1

Keys en vez de passwords

Se considera a las claves SSH como una solución a ataques de fuerza bruta, ya que:

  • no hay un campo "password" contra qué probar
  • se desactiva la autenticación por contraseña
  • los bots ni siquiera pueden intentar adivinar

Se rechazan inmediatamente sin procesamiento de CPU/PAM/logs.

Comparación:

Pregunta Password SSH Key
¿Aparece en logs? Sí (intentos fallidos) No
¿Objetivo de bots? Casi nunca
Nivel de seguridad Medio Muy fuerte
Uso de CPU Alto durante ataques Muy bajo

Cómo funciona

Veamos qué sucede durante un ataque.

Login con contraseña

Cuando se prueba una password:

  • sshd empieza una sesión, correspondiente al intento.
  • PAM (Pluggable Authentication Modules) verifica la contraseña contra /etc/shadow.
  • Si la contraseña es incorrecta, se escriben logs en /var/log/auth.log.

El proceso termina.

Cada intento consume CPU, incluso si falla.

En un droplet pequeño, cientos de intentos por minuto pueden representar un spike importante en el uso del CPU.

Login basado en keys

Cuando el login con password se desactiva y usas SSH keys:

  • sshd rechaza todo intento con password inmediatamente, sin invocar PAM.
  • Los bots que prueban contraseñas, sólo reciben un "connection closed", sin ningún procesamiento.
  • Sólo los clientes con keys válidas pueden iniciar el protocolo.

Si muchos atacantes intentan con diferentes passwords, el servidor a penas consume CPU.

Los ataques de fuerza bruta pierden sentido

Esto es porque:

Las SSH keys usan criptografía asimétrica.

"Adivinar" una clave privada es prácticamente imposible ya que:

El atacante se enfrenta a un problema matemáticamente imposible de resolver con el poder de una computadora.

Ejemplo:

  • Contraseña promedio: aproximadamente 10¹⁰ posibilidades.
  • Clave SSH promedio: ~10⁶¹⁷ posibilidades.

Además, los bots NO prueban posibles valores para las private keys porque:

  • Necesitan enviar primero una public key válida
  • El servidor rechaza keys desconocidas al instante

No se verifica siquiera la private key

Así es cómo funciona realmente la autenticación con SSH keys:

El servidor tiene un archivo authorized_keys con las public keys que son válidas para cada usuario.

Cuando un cliente intenta conectarse:

  • El cliente no envía su private key.
  • Envía la public key que corresponde a su private key.

El servidor verifica:

¿Está esta public key en authorized_keys?

  • Si no está → la conexión se rechaza inmediatamente.
  • Sin hacer ningún cálculo criptográfico.

Solo si la public key coincide con alguna entrada:

  • El servidor envía un desafío criptográfico (challenge).
  • El cliente debe firmar el desafío con su private key.
  • El servidor valida la firma usando la public key.

Por eso los ataques de fuerza bruta no funcionan con keys:

  • si tu public key no está registrada, la "adivinanza" nunca avanza.

Private Key y Public Key

Por si aún no está del todo claro:

Cada usuario legítimo tiene un par de llaves:

  • Private key → se queda en el dispositivo del usuario (no se comparte).
  • Public key → se copia al servidor en ~/.ssh/authorized_keys.

Ejemplo:

Si Juan es un usuario legítimo y tiene keys, esta es la situación.

juan@cliente:
  ~/.ssh/id_ed25519      <- private key (se queda en su laptop)
  ~/.ssh/id_ed25519.pub  <- public key (se copia al servidor)

Servidor:
  /home/juan/.ssh/authorized_keys  <- contiene la public key de Juan

Cómo configurar SSH Keys

Lo primero es verificar si ya cuentas con SSH Keys para tu PC:

ls -l ~/.ssh

Si encuentras archivos como:

  • id_ed25519 y id_ed25519.pub
  • o bien id_rsa y id_rsa.pub

Significa que ya cuentas con claves.

Ambos son válidos y seguros.

La diferencia es que Ed25519 es un algoritmo más reciente.

Si no tienes SSH Keys, puedes generarlas con este comando:

ssh-keygen -t ed25519 -C "juan@pc"

Explicación:

  • ssh-keygen es el programa que crea el nuevo par de keys.
  • -t ed25519 especifica el tipo de key a generar.
  • -C "juan@pc" es simplemente un comentario, para que puedas identificar a la key.

Esto va a generar 2 archivos:

  • id_ed25519 → private key
  • id_ed25519.pub → public key (a copiar en el servidor)

Y en la terminal verás una salida similar a la siguiente:

Generating public/private ed25519 key pair.
Your identification has been saved in C:\Users\USER/.ssh/id_ed25519.
Your public key has been saved in C:\Users\USER/.ssh/id_ed25519.pub.
The key fingerprint is:
SHA256:zqyRmcI80kx+Rk/Vdgi00h5lGx4F+3CSxVDF7/v92Xs juan@pc
The key's randomart image is:
+--[ED25519 256]--+
|         .o B*+o.|
|         . B O. .|
|        . = @ o .|
|         + o *  .|
|    . . S .   .. |
|   B . X        .|
|  . O B =       .|
|   . = o       .E|
|      .        oX|
+----[SHA256]-----+

Con esto ya estás listo para copiar la public key en tu droplet.

Veamos si la carpeta ssh ya existe:

ls -ld ~/.ssh

Si ves algo como lo siguiente:

drwx------ 2 root root 4096 Nov 11 14:11 /root/.ssh

Entonces no necesitas crear la carpeta porque ya existe.

Sólo debes añadir tu clave pública (en tu servidor):

echo "TU_PUBLIC_KEY" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

No olvides reemplazar TU_PUBLIC_KEY.

El valor de tu public key lo puedes obtener con este comando (en tu PC):

cat ~/.ssh/id_ed25519.pub

Finalmente, puedes verificar la conexión con este comando:

ssh -i ~/.ssh/id_ed25519 root@IP_DEL_SERVIDOR

Y listo!

Desactivar login con contraseña

Con esto ya estamos listos para desactivar la autenticación con contraseña.

Primero verifiquemos qué archivos de configuración tenemos que actualmente definen PasswordAuthentication:

sudo grep -R "PasswordAuthentication" /etc/ssh/

Como resultado verás algo como lo siguiente:

/etc/ssh/sshd_config.d/50-cloud-init.conf:PasswordAuthentication yes
/etc/ssh/ssh_config:#   PasswordAuthentication yes
/etc/ssh/sshd_config.ucf-dist:PasswordAuthentication no
/etc/ssh/sshd_config.ucf-dist:# PasswordAuthentication.  Depending on your PAM configuration,
/etc/ssh/sshd_config.ucf-dist:# PAM authentication, then enable this but set PasswordAuthentication
/etc/ssh/sshd_config:PasswordAuthentication no
/etc/ssh/sshd_config:# PasswordAuthentication.  Depending on your PAM configuration,
/etc/ssh/sshd_config:# PAM authentication, then enable this but set PasswordAuthentication

Explicación:

  • sshd carga primero /etc/ssh/sshd_config
  • Luego carga todos los archivos dentro de /etc/ssh/sshd_config.d/*.conf en orden alfabético.
  • En este caso, por ese motivo, /etc/ssh/sshd_config.d/50-cloud-init.conf se carga después y sobrescribe PasswordAuthentication.

Para confirmarlo, puedes ejecutar:

sudo sshd -T | grep passwordauthentication

Y verás el valor actual, después de aplicar todas las reglas.

Entonces editamos /etc/ssh/sshd_config.d/50-cloud-init.conf y actualizamos el valor a no.

Luego reiniacimos:

sudo systemctl restart ssh

Y confirmamos:

root@lamp-ubuntu:~# sudo sshd -T | grep passwordauthentication
passwordauthentication no

Felicitaciones! 🎉

Con esto has desactivado por completo la autenticación con contraseña.

En programas como PuTTy por ejemplo, se muestra un error como el siguiente si intentas conectarte:

Mensaje de error en PuTTy cuando password authentication no se permite

Y esto es mejor para ti:

Ya que no necesitas de hecho escribir ninguna contraseña de ahora en adelante 😉.

# ssh

Logo de Programación y más

Comparte este post si te fue de ayuda 😉.

Regístrate

Accede a todos los cursos, y mejora tus habilidades 🚀.

Cursos Recomendados 🚀

Imagen para el curso Laravel y Android

Laravel y Android

Curso intensivo. Incluye el desarrollo de una API, su consumo, y autenticación vía JWT. También vemos Kotlin desde 0.

Iniciar curso
Imagen para el curso Aprende Javascript

Aprende Javascript

Domina JS con este curso práctico y completo! Fundamentos, ejemplos reales, ES6+, POO, Ajax, Webpack, NPM y más.

Iniciar curso
Imagen para el curso Docker y Microservicios

Docker y Microservicios

Aprende por qué es importante y cómo funciona Docker, con este nuevo curso práctico!

Iniciar curso

Artículos Relacionados 📚

Espera un momento 🎁 ...

¿Te gustaría aprender a programar, gratis?

Mago de Programación y más

Sólo debes registrarte 😉.