# Riesgos del reenvío del agente SSH en el desarrollo asistido por IA

Los riesgos del reenvío del agente SSH suelen subestimarse porque la clave privada permanece en el equipo del desarrollador. Es cierto, pero no basta. Un host remoto que recibe un socket de agente reenviado puede pedirle al agente local que firme desafíos de autenticación SSH. Para cualquier sistema que acepte esa identidad, el host remoto podría actuar en tu nombre mientras dure ese acceso.

El desarrollo asistido por IA hace que este error sea más fácil de cometer y más difícil de detectar. Un agente de programación puede abrir shells remotos, ejecutar comandos de despliegue, inspeccionar repositorios y seguir una configuración SSH que escribiste hace años para una sesión interactiva. Si el agente llega a una máquina que tiene tu socket reenviado, el límite ya se ha desplazado. Desactiva el reenvío de forma predeterminada y asigna a las tareas remotas identidades propias y limitadas cuando de verdad necesiten un salto adicional.

## Un agente reenviado puede autenticarse más allá del primer host

El reenvío del agente SSH expone un servicio de firma, no una copia de la clave privada. Tu `ssh-agent` local guarda una o varias identidades privadas. Cuando te conectas con el reenvío activado, SSH crea un socket en la máquina remota y retransmite las solicitudes de firma por la conexión cifrada hasta el agente local.

Esta diferencia suele llevar a decisiones equivocadas sobre el riesgo. La gente oye «la clave nunca sale de mi portátil» y concluye que la máquina remota no tiene nada que valga la pena atacar. Los bytes privados pueden permanecer en local, pero la autenticación SSH no necesita que la máquina remota posea esos bytes. Necesita una firma válida sobre una solicitud de autenticación. El socket reenviado proporciona una forma de obtenerla.

El manual de OpenSSH `ssh_config(5)` describe `ForwardAgent` con claridad y advierte que los usuarios capaces de saltarse los permisos de archivos en el host remoto pueden acceder al agente reenviado. En la práctica, eso significa que la propia cuenta remota, un proceso que se ejecute con esa cuenta, un administrador o un atacante que consiga suficiente control pueden usar el socket. El acceso root al host remoto zanja cualquier discusión sobre los permisos del socket.

RFC 4252 describe la autenticación SSH mediante clave pública como una firma sobre datos específicos de la conexión. El servidor valida esa firma frente a una clave pública autorizada. Un host remoto comprometido no puede convertir el agente en un oráculo de firma genérico para documentos arbitrarios, pero sí puede solicitar las firmas SSH necesarias para intentar iniciar sesión en otros lugares. Esa es exactamente la capacidad que busca un atacante si tu identidad funciona en el control de código fuente, en hosts de producción o en sistemas internos de administración.

El fallo habitual tiene este aspecto:

1. Un desarrollador se conecta a `build.example.net` con `ssh -A` porque esa máquina debe llegar a un repositorio privado.
2. El agente local del desarrollador aparece en el host de compilación mediante `SSH_AUTH_SOCK`.
3. Un script de compilación, una dependencia comprometida u otro usuario con permisos suficientes pide a ese socket que firme para `git.example.net` o para un host interno.
4. El destino acepta la clave pública del desarrollador y registra la nueva conexión como si la hubiera iniciado el desarrollador.

El servidor SSH del destino ve una firma criptográfica legítima. No puede saber si una persona inició la conexión desde su portátil o si un proceso de la primera máquina remota la solicitó mediante el reenvío. Los registros del servidor identificarán la clave aceptada y la dirección de origen, pero no recuperarán esa distinción perdida.

## La herencia de la configuración crea exposiciones accidentales

La mayoría de los reenvíos inseguros empiezan en la configuración SSH, no en una decisión consciente tomada durante una sesión arriesgada. Una única sección antigua como `Host *` con `ForwardAgent yes` puede aplicarse a todos los hosts con los que se comunique un desarrollador, una herramienta de terminal o un agente de programación. También se aplica cuando la herramienta llama a `ssh` desde un script, sin que nadie vea una advertencia.

La configuración de OpenSSH tiene otro detalle delicado: para cada parámetro, el cliente suele usar el primer valor que encuentra. Una regla general colocada antes de una regla más específica puede anular la excepción que creías haber escrito. No confíes en una revisión visual rápida de `~/.ssh/config`; pregunta a SSH qué configuración utilizará.

