# Acceso mediante bastion host para agentes de programación con IA, bien hecho

Un bastion host puede ofrecer a los agentes de programación con IA una ruta más segura hacia la infraestructura privada, pero solo si lo tratas como una parte del límite de acceso. Un jump box que solo aparece en un archivo de configuración SSH no es un control. El agente puede saltárselo, tomar prestada una credencial, abrir un túnel o dejarte un registro de auditoría que solo demuestra que algo se conectó al jump box.

Para el trabajo autónomo, el diseño útil es más estricto: el agente llega a un único punto de entrada controlado, ese punto de entrada solo llega a destinos conocidos, las credenciales permanecen fuera del contexto del agente y tus registros distinguen entre el host que el agente solicitó y el host que realmente aceptó la conexión. Esa distinción puede parecer minuciosa hasta la primera revisión de un incidente. Entonces marca la diferencia entre una evidencia y una historia tranquilizadora.

## Un bastion cambia la ruta, no la autoridad del agente

Un diseño de acceso con bastion host limita la accesibilidad de red, pero no reduce lo que un agente puede hacer después de llegar a una cuenta privilegiada. Los equipos suelen confundir ambas funciones porque las dos implican SSH. Son controles independientes y deben fallar de forma independiente.

Un jump host ayuda cuando los destinos privados solo aceptan tráfico de administración desde ese host o esa subred. Ofrece un lugar único para aplicar límites de salida, recopilar evidencias de conexión y cortar el acceso durante una investigación. También evita que cada estación de trabajo, runner de compilación y sandbox de agente necesite rutas directas hacia redes sensibles.

No responde a cuatro preguntas que importan aún más con un cliente autónomo:

- ¿Puede el agente llegar directamente al destino por otra ruta?
- ¿La cuenta de destino tiene permisos más amplios de los que necesita el trabajo solicitado?
- ¿Puede el agente crear un túnel que le proporcione una ruta no revisada?
- ¿Puedes identificar después la máquina final y el comando remoto?

Si la respuesta a la primera pregunta es sí, ProxyJump es una opción de comodidad, no una medida de cumplimiento. Si la respuesta a la segunda también es sí, el bastion solo ha desplazado el punto donde una credencial con demasiados privilegios entra en la red.

He visto equipos construir un jump box cuidadosamente mantenido y después permitir una salida SSH amplia desde una subred de CI «para solucionar problemas». Un agente que se ejecute en esa subred no necesita ningún exploit ni una maniobra ingeniosa. Puede llamar directamente a la dirección final. La arquitectura falló porque la red permitía saltarse la ruta, no porque SSH se comportara mal.

Mantén las funciones claras. El entorno del agente solo debería conectarse al host de entrada. El host de entrada solo debería conectarse a las redes y puertos de destino previstos. El host final debería autorizar una cuenta con una finalidad limitada. Cada capa debería producir registros que respondan a una pregunta diferente.

## ProxyJump no expone el comando final

Un jump host SSH normalmente no puede inspeccionar el comando que el agente ejecuta en el host final. Este es el punto que los equipos pasan por alto cuando prometen «registros completos de comandos desde el bastion».

OpenSSH documenta `ProxyJump` en `ssh_config(5)` como una conexión inicial al jump host seguida del establecimiento de un reenvío hacia el destino definitivo. En el uso habitual de OpenSSH, el cliente pide en la práctica al jump host que realice una conexión TCP con el destino y después ejecuta su sesión SSH de extremo a extremo a través de ese flujo de bytes. El transporte SSH final sigue cifrado entre el cliente y el destino.

El bastion puede saber que se conectó a `10.42.8.19:22`. También puede registrar la cuenta utilizada en el bastion, el origen, los tiempos y el destino de una solicitud de reenvío. Pero no sabe automáticamente si la sesión cifrada ejecutó después `systemctl restart api`, `cat /etc/shadow` o un shell interactivo.

Esto deja tres observaciones diferentes que la gente suele mezclar:

1. El **destino solicitado** es lo que proporcionó el agente, como `prod-api-01`.
2. El **destino de conexión** es la dirección y el puerto que abrió realmente el proxy.
3. El **destino de ejecución** es la identidad del servidor y la cuenta que aceptaron la autenticación SSH final.

