El demonio sshd escucha en un puerto de red y acepta credenciales: eso lo convierte en el servicio más atacado en cualquier máquina expuesta a internet. La configuración por defecto de Debian es razonable para empezar, pero no está pensada para producción. El archivo que controla todo esto es /etc/ssh/sshd_config, y cada directiva que tocamos ahí reduce la superficie de ataque de una manera concreta y medible.
El mecanismo interno es simple: sshd lee ese archivo al arrancar y en cada recarga, construye una política de acceso en memoria, y la aplica a cada conexión entrante antes de que el usuario llegue siquiera a introducir credenciales. Esto significa que directivas como MaxAuthTries o LoginGraceTime actúan en la capa de protocolo, no en la capa de aplicación — no hay forma de esquivarlas desde el cliente.
Cuándo aplicar esto: siempre que el servidor tenga sshd expuesto a una red no completamente confiable. Incluso en redes internas, la defensa en profundidad vale la pena porque el vector de ataque más común no es externo sino lateral — una máquina comprometida dentro de la misma red.
Lo que se rompe si lo haces mal es exactamente lo que temes: te quedas sin acceso. Deshabilitar PasswordAuthentication antes de instalar las claves públicas correctamente es el error clásico que obliga a usar la consola de emergencia del proveedor cloud o, en bare metal, teclado físico. La regla de oro es: nunca recargues sshd sin tener una segunda sesión SSH abierta y sin haber validado la configuración con sshd -t.
# ── 1. Validar que las claves ya están en su sitio ─────────────────────
# Antes de tocar nada, confirma que puedes autenticarte con clave
# desde OTRA terminal. Este paso no es opcional.
grep -r "ssh-" /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys 2>/dev/null
# ── 2. Hacer una copia de seguridad del archivo original ───────────────
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%F)
# ── 3. Aplicar la configuración de producción ──────────────────────────
cat > /etc/ssh/sshd_config << 'EOF'
# Puerto no estándar: no es seguridad real, pero elimina el 90% del
# ruido de bots en los logs y reduce la carga sobre fail2ban.
Port 2222
# Escuchar solo en IPv4 si no usas IPv6; evita vectores innecesarios.
AddressFamily inet
# Protocolo: sshd en Debian moderno ya solo soporta SSHv2, pero
# forzar los algoritmos fuertes es explícito y auditable.
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key
# ── Autenticación ──────────────────────────────────────────────────────
# Nunca login directo como root. Si necesitas root, entra como usuario
# normal y usa sudo. "prohibit-password" es el default de Debian;
# "no" es más estricto y no admite ninguna excepción.
PermitRootLogin no
# Solo llaves. Deshabilitar contraseñas elimina ataques de fuerza bruta
# por definición, no por configuración de intentos.
PasswordAuthentication no
PubkeyAuthentication yes
# Desactivar métodos que no usamos para reducir superficie.
ChallengeResponseAuthentication no
KerberosAuthentication no
GSSAPIAuthentication no
# Whitelist explícita: solo estos usuarios pueden conectarse por SSH.
# Más seguro que confiar en que el resto no tenga authorized_keys.
AllowUsers deploy ansible backup-agent
# Limitar intentos por conexión. Con llaves, 3 es más que suficiente.
# Más de 3 intentos fallidos = cierre inmediato de la conexión.
MaxAuthTries 3
# Tiempo máximo para completar la autenticación. Un cliente legítimo
# con clave tarda menos de 2 segundos; 30 es generoso pero limita
# conexiones fantasma que consumen descriptores de fichero.
LoginGraceTime 30
# Número máximo de conexiones en fase de autenticación simultáneas.
# Mitiga ataques de agotamiento de conexiones.
MaxStartups 10:30:60
# ── Forwarding ─────────────────────────────────────────────────────────
# Desactivar todo lo que no uses. X11 y agent forwarding son vectores
# de escalada si el servidor está comprometido: un proceso local puede
# usar el socket del agente para saltar a otras máquinas.
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
# ── Sesión ─────────────────────────────────────────────────────────────
# Desconectar sesiones idle: 10 minutos sin actividad = cierre limpio.
ClientAliveInterval 120
ClientAliveCountMax 3
# No mostrar el banner del sistema antes de autenticar.
# Menos información para un atacante.
PrintLastLog yes
PrintMotd no
# ── Regla específica para usuario git (servidor de repositorios) ───────
# Match User sobrescribe las directivas globales solo para ese usuario.
# git-shell solo necesita el canal de sesión, sin TTY ni forwarding.
Match User git
AllowTcpForwarding no
X11Forwarding no
PermitTTY no
ForceCommand /usr/bin/git-shell
EOF
# ── 4. Validar sintaxis ANTES de recargar ─────────────────────────────
# sshd -t sale con código 0 si todo está bien, >0 si hay error.
# No recargues nunca sin ejecutar esto primero.
sshd -t && echo "Configuración válida" || echo "ERROR: revisar antes de recargar"
# ── 5. Recargar (no reiniciar) para aplicar sin cortar sesiones activas
systemctl reload sshd
# ── 6. Verificar que el demonio está escuchando en el puerto nuevo ─────
ss -tlnp | grep sshd
# ── 7. Instalar y configurar fail2ban para el puerto personalizado ─────
apt-get install -y fail2ban
cat > /etc/fail2ban/jail.d/sshd-custom.conf << 'EOF'
[sshd]
enabled = true
port = 2222
# 5 fallos en 10 minutos = baneo de 1 hora
maxretry = 5
findtime = 600
bantime = 3600
EOF
systemctl restart fail2ban
# Confirmar que la jail está activa
fail2ban-client status sshd
Desglose de las decisiones
PermitRootLogin no va más allá del valor por defecto de Debian (prohibit-password). La diferencia es que prohibit-password aún permite a root autenticarse con clave, lo que significa que si esa clave se compromete, el atacante entra directamente con UID 0 sin pasar por sudo ni por ningún log de auditoría de elevación de privilegios. Con no, root simplemente no puede conectarse, punto.
AllowUsers deploy ansible backup-agent es una whitelist, no una blacklist. El modelo mental correcto es: por defecto ningún usuario puede conectarse por SSH a menos que esté explícitamente listado. Eso incluye cuentas de sistema que alguien pudiera haber dejado con authorized_keys accidentalmente. Si añades un usuario nuevo al sistema y no lo pones aquí, SSH lo rechaza aunque tenga clave configurada.
MaxStartups 10:30:60 merece atención porque acepta tres valores separados por :. El significado es: hasta 10 conexiones simultáneas en autenticación se aceptan sin restricción; entre 10 y 60 se empiezan a rechazar probabilísticamente (empezando en 30% de probabilidad); por encima de 60 se rechazan todas. Esto mitiga ataques de agotamiento de conexiones que saturan los descriptores de fichero del proceso sshd antes de que fail2ban tenga tiempo de reaccionar.
ClientAliveInterval 120 con ClientAliveCountMax 3 envía un paquete de keepalive cada 120 segundos y espera respuesta. Si el cliente no responde 3 veces consecutivas (6 minutos de silencio total), sshd cierra la sesión. Esto no es solo comodidad: sesiones zombie que mantienen locks de archivos o conexiones de base de datos en estado ESTABLISHED son un problema real en servidores con alta rotación de conexiones.
El bloque Match User git demuestra el poder del sistema de directivas condicionales de sshd_config. La directiva ForceCommand /usr/bin/git-shell reemplaza cualquier comando que el cliente intente ejecutar — aunque git push internamente pida ejecutar git-receive-pack, git-shell actúa de intermediario y solo permite operaciones git. Sin PermitTTY no, un atacante que comprometa la clave de git podría simplemente abrir un shell interactivo.
sshd -t antes de systemctl reload sshd no es paranoia, es el flujo correcto. reload envía SIGHUP al proceso existente, que relee la configuración; si hay un error de sintaxis, el demonio puede terminar sin volver a arrancar correctamente, dejando el puerto cerrado. El test de sintaxis es atómico y no afecta las sesiones en curso.
La diferencia entre fail2ban y las restricciones nativas de sshd es de capa: sshd actúa conexión a conexión, fail2ban actúa a nivel de IP a través de nftables (en Debian Bookworm) acumulando intentos entre conexiones distintas. Necesitas ambas porque un atacante que haga un intento por conexión y luego desconecte no acumula fallos en MaxAuthTries, pero sí los acumula en el log que fail2ban está monitorizando.
N° 84