Ejecuta esto en la máquina local antes de conectarte:

```sh
ssh -G deploy.internal.example | grep '^forwardagent '
```

Un resultado seguro es:

```text
forwardagent no
```

`ssh -G` muestra la configuración efectiva del cliente después de procesar sus archivos y las opciones de la línea de comandos. No abre una conexión. Por eso resulta útil en comprobaciones de configuración y en un script de prueba que lea una lista de hosts sensibles.

Establece una regla explícita de denegación predeterminada cerca del final del archivo de configuración correspondiente. Coloca las excepciones específicas antes, porque OpenSSH toma el primer valor coincidente:

```sshconfig
Host docs-bastion.example
    ForwardAgent yes

Host *
    ForwardAgent no
```

Este ejemplo todavía concede una excepción peligrosa, así que necesita una justificación y una persona responsable. La idea es que esa excepción sea visible y quede limitada a un host, en lugar de aplicarse silenciosamente a cada entorno nuevo.

Para una conexión puntual, fuerza la configuración segura aunque un archivo de configuración indique lo contrario:

```sh
ssh -o ForwardAgent=no developer@host.example
```

La forma abreviada `ssh -a developer@host.example` hace lo mismo. Usa cualquiera de las dos al conectarte a una máquina que no hayas revisado, un host temporal de soporte, un entorno de formación o un sistema gestionado por un proveedor.

No confundas `IdentitiesOnly yes` con un control del reenvío. Afecta a las identidades que ofrece el cliente SSH al autenticarse en un servidor. No impide que SSH cree un socket de agente en el lado remoto. Del mismo modo, eliminar `SSH_AUTH_SOCK` de un shell no es una política completa. Un proceso hijo puede heredar otro entorno y una configuración SSH puede volver a activar el reenvío en la siguiente conexión.

## Los flujos de trabajo con IA multiplican las rutas que hay que revisar

Un agente de programación con IA no necesita malas intenciones para volver peligroso el reenvío. Solo necesita permiso para ejecutar un comando que se encuentre con tus hábitos SSH existentes. Los agentes siguen instrucciones del repositorio, ejecutan scripts de compilación, usan hosts de desarrollo remoto y reintentan comandos con pequeñas variaciones. Son comportamientos normales. Se vuelven arriesgados cuando el entorno les proporciona una identidad de desarrollador reutilizable.

La ruta preocupante suele ser indirecta. Tu agente local inicia una sesión SSH en un equipo de desarrollo. Esa sesión reenvía el agente debido a una regla de configuración global. El agente de programación ejecuta un script del repositorio en el equipo de desarrollo. El script obtiene una dependencia o abre una segunda conexión SSH. La segunda conexión puede utilizar el socket que la primera sesión dejó allí.

Esto es peor que un humano escribiendo un único `git fetch`, porque un agente puede realizar muchas llamadas a herramientas sin detenerse a preguntarse por qué un shell remoto necesita acceso a un destino que no está relacionado. También puede encontrar instrucciones en un repositorio que le indiquen usar un alias de host concreto. Ese alias puede ocultar un `ProxyCommand`, una regla `Match` o una regla de reenvío heredada a la vista del operador.

Trata estos permisos por separado:

- Permiso para que un agente abra un shell remoto.
- Permiso para que ese shell remoto llegue a otro destino SSH.
- Permiso para que el segundo destino acepte la identidad del desarrollador.
- Permiso para que el agente provoque la segunda conexión.

Los equipos suelen agrupar los cuatro en «el agente necesita SSH». Esa frase no describe el límite real. Un shell remoto y una capacidad de firma reutilizable tienen consecuencias distintas y requieren decisiones separadas.

Comprueba cómo se inicia el proceso del agente. Si hereda `SSH_AUTH_SOCK` de un terminal interactivo, quizá ya tenga acceso a identidades del agente local para autenticarse directamente mediante SSH. Si después se conecta a un host con el reenvío activado, ese host obtiene una segunda vía hacia esas identidades. Un envoltorio que elimine la variable de entorno puede reducir el uso local accidental, pero no sustituye una configuración SSH segura ni una identidad separada para la automatización.