Pueden ser diferentes. Un alias puede resolverse de otra forma después de un cambio de DNS. Una entrada conocida comprometida u obsoleta puede hacer sospechosa una conexión. Una regla del proxy puede enviar un alias amigable a una dirección inesperada. Registrar solo `prod-api-01` no demuestra qué servidor ejecutó un comando.

La misma separación se aplica a los comandos. Una puerta de enlace del lado del cliente puede registrar el comando solicitado antes del transporte. Un wrapper en el host final puede registrar el comando que OpenSSH pasa para una solicitud exec. El bastion puede registrar el destino TCP. Correlaciona esas observaciones mediante un identificador de sesión y una ventana temporal. No finjas que una sola capa puede verlas todas.

Si necesitas un único lugar que pueda inspeccionar y autorizar cada comando remoto, el SSH normal de extremo a extremo mediante un jump box es el mecanismo equivocado. Usa un servicio específico de ejecución remota, un endpoint de comandos restringido o un wrapper en el destino que acepte únicamente la operación que quieres admitir. Esa elección cuesta flexibilidad, pero evita que afirmes tener una visibilidad que no existe.

## Las reglas de red deben hacer inevitable la ruta aprobada

Un punto de entrada controlado solo funciona cuando la red que lo rodea hace que las rutas alternativas no estén disponibles. Empieza por la conectividad antes de debatir sobre diálogos de aprobación o formatos de registro.

El runtime del agente solo necesita SSH saliente hacia la dirección y el puerto del bastion. No debería tener una ruta amplia hacia subredes de producción, interfaces de administración públicas ni un segundo jump host que nadie consideró parte del diseño. El DNS también importa. Si el agente puede resolver y conectarse a direcciones de destino sin pasar por el alias, una convención de nombres amigables no te protege.

El bastion necesita una lista corta de salidas permitidas. Si administra hosts en dos rangos privados mediante el puerto TCP 22, permite esos rangos y ese puerto. No le des acceso saliente arbitrario para después llamar bastion al host. Un host de entrada que puede llegar a todos los servicios, bases de datos y endpoints públicos se convierte en un relay general cuando se abusa de la cuenta del agente.

Los destinos finales solo deberían aceptar administración SSH desde la ruta del bastion. Los grupos de seguridad de la nube, los firewalls del host y las ACL de red pueden ayudar. Usa más de uno solo si puedes mantener clara la responsabilidad de cada control. Una regla que nadie mantiene no es defensa.

Prueba la ruta con casos de denegación, no solo con el comando correcto. Ejecuta estas comprobaciones desde el mismo usuario, contenedor o VM que ejecuta el agente:

```sh
nc -vz bastion.internal.example 22
nc -vz 10.42.8.19 22
ssh -o ConnectTimeout=5 prod-api-01 true
```

La primera conexión debería funcionar. La conexión directa al host final debería fallar. El comando SSH debería funcionar solo porque su ruta configurada usa el bastion. Si `nc` no está disponible, utiliza el comando habitual de prueba TCP de tu entorno. La idea es probar la ruta de red sin permitir que la configuración SSH oculte una ruta directa.

Repite la prueba después de cambios en el firewall, nuevas subredes y cambios en el runner del agente. Es una prueba pequeña con un beneficio excepcionalmente alto. Detecta el caso habitual en el que la configuración SSH parece correcta mientras la red sigue permitiendo una conexión directa.

## El agente necesita una identidad que puedas revocar

No concedas a un agente una clave de administrador compartida esperando que el bastion haga que sea aceptable. Una clave privada copiada es una decisión de autorización con consecuencias muy duraderas.

Asigna a cada proceso o ejecución del agente una identidad que puedas atribuir y revocar. El mecanismo puede ser un certificado SSH de corta duración, una clave efímera registrada para un trabajo limitado o una credencial guardada por una puerta de enlace de acciones que ejecute SSH en nombre del agente. La opción adecuada depende de tu entorno, pero el agente no debería imprimir un secreto reutilizable en su transcript, espacio de trabajo, historial del shell o salida de la herramienta.

Los certificados SSH ayudan cuando ya operas una autoridad certificadora SSH. Un certificado puede incluir un periodo de validez corto, un principal para la cuenta de destino y opciones críticas como restricciones de dirección de origen. El destino puede confiar en la CA en lugar de mantener una lista cada vez mayor de claves públicas individuales. Así resulta más fácil revisar la emisión y la caducidad.

