Introducción
Uso ssh todos los días para conectarme desde dos Macs, un Mac con chip M5 y un Mac Mini, a un Intel NUC con EndeavourOS que tengo en casa. Hasta hace poco entraba con usuario y contraseña, como mucha gente que usa SSH a diario sin haberlo configurado nunca.
Si estás en esa situación, este artículo es para ti. Te cuento cómo pasé a entrar solo con claves, cómo aseguré el servidor y qué encontré en el camino. Todas las salidas son reales, de mis equipos; solo cambié las IPs.
Mi escenario:
- Clientes: Mac M5 y Mac Mini, con macOS 27 y OpenSSH 10.3p1.
- Servidor: Intel NUC con EndeavourOS (basado en Arch Linux) y OpenSSH 10.6p1.
- Red: todo dentro de mi red local. El SSH del NUC no está expuesto a internet, pero eso no es excusa para dejarlo con los valores por defecto: cualquier equipo comprometido dentro de la red, como un dispositivo IoT, podría intentar entrar.
Cómo funciona SSH, lo justo para seguir
- Cliente y servidor. El cliente es el comando
sshde tu Mac. El servidor essshd, un servicio que corre en el NUC y espera conexiones en el puerto 22. - Entrar con contraseña. Escribes la contraseña, viaja cifrada al servidor y él la compara. Si alguien la adivina o la roba, puede entrar desde cualquier equipo.
- Entrar con clave. Generas un par de claves: una privada, que se queda en tu Mac, y una pública, que copias al servidor en
~/.ssh/authorized_keys. Para entrar, tu Mac firma un dato de la sesión con la clave privada y el servidor verifica esa firma con la clave pública. La clave privada nunca sale de tu equipo. - La passphrase protege el archivo de la clave privada. Si alguien copia ese archivo, sin la passphrase no puede usarlo.
El fingerprint: cómo sabes que hablas con tu servidor
La primera vez que te conectas a un servidor, ssh te muestra algo así:
The authenticity of host '192.168.x.40 (192.168.x.40)' can't be established.
ED25519 key fingerprint is SHA256:zGw5fMW2HsbEpDmjG0z0dAyftFc3Qz/2k897W9AxJbY.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
Ese fingerprint es una huella corta que identifica la clave del servidor. Antes de escribir yes, compáralo con el que muestra el propio servidor. En el NUC:
ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub
256 SHA256:zGw5fMW2HsbEpDmjG0z0dAyftFc3Qz/2k897W9AxJbY root@NucI5 (ED25519)
Si coinciden, estás hablando con tu servidor y no con otro que se hace pasar por él. Al aceptar, tu Mac guarda esa clave en ~/.ssh/known_hosts, y desde ahí la verifica en cada conexión. Puedes revisar lo que guardó con:
ssh-keygen -F 192.168.x.40 -l
# Host 192.168.x.40 found: line 10
192.168.x.40 ED25519 SHA256:zGw5fMW2HsbEpDmjG0z0dAyftFc3Qz/2k897W9AxJbY
# Host 192.168.x.40 found: line 11
192.168.x.40 RSA SHA256:xadiaRssYWZTK3TQfrKdXVR9FvhXGWacOWywkpTr3q8
# Host 192.168.x.40 found: line 12
192.168.x.40 ECDSA SHA256:u5xMZx9OqF+WTu769U8QnxU/xrThHYqwwd0kHmw5Qps
La primera línea coincide con la del servidor. Las otras dos aparecen porque el cliente aprende automáticamente las demás claves que tiene el servidor, una opción que viene activada por defecto (UpdateHostKeys, en ssh_config(5)).
Si algún día ssh te avisa que la clave de un servidor cambió, no lo ignores: o reinstalaste el servidor, o alguien está en medio de la conexión.
Paso 1: generar la clave en el Mac
ssh-keygen -t ed25519 -C "jmmv@mac-m5" -f ~/.ssh/nuc_ed25519
-t ed25519: el tipo de clave. Ed25519 es la recomendada hoy, yssh-keygenla genera por defecto desde OpenSSH 9.5 (release notes).-C "jmmv@mac-m5": un comentario. Pon algo que identifique el equipo donde vive la clave; más adelante verás por qué.-f ~/.ssh/nuc_ed25519: el nombre del archivo.
Te pide una passphrase dos veces. Usa una buena: en el paso 4 la guardaremos en el Llavero para no escribirla cada vez.
Generating public/private ed25519 key pair.
Enter passphrase for "/Users/jmmv/.ssh/nuc_ed25519" (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/jmmv/.ssh/nuc_ed25519
Your public key has been saved in /Users/jmmv/.ssh/nuc_ed25519.pub
The key fingerprint is:
SHA256:uur7Kk/KDZo3Wo6aqpPXSAqEFdsKDF2MzTLUjI8Ak28 jmmv@mac-m5
The key's randomart image is:
+--[ED25519 256]--+
|=+o@. |
|=.Bo* |
|o=.=. |
|.oE.. |
|... S |
|. . . |
|.+oo. . |
|+O*=. . |
|&=+=B=o |
+----[SHA256]-----+
Quedan dos archivos:
-rw-------@ 1 jmmv staff 444 14 oct. 11:56 nuc_ed25519
-rw-r--r--@ 1 jmmv staff 93 14 oct. 11:56 nuc_ed25519.pub
nuc_ed25519es la clave privada. Solo tú puedes leerla (-rw-------) y nunca se comparte.nuc_ed25519.pubes la clave pública. Es la que copias al servidor y no es secreta.
Paso 2: copiar la clave pública al NUC
macOS ya trae ssh-copy-id en /usr/bin. Este comando copia tu clave pública al servidor y ajusta los permisos. Te pedirá la contraseña del NUC por última vez:
ssh-copy-id -i ~/.ssh/nuc_ed25519.pub [email protected]
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/Users/jmmv/.ssh/nuc_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
[email protected]'s password:
Number of key(s) added: 1
Si no tienes ssh-copy-id
Puedes hacerlo a mano. Copia la clave pública al servidor:
scp ~/.ssh/nuc_ed25519.pub [email protected]:~/
Luego conéctate al servidor y agrégala a authorized_keys, creando antes la carpeta ~/.ssh con los permisos correctos por si no existe:
ssh [email protected]
mkdir -p ~/.ssh && chmod 700 ~/.ssh
cat ~/nuc_ed25519.pub >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
rm ~/nuc_ed25519.pub
Paso 3: probar que entras solo con la clave
Antes de cambiar nada en el servidor, comprueba que la clave funciona por sí sola:
ssh -i ~/.ssh/nuc_ed25519 -o IdentitiesOnly=yes -o PreferredAuthentications=publickey [email protected] 'echo OK-clave; ls -ld ~/.ssh; ls -l ~/.ssh/authorized_keys'
-i ~/.ssh/nuc_ed25519: usa esta clave.-o IdentitiesOnly=yes: prueba solo esa clave, aunque tengas otras en el Mac.-o PreferredAuthentications=publickey: prohíbe la contraseña. Si la clave no funciona, la conexión falla en vez de pedírtela.- El texto entre comillas se ejecuta en el NUC y devuelve el resultado.
Enter passphrase for key '/Users/jmmv/.ssh/nuc_ed25519':
OK-clave
drwx------ 2 jmmv jmmv 4096 Oct 14 11:58 /home/jmmv/.ssh
-rw------- 1 jmmv jmmv 93 Oct 14 11:58 /home/jmmv/.ssh/authorized_keys
Pidió la passphrase de la clave, no la contraseña del NUC. Además, ssh-copy-id creó ~/.ssh con permisos 700 y authorized_keys con 600, sin que tuviera que hacerlo a mano.
Paso 4: guardar la passphrase en el Llavero y crear un alias
Escribir la passphrase en cada conexión cansa. macOS puede guardarla en el Llavero:
/usr/bin/ssh-add --apple-use-keychain ~/.ssh/nuc_ed25519
En el Mac Mini, la salida fue:
Enter passphrase for /Users/jmmv/.ssh/nuc_ed25519:
Identity added: /Users/jmmv/.ssh/nuc_ed25519 (jmmv@macmini)
Uso la ruta completa /usr/bin/ssh-add a propósito: la opción --apple-use-keychain solo existe en la versión de Apple. Si instalaste OpenSSH con Homebrew, la otra versión no la reconoce.
Ahora crea un alias en ~/.ssh/config. Ábrelo con nano ~/.ssh/config y agrega:
Host nuc
HostName 192.168.x.40
User jmmv
Port 22
IdentityFile ~/.ssh/nuc_ed25519
IdentitiesOnly yes
AddKeysToAgent yes
UseKeychain yes
Host nuc: el alias. Desde ahora te conectas conssh nuc.IdentityFile: la clave que usa este servidor.IdentitiesOnly yes: ofrece solo esa clave. Yo tengo varias claves en el Mac; sin esta línea,sshlas prueba una a una, y cada intento fallido cuenta contra el límite de intentos del servidor que configuraremos en el paso 5.AddKeysToAgent yes: al usar la clave, la carga en el agente de SSH para no pedirla otra vez.UseKeychain yes: opción exclusiva de macOS. Toma la passphrase del Llavero.
Si editas el archivo agregando líneas con echo ... >>, revisa antes que termine en un salto de línea. Mi archivo no lo tenía (zsh lo indica con un % al final de la última línea), y la primera línea agregada se habría pegado a la anterior.
Prueba:
ssh nuc 'echo OK-sin-preguntas'
ssh-add -l
OK-sin-preguntas
256 SHA256:uur7Kk/KDZo3Wo6aqpPXSAqEFdsKDF2MzTLUjI8Ak28 jmmv@mac-m5 (ED25519)
Para comprobar que el Llavero funciona de verdad, simula un reinicio: saca la clave del agente y vuelve a conectarte.
ssh-add -d ~/.ssh/nuc_ed25519
ssh nuc 'echo OK-keychain'
Si responde OK-keychain sin pedirte nada, ssh tomó la passphrase del Llavero y volvió a cargar la clave.
Un detalle curioso: en el Mac Mini, ssh -G nuc (que muestra la configuración efectiva) lista identitiesonly y addkeystoagent, pero no usekeychain. La prueba anterior confirma que la opción sí funciona; simplemente ese comando no la muestra.
Más de un equipo: una clave por Mac
Desde el Mac Mini repetí los pasos 1 a 4, con otra clave. No copié la clave privada del M5. Con una clave por equipo:
- Si pierdes un Mac o te lo roban, borras solo su línea en
authorized_keysy el otro sigue funcionando. - En el servidor sabes qué equipo entró, porque cada clave tiene su fingerprint.
Ahí cometí un error útil. En el Mac Mini usé el comentario jmmv@mac-nuc, que describe el destino y no el equipo. En el authorized_keys del NUC no se distinguía a simple vista cuál era la clave del Mac Mini. Lo corregí en dos lugares.
En el Mac Mini, cambié el comentario de la clave (pide la passphrase):
ssh-keygen -c -C "jmmv@macmini" -f ~/.ssh/nuc_ed25519
ssh-keygen -l -f ~/.ssh/nuc_ed25519.pub
Old comment: jmmv@mac-nuc
Comment 'jmmv@macmini' applied
256 SHA256:ph9cssN4/b79mFa/kB/DxNGDxCsjDqGcWYSQ3nS7qqU jmmv@macmini (ED25519)
El fingerprint no cambió: el comentario es solo texto y no forma parte de la clave.
En el NUC, hice un respaldo y cambié el comentario en authorized_keys:
cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak
sed -i 's/ jmmv@mac-nuc$/ jmmv@macmini/' ~/.ssh/authorized_keys
awk '{print $1, $NF}' ~/.ssh/authorized_keys
ssh-ed25519 jmmv@mac-m5
ssh-ed25519 jmmv@macmini
Comprobé que los dos Macs seguían entrando y luego borré el respaldo.
Paso 5: asegurar sshd en el NUC
Antes de tocar nada
- Confirma que todos tus accesos usan clave. Después de este paso, cualquier equipo, app o script que entre con contraseña se queda fuera. En mi caso solo eran los dos Macs, y ya entraban con clave.
- Deja una sesión abierta en el servidor mientras haces cambios. Si algo falla, desde ahí lo reviertes. En el NUC tengo además acceso físico como último recurso.
La foto del “antes”
sshd -T muestra la configuración efectiva del servidor:
sudo sshd -T | grep -iE '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractiveauthentication|logingracetime|maxauthtries|maxstartups|persource)'
LoginGraceTime 120
MaxAuthTries 6
PermitRootLogin prohibit-password
PubkeyAuthentication yes
PasswordAuthentication yes
KbdInteractiveAuthentication no
PerSourcePenaltyExemptList none
MaxStartups 10:30:100
PerSourceMaxStartups none
PerSourceNetBlockSize 32:128
PerSourcePenalties crash:90.000000 authfail:5.000000 noauth:1.000000 invaliduser:5.000000 grace-exceeded:10.000000 refuseconnection:10.000000 max:600.000000 min:15.000000 max-sources4:65536 max-sources6:65536 overflow:permissive overflow6:permissive
Fíjate en el -i del grep. Mi primer intento fue sin él y no mostró nada. Desde OpenSSH 10.4, el volcado de configuración escribe los nombres con mayúsculas y minúsculas (PermitRootLogin); antes solo usaba minúsculas (release notes). Muchas guías todavía usan grep sin -i, y en un sistema actualizado no encuentran nada.
Dónde va la configuración
En EndeavourOS, /etc/ssh/sshd_config incluye en su línea 2 todos los archivos de /etc/ssh/sshd_config.d/:
grep -n -i '^include' /etc/ssh/sshd_config
2:Include /etc/ssh/sshd_config.d/*.conf
Esa carpeta ya tenía dos archivos: 20-systemd-userdb.conf y 99-archlinux.conf. El segundo contiene:
# sshd_config defaults on Arch Linux
KbdInteractiveAuthentication no
UsePAM yes
PrintMotd no
Nadie define PasswordAuthentication, así que queda el valor por defecto: yes. Por eso el NUC aceptaba contraseñas.
Una regla de sshd que confunde a mucha gente: para cada directiva usa el primer valor que lee, no el último (sshd_config(5)). Los archivos de la carpeta se leen en orden alfabético. Por eso llamé al mío 10-hardening.conf: se lee antes que los demás y sus valores mandan.
El archivo de hardening
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf > /dev/null <<'EOF'
# Hardening de acceso SSH
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
LoginGraceTime 30
PerSourceMaxStartups 3
EOF
sudo chmod 644 /etc/ssh/sshd_config.d/10-hardening.conf
| Directiva | Qué hace | Antes |
|---|---|---|
PermitRootLogin no | Root no entra por SSH, ni con clave | prohibit-password |
PasswordAuthentication no | Se acaba el acceso por contraseña | yes |
KbdInteractiveAuthentication no | Cierra la otra vía de contraseña | no (lo dejo explícito) |
MaxAuthTries 3 | Máximo 3 intentos de autenticación por conexión | 6 |
LoginGraceTime 30 | 30 segundos para autenticarse; si no, se corta la conexión | 120 |
PerSourceMaxStartups 3 | Máximo 3 conexiones sin autenticar desde una misma IP | sin límite |
Algunas precisiones sobre lo que hace cada una:
- Root. Para administrar el servidor entras con tu usuario y usas
sudo. Así cada acción queda asociada a una persona y registrada en los logs del sistema, no a una cuenta compartida. MaxAuthTriescorta la conexión cuando se agotan los intentos, pero no bloquea la IP: el atacante puede reconectarse de inmediato. El bloqueo temporal lo hace otra función, que veremos más abajo.LoginGraceTimees el tiempo que tiene alguien para autenticarse después de conectarse. No cierra sesiones inactivas una vez que entraste.PerSourceMaxStartupses el límite por IP. Existe desde OpenSSH 8.5. La directiva que suele citarse,MaxStartups, limita las conexiones sin autenticar en total, no por IP.
Validar antes de aplicar
sudo sshd -t && echo "config OK"
sudo sshd -T | grep -iE '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|logingracetime|maxauthtries|persourcemaxstartups)'
config OK
LoginGraceTime 30
MaxAuthTries 3
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PerSourceMaxStartups 3
sshd -t revisa que no haya errores de sintaxis. sshd -T muestra cómo quedará la configuración. Hasta aquí nada cambió para las conexiones: el servicio sigue con la configuración anterior hasta que lo recargues.
Aplicar y probar
sudo systemctl reload sshd
reload aplica la configuración nueva sin cortar las sesiones abiertas. Primero comprueba, desde cada Mac, que sigues entrando:
ssh nuc 'echo OK-M5-tras-hardening'
OK-M5-tras-hardening
Después, las pruebas que deben fallar:
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password nuc
ssh -l root nuc
[email protected]: Permission denied (publickey).
[email protected]: Permission denied (publickey).
La primera prueba que ya no se acepta contraseña. La segunda, que root ya no puede entrar. El (publickey) final indica el único método que el servidor ofrece ahora.
Qué queda en los logs
sudo journalctl -u sshd --since "15 min ago" --no-pager | tail -25
Extracto, sin las líneas repetidas:
oct 14 12:47:35 NucI5 systemd[1]: Reloading OpenSSH Daemon...
oct 14 12:47:35 NucI5 sshd[566]: Received SIGHUP; restarting.
oct 14 12:47:35 NucI5 systemd[1]: Reloaded OpenSSH Daemon.
oct 14 12:48:00 NucI5 sshd-session[7881]: Accepted publickey for jmmv from 192.168.x.32 port 49212 ssh2: ED25519 SHA256:ph9cssN4/b79mFa/kB/DxNGDxCsjDqGcWYSQ3nS7qqU
oct 14 12:48:19 NucI5 sshd-session[7909]: Connection closed by authenticating user jmmv 192.168.x.32 port 49213 [preauth]
oct 14 12:48:25 NucI5 sshd-session[7914]: Connection closed by authenticating user root 192.168.x.32 port 49214 [preauth]
Accepted publickeytrae el fingerprint de la clave.ph9css…es la del Mac Mini: aquí se ve la ventaja de tener una clave por equipo.- Las dos líneas de
Connection closed ... [preauth]son los rechazos de la prueba anterior.
Si algo sale mal
Desde la sesión que dejaste abierta, borra el archivo y recarga:
sudo rm /etc/ssh/sshd_config.d/10-hardening.conf
sudo systemctl reload sshd
Lo que OpenSSH ya hace por ti
Bloqueo temporal tras fallos
Desde OpenSSH 9.8, sshd bloquea por un tiempo las direcciones que fallan la autenticación repetidamente, y viene activado por defecto (release notes). Es la línea PerSourcePenalties de la foto del “antes”:
authfail:5: 5 segundos de penalización por cada conexión con autenticación fallida.invaliduser:5: lo mismo para intentos con usuarios que no existen. Esta penalización se añadió en OpenSSH 10.3.max:600: las penalizaciones se acumulan hasta un máximo de 10 minutos.
Este es el bloqueo temporal que muchas guías atribuyen a MaxAuthTries. Si haces varias pruebas fallidas seguidas, es normal que tu propia IP quede rechazada unos segundos.
“Usa el Protocolo 2”: un consejo que ya no aplica
Muchas guías todavía recomiendan configurar Protocol 2 para desactivar la versión 1 de SSH. OpenSSH eliminó el soporte de SSH-1 del servidor en la versión 7.4 (2016) y del cliente en la 7.6 (2017), y la directiva Protocol ya no existe en sshd_config. Si una guía te pide configurarla, es una guía vieja.
Un hallazgo en macOS: intercambio de claves poscuántico
Revisando los logs del NUC encontré esto en cada conexión de mis Macs:
WARNING: connection from 192.168.x.74 port 51512 is not using a post-quantum key exchange algorithm: "ecdh-sha2-nistp256" [preauth]
Al inicio de cada conexión, cliente y servidor acuerdan cómo generar las claves que cifran la sesión. Los algoritmos poscuánticos protegen contra ataques de “guardar ahora, descifrar después”: alguien graba tu tráfico hoy y lo descifra en el futuro con un computador cuántico. El NUC los soporta, pero mis Macs estaban eligiendo uno clásico.
La causa es el orden de preferencia del cliente. En SSH se usa el primer algoritmo de la lista del cliente que el servidor también soporte:
ssh -G nuc | grep -i '^kexalgorithms'
kexalgorithms ecdh-sha2-nistp256,mlkem768x25519-sha256,sntrup761x25519-sha512,[email protected],curve25519-sha256,[email protected],ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256
En el OpenSSH original, el primero de esa lista es mlkem768x25519-sha256 y ecdh-sha2-nistp256 aparece más abajo (ssh_config(5)). En mis dos Macs ningún archivo de configuración cambia ese orden, así que es un ajuste del cliente de Apple. Por qué lo hace no lo pude confirmar.
La corrección es una línea al final de ~/.ssh/config:
Host *
KexAlgorithms ^mlkem768x25519-sha256
Host *aplica a todos los servidores. Va al final del archivo porque el cliente también usa el primer valor que encuentra: lo específico arriba, lo general abajo.- El
^pone ese algoritmo al inicio de la lista sin quitar los demás. Con un servidor que no lo soporte, la conexión sigue funcionando con otro algoritmo.
Resultado:
ssh -v nuc exit 2>&1 | grep -i 'kex: algorithm'
debug1: kex: algorithm: mlkem768x25519-sha256
Conclusión
Empecé entrando al NUC con una contraseña y terminé con esto:
- Una clave Ed25519 por Mac, protegida con passphrase y guardada en el Llavero.
- Un alias en
~/.ssh/configque ofrece solo la clave correcta. - Un servidor que solo acepta claves, no deja entrar a root y limita intentos, tiempos y conexiones por IP.
- Un intercambio de claves poscuántico entre mis Macs y el NUC.
Nada de esto requirió instalar software: todo viene en macOS y en OpenSSH. Lo que más tiempo me tomó no fue la configuración, sino verificar cada cambio antes de aplicar el siguiente: probar la clave antes de apagar las contraseñas, validar con sshd -t antes de recargar y mantener una sesión abierta por si algo fallaba. Ese orden es lo que te evita quedarte fuera de tu propio servidor.