Revisa también las instrucciones del agente y los envoltorios de automatización en busca de `ssh -A`, `scp -A` o un alias que se expanda a ellos. `scp` y `sftp` dependen de la configuración del transporte SSH, así que un hábito de reenvío puede extenderse más allá del comando que lo creó. Coloca la decisión sobre el reenvío donde corresponde: en la definición de la conexión, con un valor predeterminado explícito de no.

## Un host de salto no necesita tu agente

Muchos desarrolladores activan el reenvío porque deben atravesar un bastión antes de llegar a un sistema interno. Era una solución habitual cuando el único modelo cómodo consistía en «iniciar sesión en el bastión y después volver a usar SSH». Sigue siendo popular porque funciona rápidamente durante la configuración. También convierte el bastión en una máquina que puede reutilizar tu identidad.

Usa `ProxyJump` cuando el primer host solo necesite transportar tráfico. El cliente local puede autenticarse en el host final mientras crea un túnel a través del host de salto, sin reenviar un socket del agente a ese host.

```sshconfig
Host engineering-bastion
    HostName bastion.example
    User developer
    ForwardAgent no

Host release-host
    HostName release.internal.example
    User deploy
    ProxyJump engineering-bastion
    ForwardAgent no
```

Con esta disposición, el cliente SSH local establece la conexión SSH final a través del bastión. El bastión transporta tráfico cifrado. No recibe un `SSH_AUTH_SOCK` remoto que pueda utilizar para pedir firmas a tu agente.

Prueba la ruta en lugar de asumir que la configuración significa lo que parece:

```sh
ssh -vvv release-host
```

En la salida de depuración, busca la conexión de proxy jump y verifica que no indique el reenvío del agente. Después de iniciar sesión en el host final, revisa el entorno remoto:

```sh
printf 'SSH_AUTH_SOCK=%s\n' "${SSH_AUTH_SOCK:-}"
ssh-add -l
```

Una variable de socket vacía es el resultado esperado cuando has desactivado intencionadamente el reenvío. Si `ssh-add -l` indica que no hay conexión con un agente de autenticación, también es coherente con la ausencia de un agente reenviado. No hagas estas comprobaciones únicamente en el host final cuando investigues un problema. Hazlas en cada salto interactivo en el que alguien pudiera haber activado el reenvío.

Hay casos legítimos en los que el propio host de salto debe autenticarse en otro host, por ejemplo, durante una operación de publicación controlada. Eso significa que el host de salto necesita su propia identidad de despliegue o una identidad de carga de trabajo de corta duración. No significa que deba tomar prestadas todas las identidades cargadas en el agente del portátil de un desarrollador.

## La automatización remota necesita su propia identidad

Un equipo de compilación remoto, un host de despliegue o una herramienta de IA debería autenticarse como la carga de trabajo que tiene asignada, no como el desarrollador que inició la sesión. Este cambio de diseño elimina la necesidad del reenvío en lugar de limitarse a hacerlo menos cómodo.

Una identidad de carga de trabajo debería conceder acceso únicamente a los servicios que esa carga de trabajo necesita. Para un repositorio, usa una identidad de despliegue limitada al repositorio cuando el servicio lo permita. Para un destino SSH, autoriza una clave pública exclusiva para una cuenta exclusiva y restringe los comandos o permisos de esa cuenta cuando el servidor admita esos límites. En una configuración SSH basada en certificados, emite certificados de corta duración con principales que correspondan al rol de la carga de trabajo.

Los certificados SSH pueden limitar el acceso, pero no son una solución mágica. El principal de un certificado controla qué cuentas lo aceptan solo si el servidor comprueba correctamente los principales. Un periodo de validez corto limita cuánto tiempo puede autenticar una firma, pero un proceso comprometido todavía puede usarla durante ese intervalo. Revisa la configuración del servidor que confía en la autoridad certificadora y las reglas de las cuentas que consumen esos principales.

No reutilices la clave pública personal de un desarrollador como identidad «temporal» para un trabajador de CI o un agente remoto. Eso mantiene ambiguo el rastro de auditoría. Cuando un registro de autenticación indica que inició sesión la clave personal, no puedes determinar fácilmente si actuó el desarrollador, un trabajo de compilación o un shell remoto comprometido mediante el reenvío.

