Claves SSH en el Secure Enclave del Mac: que tus agentes de IA no puedan llevárselas

Esta semana revisé la seguridad de mi Mac. No había nada infectado, pero encontré algo que me gustó menos: ocho claves SSH en ~/.ssh, ninguna con passphrase. Cualquier proceso que corra con mi usuario puede leerlas, y hoy eso incluye varios agentes de IA con acceso a la terminal. Las he cambiado por una clave que vive dentro del Secure Enclave del Mac, no se puede copiar y pide Touch ID en cada conexión nueva. macOS ya trae todo lo necesario, sin instalar nada.

La técnica no es mía: la documentó Arian van Putten en un gist. Lo que cuento aquí es cómo la apliqué en un caso real, con varios servidores, GitHub y un túnel automático, y los problemas que me encontré por el camino.

El problema: tus agentes tienen tus llaves

Un agente como Claude Code, Codex o cualquier otro que ejecute comandos corre con tu usuario. Si puede ejecutar cat, puede leer ~/.ssh/id_ed25519. Las listas de comandos prohibidos ayudan poco, porque hay muchas formas de leer un fichero sin escribir cat. Y no hace falta que el agente se equivoque solo: basta con que lea una web o un correo con instrucciones escondidas (prompt injection) para que intente mandar tu clave a otro sitio.

Una passphrase protege el fichero, pero en cuanto la clave está cargada en el agente SSH, cualquier proceso tuyo puede usarla sin preguntarte. Yo quería dos cosas: que la clave no se pueda copiar nunca, y que cada uso me pida algo que solo tengo yo.

Qué trae macOS

macOS incluye en /usr/lib/ssh-keychain.dylib un proveedor de "security keys" para OpenSSH. Es la misma interfaz que usa una YubiKey, pero aquí la llave es el Secure Enclave del propio Mac. Según el gist funciona en macOS 26 Tahoe, y algunos comentarios dicen que también en Sequoia. Yo lo he probado en macOS 27.0.1 con el OpenSSH del sistema (10.3). Apple no lo documenta para SSH en ningún sitio: el man page de ssh-keychain es de 2020 y solo habla de smartcards.

La clave privada se crea dentro del chip y, si eliges el tipo no exportable, no sale de ahí. En disco solo queda un fichero de referencia que no sirve de nada en otro ordenador.

Paso a paso

1. Comprueba que tu Mac lo soporta

ls -la /usr/lib/ssh-keychain.dylib
nm -gU /usr/lib/ssh-keychain.dylib | grep sk_
ssh -V

Si aparecen sk_enroll y sk_sign, el proveedor está.

2. Crea la clave en el Secure Enclave

sc_auth create-ctk-identity -l ssh-mac -k p-256-ne -t bio
sc_auth list-ctk-identities
  • -k p-256-ne: P-256 no exportable. Sin el -ne la clave se puede exportar con sc_auth export-ctk-identity, y pierdes la gracia. Existe p-384-ne, pero OpenSSH no lo admite.
  • -t bio: Touch ID en cada firma. Con -t none no pide huella; lo uso más abajo para GitHub.

3. Saca la clave pública y el fichero de referencia

mkdir -p ~/.ssh/secure-enclave && cd ~/.ssh/secure-enclave
ssh-keygen -K -N "" -w /usr/lib/ssh-keychain.dylib

Te pide la huella y luego un PIN: déjalo vacío. Crea id_ecdsa_sk_rk_ssh-mac y su .pub. El primero no contiene la clave privada, solo apunta a ella, por eso -N "" no es un problema.

Un detalle que me hizo perder un rato: -K descarga todas las claves del chip, no solo la nueva. Si en la carpeta ya está el fichero de una clave anterior, pregunta si quieres sobrescribirlo, y si lo lanzas desde un script se queda parado ahí. Lo más fácil es ejecutarlo en una carpeta vacía y mover después el fichero que quieres.

4. Configura SSH

# ~/.ssh/config
Host mi-servidor
    HostName servidor.example.com
    User deploy
    IdentityFile ~/.ssh/secure-enclave/id_ecdsa_sk_rk_ssh-mac
    SecurityKeyProvider /usr/lib/ssh-keychain.dylib
    IdentitiesOnly yes
    ControlMaster auto
    ControlPath ~/.ssh/cm/%r@%h:%p
    ControlPersist 1h

IdentitiesOnly evita que SSH pruebe antes tus claves antiguas. La carpeta de ControlPath tiene que existir (mkdir -p ~/.ssh/cm). Lo de ControlMaster lo explico más abajo.

5. Autoriza la clave en el servidor y prueba

Añade el contenido del .pub al ~/.ssh/authorized_keys del servidor, sin quitar todavía la clave antigua, y conéctate:

ssh mi-servidor 'echo ok'

Salta Touch ID y entras. Para estar seguro de que ha usado la clave del chip y no otra, mira el log del servidor. Tiene que decir ECDSA-SK:

journalctl -u ssh --since "-5 min" | grep Accepted
# Accepted publickey for deploy from ... ssh2: ECDSA-SK SHA256:...

Lo que no cuenta la guía original

El servidor tiene que soportarlo

Las claves de este tipo necesitan OpenSSH 8.2 o superior en el servidor. Uno de los míos es AlmaLinux 8, que trae OpenSSH 8.0, y rechaza la clave sin más explicación que un "Permission denied". Se comprueba así:

sudo sshd -T | grep -i pubkeyacceptedalgorithms | tr ',' '\n' | grep sk-

Si no sale nada, ese servidor se queda con una clave normal, con passphrase, hasta que lo actualices.

Una huella por conexión, no por comando

Lo primero que pensé fue: "¿voy a tener que poner el dedo en cada comando?". No. Touch ID se pide en cada conexión nueva. Con ControlMaster y ControlPersist, la primera conexión se queda abierta y las siguientes la reutilizan sin pedir nada. Yo uso una hora: una huella por servidor y por hora de trabajo.

Tiene un coste. Mientras esa conexión siga abierta, otro proceso de tu usuario también puede usarla sin huella. Cuanto más larga la ventana, más cómodo y menos protegido. Una hora me parece un equilibrio razonable; días, no.

GitHub: una segunda clave sin huella

Con GitHub hice otra cosa. Tengo la CLI gh autenticada con un token que puede hacer push por HTTPS, y cualquier agente podría usarlo sin huella. Poner Touch ID solo en el camino SSH no protegía casi nada y sí molestaba en cada push. Así que creé una segunda clave en el chip, sin huella, que sigue sin poder copiarse:

sc_auth create-ctk-identity -l ssh-github -k p-256-ne -t none

Si en tu Mac no hay ningún token con permiso de escritura, la huella en GitHub sí tiene sentido.

Lo automático necesita su propia clave

Tengo un túnel SSH que launchd mantiene abierto todo el día. Con Touch ID no puede funcionar, porque no hay nadie para poner el dedo. La solución es una clave normal sin passphrase, para un usuario que no sea root, y limitada en el servidor a hacer solo eso. En su authorized_keys:

restrict,port-forwarding,permitopen="127.0.0.1:8080",command="/usr/sbin/nologin" ssh-ed25519 AAAA... tunel-mac

restrict lo desactiva todo y luego solo se vuelve a permitir el reenvío a ese puerto. Si alguien roba esa clave, no puede abrir una shell ni llegar a otro puerto. Lo probé antes de darlo por bueno: el login devuelve "This account is currently not available" y un reenvío a otro puerto falla con "administratively prohibited".

Sin atestación y sin copia de seguridad

Dos límites que conviene saber. Apple no da atestación de estas claves, así que no puedes demostrar a un tercero que la clave está de verdad en hardware; el propio gist lo comenta. Y como no se puede exportar, si pierdes o formateas el Mac, pierdes la clave. Antes de quitar las claves antiguas de un servidor, asegúrate de tener otra forma de entrar: la consola web del proveedor, un panel como Plesk o una segunda clave guardada en otro sitio.

Cómo hice la migración

  • Creé la clave nueva y la añadí a los servidores entrando con las claves antiguas, sin quitar nada.
  • Probé cada servidor con la clave nueva y comprobé en el log que entraba como ECDSA-SK.
  • Quité de authorized_keys y de GitHub solo las claves de este Mac, comparando la clave pública y no el comentario. Las de otras máquinas y los deploy keys se quedaron.
  • Comprobé lo contrario: que las claves antiguas ya no entran.
  • Las claves que no recordaba para qué servían no las borré: las moví a ~/.ssh/retired/. SSH ya no las prueba solo, y si algo falla en unas semanas sabré qué era.

Qué no haría

  • Usar el tipo exportable "por si acaso".
  • Dejar ControlPersist en días.
  • Quitar las claves antiguas antes de probar la nueva y tener un acceso alternativo al servidor.
  • Dar a la clave del túnel una shell, o ponerla en root.

¿Merece la pena?

Para mí, sí. El cambio de hábito es pequeño, una huella por servidor y hora, y quita un riesgo que con los agentes de IA ha dejado de ser teórico: que algo que corre con tu usuario se lleve las llaves de tus servidores. Un agente comprometido todavía puede intentar usar la clave, pero no copiarla. Y si lo intenta, te aparece un Touch ID que no esperabas. Esa ventana inesperada es justo la alarma que quieres tener.

Fuentes