# Verificación de hosts SSH para agentes que acceden a producción

Un agente autónomo de programación nunca debería decidir por sí mismo que un servidor SSH nuevo es de confianza. Puede inspeccionar un repositorio, preparar un despliegue y solicitar acceso, pero debe detenerse cuando la identidad remota no coincide con un registro establecido por una persona o por un proceso de aprovisionamiento fiable.

SSH plantea dos preguntas de seguridad distintas: «¿Llegué al servidor previsto?» y «¿Qué puede hacer esta cuenta en ese servidor?». Los equipos suelen mezclarlas porque ambas usan claves públicas. Ese error convierte una conexión mal dirigida en una exposición de credenciales y después convierte una cuenta con permisos excesivos en un incidente de producción.

El diseño práctico es sencillo: fija la identidad del host antes de que el agente se conecte, asigna a cada tarea del agente una cuenta remota con permisos limitados y pide a una persona que revise los comandos cuyo efecto no pueda inferirse de forma segura. Cada capa limita un fallo distinto. Ninguna sustituye a las demás.

## La identidad del host debe comprobarse antes de autenticar al usuario

La verificación del host comprueba si el cliente SSH llegó al servidor al que pretendía llegar. La autenticación del usuario comprueba si ese servidor acepta la credencial de cuenta ofrecida por el cliente. El orden importa porque SSH negocia y verifica la clave de host del servidor antes de que el cliente envíe material de autenticación del usuario.

Una clave de host pertenece al servidor, no al administrador ni al agente. La huella digital es una representación breve de esa clave pública de host, normalmente mostrada en formato SHA256. Si `deploy.example.internal` suele presentar una huella y de pronto presenta otra, el cliente tiene pruebas de que algo ha cambiado. Puede tratarse de una reconstrucción legítima. También puede ser un error de DNS, una dirección reutilizada, un fallo en el bastion o un intento activo de interceptación.

Imagina que un agente de programación recibe la instrucción de ejecutar una migración en `db-prod.internal`. Su configuración SSH resuelve ese nombre en una dirección. Un atacante que pueda influir en el DNS, en la ruta del proxy o en una entrada obsoleta del inventario puede dirigir la conexión a un servidor bajo su control. Si el cliente acepta la clave de host desconocida, ese servidor puede solicitar la autenticación del usuario. Una clave privada SSH del lado del cliente quizá no salga del cliente, pero el acceso del agente suele incluir contraseñas, flujos de firma de certificados, reenvío o comandos que revelan información útil después del inicio de sesión. Y lo que es más importante, el agente puede ejecutar su comando previsto en la máquina equivocada.

Una huella de host fijada hace que la conexión falle antes de que el agente pueda tratar esa máquina como su objetivo. Por eso un host desconocido o modificado es un límite de autorización, no una advertencia menor que deba ocultarse.

La verificación del host no confirma que una máquina esté en buen estado, correctamente configurada o sea segura de modificar. Solo confirma la continuidad de su identidad criptográfica. Ese límite resulta útil. No le pidas a una huella de host que decida si `rm -rf`, una migración de esquema o un cambio en el firewall tienen sentido.

## Confiar en el primer uso no encaja bien con el trabajo desatendido

Confiar en el primer uso puede tolerarse cuando un desarrollador se conecta manualmente a una máquina personal desechable. Es un valor predeterminado deficiente para un proceso autónomo, porque la primera conexión es precisamente el momento en que alguien debe decidir si el nombre de host, la ruta y la huella corresponden entre sí.

OpenSSH documenta esta elección en el manual `ssh_config`, dentro de `StrictHostKeyChecking`. Con `yes`, el cliente nunca añade automáticamente claves de host desconocidas y rechaza una clave modificada. Con `accept-new`, registra automáticamente una clave desconocida, pero sigue rechazando una clave modificada. Con `no` u `off`, acepta más casos que requieren la atención de un operador.

A menudo se presenta `accept-new` como un compromiso razonable. Reduce la fricción cuando los hosts se crean con frecuencia. También concede a una ruta de red el derecho de establecer el registro de identidad inicial. Para un agente que puede modificar la infraestructura, esa decisión debería corresponder a otra parte.