Para acciones HTTP y SSH controladas por un agente, otra opción es mantener las credenciales en una pasarela de acciones local y devolver al agente únicamente el comando o el resultado de la API. Sallyport utiliza este modelo en macOS: su bóveda guarda la clave SSH y su ayuda `sp-ssh` ejecuta la acción SSH sin entregar la credencial al agente.

El límite útil es operativo, no retórico. El agente solicita una acción con nombre, la pasarela aplica la credencial y el agente recibe la salida. El agente no recibe un socket que pueda pasar a un proceso remoto no relacionado. Así, las aprobaciones y los registros tienen sentido porque corresponden a una acción, no a una capacidad abierta para solicitar firmas futuras.

## Las solicitudes de confirmación reducen la exposición, pero no solucionan la confianza

A veces una tarea breve de mantenimiento necesita realmente el reenvío y todavía no existe una identidad específica. En ese caso, reenvía la menor autoridad de firma posible y haz visible cada uso que quede. Es un control temporal, no una arquitectura permanente.

Inicia un socket de agente separado en lugar de reenviar el agente que contiene tu colección diaria de identidades. Añade únicamente la identidad necesaria para la tarea de mantenimiento, con una duración breve y la confirmación activada:

```sh
ssh-agent -a "$HOME/.ssh/maintenance-agent.sock" > "$HOME/.ssh/maintenance-agent.env"
. "$HOME/.ssh/maintenance-agent.env"
ssh-add -c -t 900 ~/.ssh/maintenance_ed25519
ssh -o ForwardAgent=yes operator@maintenance.example
```

`ssh-add -c` pide confirmación antes de que el agente firme. `-t 900` elimina la identidad después de 15 minutos. Consulta el manual `ssh-add(1)` de la versión de OpenSSH instalada, porque el comportamiento de la confirmación depende del agente local y de su interfaz de usuario.

Usa un shell separado para esta tarea. Cuando termines, elimina la identidad y detén ese agente:

```sh
ssh-add -D
ssh-agent -k
```

Esta secuencia impide solicitudes posteriores desde ese socket temporal. No revoca firmas ya emitidas, conexiones ya autenticadas ni copias de datos que un proceso remoto ya haya obtenido. Cierra la sesión remota y comprueba los destinos a los que podía acceder esa identidad.

OpenSSH también admite restricciones de destino mediante `ssh-add -h` en las versiones que incluyen la función. Estas restricciones pueden limitar para qué rutas de host firmará un agente, basándose en las claves de host de `known_hosts`. Vale la pena evaluarlas en un entorno controlado, pero añaden dependencias de configuración que los equipos suelen dejar de mantener. Un archivo `known_hosts` obsoleto o incompleto puede convertir una medida de seguridad en una interrupción justo en el peor momento. Pruébala con la ruta de salto exacta y los alias de host que usa tu automatización.

No dependas del cansancio provocado por las solicitudes como límite de seguridad. Un host comprometido puede pedir firmas repetidamente y nombrar destinos que se parezcan a la infraestructura normal. Si los operadores aprueban rápidamente para terminar un incidente, la confirmación protege menos de lo que creen. Una identidad limitada y una duración breve reducen el alcance de la solicitud.

## Los registros deben distinguir las sesiones de las acciones SSH

Los registros de autenticación SSH indican que una clave se autenticó en un servidor. Rara vez explican por qué se solicitó la firma o si el reenvío del agente creó la ruta. Si permites acciones remotas automatizadas, captura información suficiente para reconstruir quién inició el agente, qué proceso recibió la aprobación, qué destino contactó y qué acción solicitó.

Mantén separados los registros de sesión y los registros de acción. Un registro de sesión responde qué proceso del agente recibió acceso y cuándo terminó ese acceso. Un registro de acción responde qué comando SSH o llamada de API se ejecutó con ese acceso. Mezclarlos en un único registro genérico ralentiza las investigaciones porque el operador debe deducir una cadena de causa y efecto a partir de fragmentos.

En un servidor SSH, revisa sus registros normales de autenticación después de cualquier incidente de reenvío sospechoso. El origen exacto depende del sistema operativo y de la configuración del servicio, pero los lugares habituales incluyen las entradas del registro del sistema y el registro de autenticación del demonio SSH. Busca la huella de la clave pública aceptada, el nombre de la cuenta, la dirección de origen y la hora. Compáralos con el historial de sesiones del primer host remoto.