Los certificados no hacen que una cuenta amplia sea limitada. Un certificado para `root` sigue siendo una credencial de root hasta que caduca. Tampoco demuestran la intención de un comando. Trátalos como un mecanismo de emisión y duración.

Una puerta de enlace cambia más claramente el patrón de exposición. Sallyport puede ejecutar SSH mediante su helper `sp-ssh` mientras la clave SSH permanece en su bóveda cifrada en lugar de llegar al proceso del agente. Eso protege la clave privada frente al transcript del agente, pero los equipos deben comprobar cómo se registran la ruta prevista del salto, la identidad del destino y las evidencias del comando.

Vincula la aprobación a un proceso identificable, no a la frase que el agente escribió en una ventana de chat. Un proceso puede estar firmado, iniciarse en un momento concreto, asociarse a un usuario local y revocarse como sesión activa. Una explicación en lenguaje natural aporta contexto, pero no es un límite de control de acceso.

Evita que todos los trabajos automatizados compartan una sola credencial. Cuando un registro del lado del destino dice `deploy`, necesitas un segundo registro que identifique la ejecución del agente que la invocó. Si todas las ejecuciones usan la misma clave y la misma cuenta, la revisión de un incidente se convierte en arqueología.

## Registra una solicitud de ejecución, un hecho de ruta y un resultado

Un registro de auditoría SSH del agente debe indicar exactamente qué significa cada campo. Así evitas que el sistema de auditoría haga afirmaciones que no puede respaldar.

En el momento de solicitar la acción, registra la identidad de la ejecución del agente, el alias de host solicitado, la cuenta remota solicitada, el texto del comando o la operación estructurada y si el llamador pidió un TTY o reenvío. Esta es la solicitud de ejecución. Te dice qué intentó hacer el llamador.

En el límite de la ruta, registra la identidad del host de entrada, la dirección de destino resuelta, el puerto de destino y el resultado de la conexión. Este es el hecho de ruta. Si el proxy abre una conexión, puede decir dónde la abrió. No puede inferir de forma veraz el comando final a partir de bytes cifrados.

En el host final, registra la identidad estable del host de destino, la cuenta autenticada, la huella de autenticación o el número de serie del certificado, el comando si el servidor recibió una solicitud exec, el estado de salida y los registros locales relevantes del servicio. Esta es la evidencia de ejecución. El host final es el único endpoint SSH normal que puede ver el comando remoto sin romper el diseño SSH de extremo a extremo.

Usa una estructura de eventos que haga visible la falta de evidencia:

```json
{
  "run_id": "run_7c31",
  "requested": {
    "host": "prod-api-01",
    "user": "deploy",
    "command": "sudo systemctl restart api",
    "tty": false
  },
  "route": {
    "bastion": "bastion.internal.example",
    "destination_ip": "10.42.8.19",
    "destination_port": 22
  },
  "final_host": {
    "host_fingerprint": "SHA256:example",
    "authenticated_user": "deploy",
    "command_observed": true,
    "exit_status": 0
  }
}
```

El ejemplo separa deliberadamente `requested.command` de una observación en el host final. Si la sesión fue interactiva, establece `command_observed` en false y explica el motivo. Un campo vacío sin explicación invita a suponer que la herramienta lo capturó.

Para la ejecución normal de comandos, `sshd_config(5)` documenta `ForceCommand`, que fuerza un comando para una cuenta o un bloque match determinados. Un wrapper en el lado del destino puede leer `SSH_ORIGINAL_COMMAND` en una solicitud exec, validar una operación permitida y escribir un registro de auditoría antes de invocar el programa aprobado. Necesita un tratamiento cuidadoso del shell. No pases una cadena de comandos no confiable a `eval` ni supongas que `SSH_ORIGINAL_COMMAND` existe para un shell interactivo.

El encadenamiento de hashes y el almacenamiento de solo anexado ayudan a detectar alteraciones posteriores de los eventos de auditoría. No corrigen un contenido de eventos débil. Un registro perfectamente conservado que solo dice «SSH conectado» sigue siendo una evidencia débil.