Usa `StrictHostKeyChecking=yes` para los endpoints de producción y staging operados por agentes. Cuando una conexión falle porque el host es desconocido, envía la solicitud del agente a una persona que pueda comparar la huella indicada con una fuente externa a esa conexión SSH. Una consola del proveedor, un registro de inventario firmado, una consola física o un canal de gestión existente pueden servir para hacer esa comparación.

No resuelvas la interrupción colocando `StrictHostKeyChecking=no` en un archivo de configuración global. Esa línea suele sobrevivir al incidente temporal que la justificó y después se aplica silenciosamente a hosts para los que nadie quiso relajar la protección.

Existe una excepción más limitada: infraestructura de pruebas de corta duración cuyas identidades de host proceden de un sistema de aprovisionamiento capaz de publicar una lista autenticada de hosts antes de que comiencen las pruebas. Aun así, el agente no debería aprender la identidad de un host a partir de su propio primer contacto de red. La fuente de confianza cambió, pero no desapareció.

## Una huella solo sirve si su fuente es independiente

Una huella copiada del endpoint que intentas verificar demuestra muy poco. `ssh-keyscan` resulta práctico para recopilar claves públicas de host, y esa comodidad crea una trampa conocida: un operador lo ejecuta contra un nombre de host, pega el resultado en `known_hosts` y da el host por verificado. Si el DNS o el enrutamiento ya apuntan a un atacante, lo que ha fijado es la clave del atacante.

Usa `ssh-keyscan` como comando de recopilación después de obtener una huella independiente, no como fuente de confianza. Por ejemplo, un administrador puede recuperar la huella de una clave de host desde la consola de un proveedor o desde un registro de compilación firmado y después compararla con la clave recopilada.

```sh
ssh-keyscan -t ed25519 app-prod.internal > /tmp/app-prod.hostkey
ssh-keygen -lf /tmp/app-prod.hostkey -E sha256
```

El segundo comando muestra una salida con este formato:

```text
256 SHA256:exampleFingerprintMaterial app-prod.internal (ED25519)
```

Compara carácter por carácter el valor `SHA256:` con el valor obtenido de forma independiente. Comprueba también el algoritmo. Si el registro indica ED25519 y el resultado recopilado es RSA, detente e investiga en lugar de tratar ambos resultados como intercambiables.

Después fija la clave pública, no solo una nota que contenga su huella. Un archivo dedicado mantiene los objetivos del agente separados de la acumulación personal de máquinas antiguas de un desarrollador:

```text
app-prod.internal ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExamplePublicHostKeyMaterial
[192.0.2.44]:22 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExamplePublicHostKeyMaterial
```

Incluye tanto el nombre de host canónico como cualquier formato de dirección que el agente tenga permitido usar. De lo contrario, un flujo de trabajo que un día usa una dirección y al siguiente un nombre puede volver a tomar una decisión de confianza nueva. Mantén el archivo bajo una gestión de configuración controlada, con una revisión del cambio que identifique la huella anterior y la de reemplazo.

Los registros DNS SSHFP pueden ayudar cuando la validación DNSSEC está correctamente desplegada de extremo a extremo. No solucionan una configuración del agente que acepta respuestas DNS normales sin firma como prueba. Trata SSHFP como un canal adicional de publicación verificada, no como un registro decorativo que vuelve seguro el primer uso ciego.

## Fija la ruta y el registro del host en la configuración SSH

Un agente necesita una configuración SSH que elimine la ambigüedad, no una que herede los hábitos de una estación de trabajo. Fija el nombre de destino, el archivo de claves de host esperado, la cuenta y el comportamiento de la conexión en una entrada explícita.

```sshconfig
Host app-production
    HostName app-prod.internal
    User agent_release
    Port 22
    UserKnownHostsFile ~/.ssh/agent_known_hosts
    GlobalKnownHostsFile /dev/null
    StrictHostKeyChecking yes
    UpdateHostKeys no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    ForwardAgent no
    PermitLocalCommand no
```