No afirmes tener certeza basándote en una dirección IP. Una conexión reenviada puede proceder de un host de compilación, un bastión, una pasarela de traducción de direcciones de red o una red superpuesta privada. El servidor puede establecer de dónde procede la conexión TCP, no quién inició la solicitud de firma en el equipo del desarrollador.

Los registros resistentes a la manipulación solo ayudan si el sistema los escribe fuera del control del agente. Un proceso capaz de editar su propio historial de acciones puede borrar el segundo salto sospechoso antes de que alguien lo revise. Sallyport registra las sesiones del agente y las llamadas individuales en un único registro de auditoría cifrado y encadenado mediante hashes, y `sp audit verify` puede comprobar esa cadena sin conexión y sin una clave de bóveda.

Elijas la herramienta que elijas, prueba su registro con una autorización fallida real y con un comando SSH correcto real. Confirma que identifica la ejecución del agente, el destino, la referencia o huella de la credencial, el resultado y la hora. Los registros que solo dicen «herramienta completada» no pueden responder a una pregunta de un incidente sobre la reutilización de una identidad.

## Trata una exposición como un abuso de autorización, no como un robo automático de la clave

Si descubres que has reenviado un agente a un host en el que no confías, responde como si ese host pudiera haber usado tu identidad mientras la sesión estaba activa. No esperes a tener pruebas de que extrajo una clave privada. El riesgo importante es la autenticación no autorizada, y la clave privada quizá nunca haya salido de tu máquina.

Primero detén los usos futuros. Cierra todas las sesiones SSH con ese host, elimina la identidad expuesta del agente y desactiva la regla de reenvío específica para el host. Si la identidad estaba cargada en un agente compartido, no ejecutes `ssh-add -D` de forma refleja durante un día de trabajo y declares la victoria. Podrías interrumpir sesiones legítimas y dejar autorizada la clave pública afectada en todos los servidores.

Después elimina o revoca la autorización en los destinos a los que puede llegar la identidad. En configuraciones normales de claves autorizadas, elimina la clave pública de las cuentas en las que ya no deba funcionar y sustitúyela por otra donde sea necesario. Para certificados SSH, revoca el certificado según el proceso de tu autoridad certificadora y del servidor, o deja que caduque un certificado de corta duración si puedes verificar que el periodo de exposición es aceptable. Para el acceso a repositorios, revoca o sustituye la credencial de despliegue o de usuario correspondiente mediante los controles habituales del servicio.

Investiga un intervalo de tiempo delimitado: desde que el reenvío estuvo disponible hasta que terminó la sesión remota o el agente local dejó de aceptar solicitudes. Revisa los registros de autenticación de los destinos, el historial del shell remoto cuando sea fiable, los registros de trabajos y los registros de acciones. Conserva los registros antes de limpiar el host remoto si sospechas que ha sido comprometido.

Por último, corrige la ruta que lo permitió. Si un `ForwardAgent yes` global provocó el incidente, cambiar solo la entrada del host comprometido deja expuesto al siguiente host desconocido. Si un flujo de trabajo de IA heredó un socket personal, dale una configuración de conexión explícita y una identidad de carga de trabajo exclusiva. La corrección debe hacer que la ruta insegura sea imposible de forma predeterminada, no limitarse a recordar a la gente que debe acordarse de una opción.

## El valor predeterminado seguro debe resistir el trabajo apresurado

El reenvío persiste porque elimina fricción en el momento. Un desarrollador necesita un salto más, una compilación necesita obtener un recurso privado o un agente debe completar una tarea antes de una fecha límite. Son necesidades reales. No justifican colocar una identidad de desarrollador con confianza amplia en cada máquina que se encuentre en el camino.

Establece `ForwardAgent no` de forma global. Usa `ProxyJump` para bastiones que solo transporten tráfico. Asigna a la automatización remota una identidad que indique qué puede hacer. Cuando no pueda evitarse una excepción temporal, aísla una identidad, exige confirmación, establece una caducidad breve y elimínala al terminar la tarea.

Ejecuta `ssh -G` contra los alias de host que tu equipo utiliza esta semana. Esta pequeña comprobación detecta el error silencioso de configuración antes de que un proceso remoto obtenga un servicio de firma que nunca debería haber recibido.