## Haz que la configuración SSH se pueda inspeccionar

Conserva los alias de host para las personas, pero inspecciona lo que OpenSSH hará realmente antes de entregar una configuración a un agente. `ssh -G` muestra la configuración efectiva del cliente después de que OpenSSH procese las entradas `Host` coincidentes.

Este ejemplo dirige un destino con nombre mediante un bastion también identificado por nombre y desactiva opciones que suelen sorprender a los equipos:

```sshconfig
Host agent-bastion
    HostName bastion.internal.example
    User agent-gateway
    IdentityFile ~/.ssh/agent_gateway
    IdentitiesOnly yes

Host prod-api-01
    HostName 10.42.8.19
    User deploy
    ProxyJump agent-bastion
    StrictHostKeyChecking yes
    UserKnownHostsFile ~/.ssh/agent_known_hosts
    ForwardAgent no
    RequestTTY no
```

Después, comprueba la configuración resultante:

```sh
ssh -G prod-api-01 | grep -E '^(hostname|user|proxyjump|forwardagent|requesttty) '
```

La salida debería tener esta forma:

```text
user deploy
hostname 10.42.8.19
requesttty no
forwardagent no
proxyjump agent-bastion
```

Esto demuestra cómo planea comportarse el cliente. No demuestra que la red bloquee el SSH directo, que el destino presente la clave de host esperada ni que el bastion restrinja sus salidas. Prueba esos aspectos por separado.

No permitas que el agente proporcione libremente flags arbitrarios de `ssh`. Un agente puede anular la configuración con opciones de línea de comandos, especificar una dirección IP directa, reemplazar `ProxyJump` o añadir opciones de reenvío si la capa de ejecución simplemente pasa una cadena de shell. Cuando sea posible, coloca el destino, la cuenta y las opciones SSH permitidas en entradas de acción estructuradas. Si aceptas un comando sin estructurar, trátalo como código y desconfía de él como lo harías con un script de shell procedente de una fuente no confiable.

La verificación de claves de host merece atención porque los agentes reintentan automáticamente y a menudo tratan un error como un obstáculo para la tarea. Precarga las huellas de host confiables mediante un proceso controlado. Mantén activada la comprobación estricta. No enseñes al agente a eliminar entradas de hosts conocidos ni a aceptar claves modificadas para desbloquear un despliegue. Un cambio de clave de host requiere que un operador verifique por qué ha cambiado.

## El reenvío y los shells crean rutas que rodean tu revisión

Desactiva el reenvío y los shells interactivos para los agentes autónomos, salvo que una tarea concreta los necesite. Ambas funciones convierten un comando remoto limitado en un canal de acceso más amplio.

El reenvío local permite que un cliente abra un puerto local que llega a un servicio interno mediante SSH. El reenvío remoto permite que un endpoint remoto exponga una ruta de vuelta hacia el lado del cliente. El reenvío dinámico crea un proxy SOCKS. El reenvío del agente hace que las credenciales estén disponibles en otro host. Cada función puede ser legítima para un administrador humano. Cada una también debilita la afirmación de que el bastion es la única ruta controlada.

OpenSSH proporciona restricciones del servidor que deben aplicarse en la cuenta del destino final o del bastion, no solo en la configuración del cliente del agente. `sshd_config(5)` documenta `DisableForwarding`, y `authorized_keys` admite opciones como `no-port-forwarding`, `no-agent-forwarding`, `no-X11-forwarding` y `no-pty`. Usa los controles que admita tu versión de OpenSSH y después verifícalos con un intento de conexión real.

Una cuenta restringida podría usar una entrada de clave autorizada con esta forma:

```text
no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... agent-run
```

Esa línea limita las funciones para esa clave pública. No restringe los comandos que puede ejecutar la cuenta. Combínala con una cuenta limitada, un wrapper `ForceCommand` o una interfaz específica del servicio si importa el alcance de los comandos.

La regla para los shells interactivos debe ser clara: no se los des por defecto a un agente autónomo. Un shell acepta secuencias de comandos, pipes, redirecciones, procesos en segundo plano y credenciales copiadas. El campo de comando de la solicitud de acción inicial no describirá la sesión completa. Un wrapper forzado puede bloquear un shell, pero debes probarlo porque su comportamiento cambia según la configuración de la cuenta y las solicitudes de subsistema SSH.