Este fragmento evita varios errores habituales. `UserKnownHostsFile` evita depender por sorpresa de las entradas de host que haya acumulado una persona. `GlobalKnownHostsFile /dev/null` impide que un archivo no administrado de toda la máquina amplíe la confianza en silencio. `UpdateHostKeys no` evita que las actualizaciones automáticas de claves modifiquen el conjunto fijado durante el trabajo del agente. `ForwardAgent no` impide que la máquina remota use un agente de autenticación reenviado para acceder a otro lugar.

El manual `ssh_config` de OpenSSH explica que `UpdateHostKeys` permite rotar claves de host cuando un host demuestra que posee una clave de confianza. Ese comportamiento puede ser útil en flotas interactivas con una gestión disciplinada de hosts. Para un agente autónomo, la mutación automática dificulta la revisión de incidentes. Una persona debería aprobar un cambio de identidad en producción y actualizar deliberadamente el archivo de hosts controlado.

Usa un alias estable como `app-production` en la solicitud del agente y reserva las direcciones sin alias para procedimientos de emergencia. El alias hace visible el objetivo aprobado en los registros y evita comandos que alternen entre nombres, direcciones temporales y fragmentos de shell copiados.

Prueba la configuración efectiva antes de conceder al agente cualquier vía de acceso a credenciales:

```sh
ssh -G app-production | grep -E '^(hostname|user|stricthostkeychecking|userknownhostsfile|forwardagent|updatehostkeys) '
```

Espera valores parecidos a estos:

```text
hostname app-prod.internal
user agent_release
stricthostkeychecking yes
userknownhostsfile /Users/operator/.ssh/agent_known_hosts
forwardagent no
updatehostkeys no
```

Esto detecta errores de precedencia causados por archivos `Include`, valores predeterminados del usuario y la gestión de configuración. He visto entradas de host cuidadosamente definidas anuladas por una estrofa comodín posterior que cambiaba el usuario, activaba el reenvío o elegía otro archivo de hosts conocidos. El comando que realmente se ejecuta depende de la configuración que debes inspeccionar.

## El alcance de la cuenta limita el daño después de una conexión correcta

Un host verificado aún puede recibir un comando incorrecto, así que asigna al agente una cuenta con una función pequeña y deliberada. No entregues a un agente autónomo la misma cuenta SSH que usa un operador para todas las emergencias.

El alcance de una cuenta tiene cuatro partes: a qué host llega, qué archivos y servicios puede afectar, qué vías de escalada de privilegios posee y durante cuánto tiempo sigue siendo utilizable. Una cuenta separada permite inspeccionar esas respuestas. Un inicio de sesión compartido como `deploy` convierte cada ejecución de automatización en un problema de atribución y suele terminar con permisos amplios porque cada flujo nuevo necesita una excepción más.

Un agente de versiones en un servidor de aplicaciones podría necesitar leer un directorio de versiones, escribir un artefacto nuevo, invocar un único envoltorio de despliegue y reiniciar un servicio. No necesita un shell con `sudo` sin restricciones, acceso de lectura a todos los directorios personales ni capacidad para modificar la configuración SSH.

Cuando la tarea es limitada, restringe la clave pública SSH autorizada con un comando forzado. En `authorized_keys`, una entrada del lado del servidor puede vincular esa credencial a un envoltorio:

```text
command="/usr/local/sbin/agent-release-wrapper",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExampleAgentPublicKey agent-release
```

El envoltorio debe analizar un conjunto pequeño de argumentos, rechazar metacaracteres del shell, registrar la operación solicitada e invocar binarios fijos mediante rutas fijas. No escribas un envoltorio que acepte una cadena arbitraria en `SSH_ORIGINAL_COMMAND` y la entregue a `sh -c`. Eso solo oculta el acceso irrestricto al shell remoto detrás del nombre de una función.

Los comandos forzados no sirven para todas las tareas de mantenimiento. Cuando un agente necesite realmente un shell para investigar, usa una cuenta de investigación separada, con permisos de lectura y sin escalada de privilegios. Crea una vía distinta y revisada de forma explícita para los cambios. Mezclar diagnóstico y modificación en una sola cuenta amplia da al agente demasiado margen para convertir una inferencia imperfecta en una acción irreversible.

