Universo SSH IV. Blindando el servidor
Como diría el Maestro Yoda: “En el puerto 22, el peligro acecha”. Si dejas la configuración SSH por defecto, es como defender la Estrella de la Muerte con una puerta de madera y una nota que diga “Por favor, no explotar”. En un universo donde los ataques de fuerza bruta son más comunes que los cameos de Stan Lee, toca convertir el acceso remoto en una fortaleza.
Los tres posts anteriores de la serie cubrieron la parte cliente. Hoy toca el lado del servidor. La instalación es trivial — un paquete y listo. La securización es otra historia, y toda ella vive en un único fichero: /etc/ssh/sshd_config.
🛡️ Backup primero, siempre
Antes de tocar nada:
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup$(date +%d-%m-%Y)Si algo se tuerce después de reiniciar el servicio, tienes la vuelta atrás en un segundo.
⚙️ sshd_config comentado
Lo más directo: un fichero de configuración real con cada opción explicada. Sin relleno.
# Incluir configs adicionales del directorio .d/
Include /etc/ssh/sshd_config.d/*.conf
# Puerto no estándar — evita el 90% de los scanners automáticos
Port 49152
# Solo IPv4 (inet) o IPv6 (inet6)
AddressFamily inet
# IP de escucha — útil si el host tiene varias interfaces
ListenAddress 0.0.0.0
# ─── Autenticación ──────────────────────────────────────────
# Segundos para completar el login antes de cortar la conexión
LoginGraceTime 60
# Root nunca por SSH — siempre un usuario sin privilegios + sudo/su
PermitRootLogin no
# Verifica permisos de ficheros clave (600 para authorized_keys)
StrictModes yes
# Intentos fallidos antes de cortar la conexión
MaxAuthTries 3
# Sesiones simultáneas por conexión
MaxSessions 2
# Autenticación por clave pública — lo que queremos
PubkeyAuthentication yes
# Fichero donde viven las claves autorizadas
AuthorizedKeysFile .ssh/authorized_keys
# Sin contraseñas — si usas claves, esto va a no
PasswordAuthentication no
# Nunca contraseñas vacías
PermitEmptyPasswords no
# Challenge-response (ej. tokens OTP vía PAM) — actívalo si usas 2FA
ChallengeResponseAuthentication no
# PAM gestiona cuentas y sesiones — mantenerlo activo
UsePAM yes
# ─── Sesión ─────────────────────────────────────────────────
# Sin X11 si no necesitas entorno gráfico remoto
X11Forwarding no
# Sin banner de último login — menos ruido
PrintLastLog yes
PrintMotd no
# Desconecta sesiones inactivas tras 5 minutos
ClientAliveInterval 300
ClientAliveCountMax 0
# Sin reverse DNS — acelera el login
UseDNS no
# ─── Subsistemas ────────────────────────────────────────────
# SFTP integrado — para transferencia de ficheros
Subsystem sftp /usr/lib/openssh/sftp-serverTras cualquier cambio, verificar la sintaxis antes de recargar:
sshd -t && systemctl reload ssh👥 Control de acceso por usuario y grupo
SSH puede restringir quién puede entrar antes de llegar a la autenticación. Estas directivas van al final del sshd_config:
# Solo estos usuarios pueden conectarse
AllowUsers adminuser backupuser
# O por grupo — más cómodo si hay muchos usuarios
AllowGroups sshusers
# Bloquear explícitamente usuarios de prueba
DenyUsers testuser guest
# Bloquear todo un grupo
DenyGroups nogroupCon Match puedes afinar más: por ejemplo, permitir a un usuario solo desde una IP concreta:
Match Address 203.0.113.0/24
AllowUsers adminuser🔒 Fail2ban — el portero de discoteca
Fail2ban lee los logs y banea automáticamente las IPs que fallan repetidamente. La configuración va en /etc/fail2ban/jail.local — nunca edites el .conf original para no perder los cambios en actualizaciones:
[sshd]
enabled = true
port = 49152
# Para sistemas con systemd (Debian 10+, Ubuntu 20.04+)
backend = systemd
maxretry = 3
bantime = 3600
findtime = 600Con esto: 3 intentos fallidos en 10 minutos → la IP queda baneada 1 hora.
# Ver estado del jail SSH
fail2ban-client status sshd
# Desbanear una IP manualmente (si te baneas tú mismo)
fail2ban-client set sshd unbanip 1.2.3.4🚪 Port Knocking — la puerta invisible
El port knocking mantiene el puerto SSH completamente cerrado en el firewall hasta que envías una secuencia secreta de paquetes. Sin la secuencia correcta, el puerto no existe para el exterior.
# Instalar knockd
apt install knockd
# /etc/knockd.conf
[options]
UseSyslog
[openSSH]
sequence = 7000,8000,9000
seq_timeout = 5
command = ufw allow from %IP% to any port 49152
tcpflags = syn
[closeSSH]
sequence = 9000,8000,7000
seq_timeout = 5
command = ufw delete allow from %IP% to any port 49152
tcpflags = synDesde el cliente:
# Abrir la puerta
knock -v servidor 7000 8000 9000
# Conectar normalmente
ssh -p 49152 usuario@servidor
# Cerrar la puerta al salir
knock -v servidor 9000 8000 7000Sin conocer la secuencia, un scanner de puertos no verá nada. El puerto no responde, no existe.
🔑 Solo claves — el estándar de oro
Las contraseñas son cosa del siglo XX. Una clave ed25519 bien guardada es infinitamente más segura que cualquier contraseña:
# Generar el par de claves — ed25519 es el algoritmo moderno
ssh-keygen -t ed25519 -a 100 -C "usuario@maquina"
# Copiar la clave pública al servidor
ssh-copy-id -p 49152 usuario@servidorUna vez copiada la clave y verificado que el acceso funciona:
# En sshd_config — el paso crítico
PasswordAuthentication no
PubkeyAuthentication yesAhora sin la clave privada, nadie entra. La fuerza bruta queda matemáticamente fuera de juego.
🤖 Playbook Ansible
Si gestionas varios servidores, este playbook aplica todo lo anterior de una vez. Usa tags para poder aplicar solo las partes que necesitas:
---
- name: Hardening SSH
hosts: servers
become: yes
vars:
ssh_port: 49152
ssh_allowed_users: "adminuser backupuser"
tasks:
- name: Backup sshd_config
copy:
src: /etc/ssh/sshd_config
dest: "/etc/ssh/sshd_config.backup{{ ansible_date_time.date }}"
remote_src: yes
tags: always
- name: Puerto y sin root
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.reg }}"
line: "{{ item.line }}"
loop:
- { reg: '^#?Port', line: "Port {{ ssh_port }}" }
- { reg: '^#?PermitRootLogin', line: "PermitRootLogin no" }
- { reg: '^#?X11Forwarding', line: "X11Forwarding no" }
- { reg: '^#?MaxAuthTries', line: "MaxAuthTries 3" }
notify: restart ssh
tags: basic
- name: Solo claves, sin contraseñas
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: "PasswordAuthentication no"
notify: restart ssh
tags: keys_only
- name: AllowUsers
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?AllowUsers'
line: "AllowUsers {{ ssh_allowed_users }}"
notify: restart ssh
tags: allowusers
- name: Instalar Fail2ban
apt:
name: fail2ban
state: present
tags: fail2ban
- name: Configurar jail SSH
copy:
dest: /etc/fail2ban/jail.local
content: |
[sshd]
enabled = true
port = {{ ssh_port }}
backend = systemd
maxretry = 3
bantime = 3600
notify: restart fail2ban
tags: fail2ban
handlers:
- name: restart ssh
service:
name: ssh
state: restarted
- name: restart fail2ban
service:
name: fail2ban
state: restarted# Aplicar hardening básico + claves
ansible-playbook hardening_ssh.yml --tags "basic,keys_only"
# Solo Fail2ban
ansible-playbook hardening_ssh.yml --tags fail2ban✅ Resultado
Con esto tienes un servidor SSH que:
- No responde en el puerto 22 — los scanners automáticos pasan de largo
- Solo acepta claves — la fuerza bruta es un problema de otro
- Banea IPs que insisten — Fail2ban hace de portero
- El puerto ni existe sin la secuencia — Port Knocking como capa extra opcional
- Solo entran los usuarios que deben — AllowUsers/AllowGroups controla la lista
Todo con un único fichero de configuración y sin instalar agentes propietarios ni servicios de terceros. OpenSSH lleva décadas entre nosotros y tiene todo lo necesario — solo hay que configurarlo.