Algunos trabajos de mantenimiento necesitan realmente un túnel o un shell. Trátalos como rutas de excepción explícitas, con aprobación de un operador identificado y una caducidad corta. Ocultar una excepción dentro de una credencial general del agente es la forma de convertir un acceso temporal en permanente.

## Las restricciones del destino son mejores que los filtros de cadenas de comandos

Una lista sencilla de prefijos de comandos de shell es fácil de esquivar y difícil de mantener. Es popular porque parece precisa: permitir `systemctl restart api` y rechazar todo lo demás. La gramática del shell hace que esa confianza sea frágil.

Considera un filtro que acepta texto que comienza por `systemctl restart api`. Un llamador puede añadir un operador de shell, usar otra ruta al ejecutable, activar un comportamiento mediante una variable de entorno o aprovechar un wrapper que analice la cadena de forma incoherente. Incluso un parser cuidadoso no puede conocer todos los efectos de un comando que invoca otro programa.

Usa operaciones estructuradas cuando el trabajo tenga una forma estable. Un runner de despliegues puede aceptar el nombre de una aplicación y un identificador de versión. Un endpoint de mantenimiento puede aceptar el nombre de un servicio de una lista fija. Un wrapper restringido puede asignar un nombre de operación pequeño a un array de argumentos fijo. En todos los casos, evita el shell por completo.

Cuando tengas que admitir comandos libres, reconoce lo que estás permitiendo: autoridad de shell remoto para esa cuenta. Regístrala, limita en el tiempo la credencial, restringe el entorno de destino y exige un nivel de aprobación separado. No presentes un filtro de cadenas como autorización de comandos.

La cuenta de destino también debería tener un límite de permisos del sistema operativo. Una cuenta `deploy` que puede ejecutar `sudo` sin restricciones es, en la práctica, una cuenta de administrador con pasos adicionales. Dale solo las acciones de servicio, archivos y directorios que necesita la automatización. Revisa sus reglas de sudo como código, incluidas todas las rutas de comandos y los patrones de argumentos.

La prueba práctica consiste en comprobar si un agente encargado de reiniciar un servicio puede leer secretos de producción, crear un túnel inverso, cambiar la autorización SSH o modificar el colector de auditoría. Si puede hacerlo, el alcance de la cuenta es incorrecto aunque todas las conexiones hayan utilizado el bastion adecuado.

## La respuesta a incidentes comienza con detener la ruta activa

Cuando un agente se comporta de forma sospechosa, revoca su acceso activo antes de empezar a reconstruir sus intenciones. Desactiva su sesión activa en el límite de acciones si dispones de uno, bloquea el origen en el bastion e impide que la credencial asociada vuelva a autenticarse. Conserva los registros antes de que alguien rote los archivos o reinicie el host que contiene la evidencia.

Después, construye una línea temporal a partir de los registros separados. Empieza por la acción solicitada por el agente. Relaciónala con el destino y la hora de la conexión en el bastion. Relaciónala después con la autenticación del host final y la evidencia del comando. Comprueba si apareció un TTY, una solicitud de reenvío o un destino alternativo. Una discrepancia no es automáticamente maliciosa, pero te indica qué control debes investigar.

No respondas concediendo de forma permanente más acceso «para que el agente pueda reparar el problema». Es una reacción de pánico habitual. Si el agente ya se ha comportado fuera de lo previsto, ampliar el alcance de su cuenta o abrir una ruta directa elimina la evidencia que necesitas y crea otro incidente.

Haz una pequeña simulación antes de necesitarla en producción. Revoca un agente de prueba durante un comando SSH inofensivo, confirma que las nuevas solicitudes fallan y verifica que tus registros muestran el host solicitado, la ruta real, la identidad del destino y el resultado de salida. Si para entender qué ocurrió necesitas un chat de grupo, tu diseño de acceso todavía es demasiado impreciso.

Un bastion se justifica cuando impone una ruta, permite revocar rápidamente el acceso y aporta un dato preciso al registro de auditoría. Configura primero la ruta, mantén las credenciales fuera del agente y recopila las evidencias en el host final, donde los comandos finales son visibles. Todo lo demás es una comodidad de SSH con un nombre que suena a seguridad.