Los certificados SSH de corta duración pueden reducir el trabajo de limpieza si ya operas una autoridad certificadora con controles claros de emisión. No eliminan la necesidad de verificar el host. Un certificado dice algo sobre la cuenta del cliente; la huella del host indica qué servidor lo recibió.

## La revisión debe mostrar el efecto, no una intención vaga

Una aprobación de comando solo sirve cuando la persona ve suficiente contexto para juzgar su efecto. «Desplegar la versión» es una intención. `sudo systemctl restart payments-api` en `app-production` como `agent_release` es una acción que un revisor puede evaluar.

Revisa el alias exacto del destino, la cuenta remota, el comando literal, los argumentos y el directorio de trabajo. Comprueba también si el comando invoca un shell, expande variables, lee un script remoto, descarga contenido o usa `sudo`. Esos detalles determinan si una solicitud aparentemente inofensiva puede llegar mucho más lejos de lo que indica su descripción.

Un registro de aprobación razonable para una acción remota sería:

```text
Target: app-production (app-prod.internal)
Account: agent_release
Command: /usr/local/sbin/agent-release-wrapper activate 2025.04.17-rc2
Reason: activate the approved release after smoke tests
```

Compáralo con esta solicitud:

```text
ssh app-production "curl $URL | sudo sh"
```

El segundo comando combina la descarga de contenido remoto, la ejecución de un shell, privilegios elevados y un valor que puede expandirse de forma distinta a la que espera el revisor. Ningún ajuste de verificación del host lo hace seguro. Recházalo y exige un artefacto cuyo resumen se haya comprobado antes, un programa de despliegue fijo y argumentos que nombren una versión aprobada.

La revisión de comandos tiene su propio punto débil: la fatiga por aprobaciones. Si un agente pide confirmación para cada `cat`, `git status` y comprobación de estado del servicio inofensivos, las personas aprenden a aprobar sin leer. Coloca la investigación de solo lectura detrás de una cuenta restringida o un envoltorio de comandos definido con precisión y reserva las aprobaciones interactivas para acciones que escriban, reinicien, roten, modifiquen permisos o crucen un límite de confianza.

Sallyport puede mantener las credenciales SSH dentro de su almacén cifrado y exigir aprobación para cada uso de una credencial seleccionada, mientras su canal SSH se ejecuta mediante `sp-ssh` sin exponer la credencial al agente. Aun así, sigues siendo responsable de fijar el host y decidir si el comando visible merece aprobación.

## La ruta de fallo peligrosa suele ser una cadena de atajos normales

La mayoría de los incidentes SSH relacionados con la automatización no empiezan con una ruptura criptográfica exótica. Empiezan con un atajo que parecía inofensivo durante la configuración.

Imagina un agente de versiones configurado con `StrictHostKeyChecking=accept-new`, una cuenta de despliegue compartida y una solicitud de aprobación que solo dice «ejecutar el despliegue». Un registro DNS apunta brevemente a una máquina de reemplazo antes de que termine la actualización del inventario. El agente ve un host desconocido, registra su clave, inicia sesión en el reemplazo porque acepta la cuenta compartida y ejecuta el envoltorio de despliegue. El envoltorio tiene permisos de escritura amplios porque la cuenta compartida también sirve para reparaciones de emergencia.

Nada de esa secuencia requiere un atacante. Un error habitual de nombres puede desplegar artefactos en el entorno equivocado, exponer la salida del despliegue a una máquina no prevista o modificar un host que no está preparado. Si añadimos un atacante capaz de influir en la resolución de nombres o en el enrutamiento, los mismos atajos abren una vía mucho peor.

Cada control interrumpe un punto distinto de esa cadena:

1. Un registro de host fijado de antemano rechaza la máquina de reemplazo hasta que un operador la verifica.
2. Una cuenta dedicada limita el daño si el objetivo aprobado sigue siendo incorrecto de una forma que la identidad del host no puede detectar.
3. Un registro de aprobación que contiene el comando completo da al revisor la oportunidad de detectar un objetivo inesperado o una acción elevada.
4. Un registro de sesiones y llamadas permite al equipo reconstruir la solicitud, la aprobación, el objetivo y el resultado después de un fallo.

No sustituyas el primer control por los siguientes. Un revisor puede pasar por alto un objetivo incorrecto. Una restricción de cuenta puede contener un privilegio no detectado. Un registro explica el daño después de los hechos. Fijar la identidad del host evita una clase de errores de conexión antes de que la cuenta remota entre en escena.

## La rotación de claves de host necesita un procedimiento de cambio, no un botón de excepción

Las claves de host cambian por motivos legítimos: se reconstruye una máquina, se reemplaza una imagen, se retira un algoritmo o un operador rota las credenciales tras sospechar una exposición. Trata esos eventos como cambios de identidad planificados y respaldados por pruebas, no como diálogos de advertencia que deben descartarse.

La persona responsable del host debe obtener la nueva clave pública de host desde la consola de la máquina o desde otra vía de gestión autenticada. Debe publicar la nueva entrada en el archivo controlado de hosts conocidos, registrar el motivo del cambio y retirar la entrada antigua solo después de que el cambio se complete correctamente. Si la máquina debe mantener el servicio durante la transición, OpenSSH puede almacenar más de una clave de host aceptada para el mismo nombre. Así, los clientes pueden aceptar las identidades antigua y nueva durante un periodo definido.

Mantén el registro de rotación breve pero específico: nombre de host, dirección si es relevante, huella anterior, huella nueva, motivo, verificador y vencimiento de cualquier periodo con dos claves. Un ticket que diga «SSH cambiado» no basta para distinguir un trabajo planificado de alguien que intenta enseñar a los clientes a confiar en un impostor.

Nunca ordenes al agente ejecutar `ssh-keygen -R hostname` y volver a conectarse automáticamente. Eliminar el registro antiguo borra la señal de discrepancia antes de que alguien haya establecido por qué apareció. Un operador puede usar ese comando después de verificar la situación como parte de una actualización revisada, pero no debe formar parte de la lógica de recuperación de un flujo del agente.

## Los registros de auditoría deben permitir reconstruir la decisión

Después de que una acción SSH falle o te sorprenda, el registro útil responde a cuatro preguntas: qué proceso del agente la solicitó, qué identidad la aprobó, qué conexión y comando exactos se usaron y qué resultado devolvió. «El agente desplegó el servicio» no responde a ninguna.

Mantén los registros de sesión separados de los registros de llamadas individuales. Una sesión identifica la ejecución del agente y permite a un operador revocar su permiso cuando empieza a comportarse mal. Un registro de llamada individual captura el alias del host, la cuenta, la hora, el resultado de la aprobación, el comando y la salida o el error. La distinción importa cuando una ejecución del agente realiza cien lecturas seguras y después una escritura insegura.

Protege los registros frente al agente que generó la solicitud. Si un agente puede editar el registro después de un comando remoto, el registro de auditoría es un diario, no una prueba. El almacenamiento de solo adición con verificación de integridad es un mínimo práctico. Evita también poner secretos en comandos o argumentos, porque un buen registro de auditoría conservará exactamente aquello que le pidas conservar.

Sallyport proyecta los diarios de sesión y actividad desde un registro de auditoría cifrado, encadenado mediante hashes y ciego a la escritura; `sp audit verify` puede verificar la cadena sin conexión y sin una clave del almacén. Usa ese tipo de evidencia para investigar una excepción, pero diseña la fijación de hosts, las restricciones de cuentas y el texto de aprobación antes de que un incidente te dé motivos para leerla.

El primer cambio operativo que conviene hacer es sencillo: encuentra todas las configuraciones SSH de agentes que acepten un host desconocido, sustituye ese comportamiento por un archivo de hosts fijado y prueba la configuración efectiva con `ssh -G`. Encontrarás alias obsoletos, reglas comodín accidentales y cuentas con mucha más autoridad de la que exige su función. Esas son las conexiones que conviene corregir antes de que el agente sea más rápido.
