# ¿La seguridad de SSH ProxyCommand es más débil de lo que parece?

SSH ProxyCommand es ejecución de código local que ocurre antes del comando remoto que pretendías ejecutar. Parece obvio al decirlo, pero en las revisiones se suele tratar como un detalle de transporte: el agente pide `ssh host uptime`, quien revisa comprueba el host y el comando remoto, y el cliente inicia silenciosamente un comando de shell local que nadie incluyó en la decisión de aprobación.

Esa brecha importa más cuando un agente autónomo de programación puede invocar SSH. El destino remoto puede estar muy restringido, mientras que la configuración SSH local contiene alias, inclusiones, reglas condicionales, un ayudante proxy y una cadena de hosts de salto acumulados durante años. Una revisión segura empieza por la acción del lado del cliente que realiza SSH y luego avanza hacia la red.

## ProxyCommand se ejecuta antes de que exista la sesión remota

`ProxyCommand` indica al cliente SSH que inicie un comando local y use su entrada y salida estándar como transporte hacia el servidor SSH. El comando no se ejecuta en el destino. Se ejecuta en la máquina que inició `ssh`, con esa cuenta local, antes de que SSH pueda autenticar el servidor previsto.

Una entrada típica parece inocua:

```
Host build-private
    HostName build.internal.example
    ProxyCommand /usr/local/bin/connect-private %h %p
```

Cuando alguien ejecuta `ssh build-private`, SSH expande `%h` y `%p`, y después inicia `/usr/local/bin/connect-private build.internal.example 22`. El ayudante puede abrir un socket, llamar a otro cliente, leer un archivo de token, cargar una biblioteca, consultar variables de entorno o ejecutar un script de shell. A partir de ese momento, el cliente SSH solo ve un flujo de bytes. Tu solicitud de aprobación puede describir una conexión SSH, mientras que la primera acción importante fue iniciar un programa local.

El manual de `ssh_config` de OpenSSH es claro sobre el límite: `ProxyCommand` especifica el comando que se usa para conectar con el servidor y advierte que el comando se ejecuta mediante el shell del usuario. Esta última parte cambia la revisión. Una línea de comandos no es un array de argumentos. El análisis del shell, las expansiones, las redirecciones, las sustituciones de comandos y el entorno de inicio del shell forman parte de la acción.

No restes importancia a esto porque el ayudante viva en un repositorio de dotfiles. Una configuración revisada hace seis meses puede invocar un binario encontrado mediante `PATH`, cargar un archivo de un directorio con permisos de escritura o depender de un alias que ahora se resuelve de otra forma. El objetivo de la revisión es la ruta de ejecución resuelta en la máquina que la ejecutará.

## Un alias de host puede seleccionar mucho más que un host

La configuración de SSH es un sistema de coincidencias, no un diccionario sencillo. Un alias corto puede activar ajustes de la configuración del sistema, la del usuario, fragmentos incluidos, hosts comodín, canonicalización y bloques condicionales. El comando final puede proceder de un archivo que nadie relaciona con el alias.

Empieza enumerando las configuraciones que lee SSH. En un cliente OpenSSH habitual, se incluyen `/etc/ssh/ssh_config`, los archivos a los que se llega mediante `Include` y `~/.ssh/config`. El empaquetado de la distribución y las opciones de línea de comandos pueden cambiar la lista, así que inspecciona también la invocación. Quien proporciona `-F /some/file` ha cambiado toda la superficie de revisión.

Usa `ssh -G` para imprimir la configuración resuelta de un destino concreto. Para un alias de prueba controlado, este es un artefacto útil:

```
ssh -G build-private | grep -E '^(hostname|user|port|proxycommand|proxyjump|identityfile) '
```

La salida tiene este aspecto:

```
hostname build.internal.example
user deploy
port 22
proxycommand /usr/local/bin/connect-private %h %p
identityfile ~/.ssh/id_deploy
```

`ssh -G` muestra lo que seleccionó SSH y detecta un número sorprendente de errores: una entrada amplia de `Host *`, una inclusión antigua o un alias que se asigna a una cuenta distinta de la esperada. No demuestra que el ayudante sea seguro. Tampoco siempre explica todas las condiciones que llevaron a ese resultado de una manera comprensible para quien revisa, así que sigue cada directiva relevante hasta su archivo de origen.

Trata la entrada de host como datos solo si la configuración la mantiene como datos. Si un agente puede enviar alias arbitrarios, puede elegir cualquier bloque `Host` coincidente que la cuenta local pueda leer. Si puede enviar opciones `-o` arbitrarias o una ruta de configuración, a menudo puede eludir por completo la revisión del alias. Un contenedor que acepta un comando SSH de formato libre no es un límite real.

Una interfaz mejor acepta un identificador de destino de una lista reducida de permitidos y lo asigna a una cuenta, un nombre de host, un puerto y un método de conexión fijos. El identificador puede ser `production-readonly`; los argumentos SSH subyacentes no deberían proceder de una cadena generada por un agente.

## La expansión del shell convierte la configuración en un límite de entrada

Como SSH pasa `ProxyCommand` por un shell, las comillas en la configuración no ofrecen la misma seguridad que los argumentos estructurados de un proceso. La sintaxis del shell permanece hasta que este la procesa. Incluye espacios, caracteres comodín, referencias a variables, redirecciones y sustituciones de comandos cuando aparecen en el texto del comando.

El patrón arriesgado es un ayudante que construye su propio comando de shell con los valores que recibe. Considera esta configuración:

```
Host *
    ProxyCommand sh -c 'relay --target %h --port %p --token \"$RELAY_TOKEN\"'
```

Aquí hay varias preguntas distintas. ¿Todo nombre de host resuelto posible contiene solo la sintaxis esperada para un nombre de host? ¿`relay` interpreta `--target` como un único valor? ¿Puede un entorno controlado por el usuario establecer `RELAY_TOKEN` o alterar la ruta de búsqueda? ¿El shell exterior recibe una cadena de comandos con caracteres que cambian de significado antes de que se inicie `relay`? Decir que el nombre de host procede de SSH no las responde.

Los tokens con porcentaje no son un sistema de plantillas seguro. OpenSSH expande tokens como `%h`, `%p`, `%r` y `%n` en muchas opciones. `%n` es el argumento de host original, mientras que `%h` es el nombre de host que SSH usará después de procesar la configuración. Es fácil pasar por alto esta diferencia. Un comando proxy que usa `%n` puede recibir un alias fácil de recordar o texto solicitado arbitrario, cuando quien lo creó esperaba un nombre DNS canónico. Un comando que usa `%h` aún puede recibir un valor modificado por `HostName` o la canonicalización.

La solución suele ser menos ingeniosa que la configuración original. Usa un ayudante dedicado pequeño con una ruta de ejecutable fija, dale una sintaxis restringida y haz que rechace cualquier cosa fuera de ella. Haz que el ayudante llame a API de conexión o a un iniciador de procesos con array de argumentos en vez de montar otra línea de shell. Si un script de shell es inevitable, valida las entradas antes de interpolarlas, pon entre comillas cada expansión para ese shell y mantén el conjunto aceptado lo bastante limitado para que una persona auditora pueda probarlo.

No sustituyas el análisis sintáctico por `eval`. Los equipos recurren a él porque hace que un contenedor guiado por configuración parezca flexible. También convierte una sola comilla olvidada en un defecto de ejecución local. La flexibilidad debe vivir en un formato de configuración revisado, no en un shell que vuelve a interpretar texto.

## ProxyJump elimina un shell, no el problema de confianza

`ProxyJump`, escrito a menudo como `-J` o `ProxyJump`, indica a SSH que alcance el destino a través de uno o varios hosts de salto SSH. Para el enrutamiento habitual mediante bastión, úsalo en lugar de un `ProxyCommand` escrito a mano que solo inicia otro comando `ssh -W`. Describe la topología prevista y evita incluir esa topología en un comando de shell personalizado.

Por ejemplo:

```
Host build-private
    HostName build.internal.example
    User deploy
    ProxyJump bastion-admin

Host bastion-admin
    HostName bastion.example
    User relay
```

Esto es más fácil de inspeccionar que una cadena con comillas anidadas. Aun así necesita una revisión cuidadosa. El cliente se autentica en el host de salto, el host de salto participa en la ruta, y su nombre, cuenta, clave de host, requisitos de MFA, reglas de reenvío y conectividad de red afectan a la acción. Un host de salto comprometido o mal configurado puede exponer patrones de tráfico y redirigir un intento de conexión. La verificación de claves de host sigue siendo obligatoria en cada salto SSH.

OpenSSH permite ambos ajustes, pero `ProxyCommand` tiene prioridad sobre `ProxyJump` cuando se aplican los dos. Es una sorpresa de configuración desagradable. Quien revisa puede ver un `ProxyJump` limpio en un bloque específico de host, mientras una regla coincidente amplia anterior o posterior proporciona un comando proxy. Verifica el resultado efectivo con `ssh -G` en vez de deducirlo de un único bloque.

Varios hosts de salto también necesitan un motivo explícito. No trates una cadena como seguridad adicional de forma predeterminada. Cada host añadido implica otro uso de credenciales, otra decisión sobre la clave de host y otro lugar que auditar. Usa una cadena solo cuando la ruta de red lo exija y registra qué cuenta se espera en cada salto.

## Include y Match exec pueden ejecutar lógica local durante la selección

`Include` hace que la configuración SSH sea modular, algo útil hasta que un repositorio, una herramienta de gestión de configuración o un instalador deja un fragmento en un directorio de coincidencia amplia. OpenSSH expande patrones comodín en `Include`; por tanto, un archivo inesperado puede cambiar los ajustes de un host sin tocar la configuración principal. Revisa la propiedad y los permisos de escritura de los directorios incluidos, no solo su contenido actual.

`Match` añade condiciones. Las condiciones `Match host`, `user`, `localnetwork` y otras cambian qué opciones se aplican. `Match exec` merece una advertencia más visible: SSH ejecuta el comando local indicado y usa su estado de salida para decidir si el bloque coincide. Leer la configuración puede, por tanto, invocar un ejecutable local.

Este ejemplo hace visible el límite:

```
Match exec \"/usr/local/bin/on-corporate-network\"
    ProxyJump corp-bastion
```

La condición puede ser razonable si el ayudante es un programa fijo, propiedad de root y con un resultado sencillo. Se vuelve frágil cuando invoca un shell, lee un archivo de proyecto con permisos de escritura, contacta con un servicio de red o depende de `PATH`. No pruebes reglas desconocidas de `Match exec` en una estación de trabajo llena de credenciales activas solo porque pensabas ejecutar `ssh -G`; inspecciona primero el comando y usa una cuenta o una máquina aislada cuando sea necesario.

El manual de OpenSSH también documenta la expansión de tokens en opciones de coincidencia y relacionadas con comandos. Eso significa que una condición `Match exec` puede depender del texto solicitado de host o usuario. Mantén los valores elegidos por el agente fuera de esas condiciones. Si la condición necesita datos contextuales, derívalos de un archivo de estado local de confianza con propiedad estricta, no de una cadena que cruzó el límite de un agente.

## El incidente suele empezar con un ayudante de conveniencia

Un fallo realista no necesita una configuración que parezca maliciosa. Imagina que un desarrollador añadió esto hace años para llegar a una red de pruebas privada:

```
Host test-*
    ProxyCommand connect-testnet %h %p
```

`connect-testnet` se instaló en su día mediante un script de arranque en un directorio bin personal. El script llama a un cliente VPN de la empresa y luego inicia un comando de relé encontrado en `PATH`. Más tarde, un agente de compilación se ejecuta con la cuenta del desarrollador. Puede pedir `ssh test-cache`. La acción esperada es un comando de solo lectura en una máquina de pruebas.

Primero, SSH encuentra la coincidencia `test-*`. Segundo, el shell local inicia `connect-testnet`. Tercero, el script inicia el cliente VPN y el ejecutable que en ese momento gana la búsqueda en `PATH`. Solo después de esos pasos locales SSH comienza su protocolo con el host indicado. Una auditoría que registra únicamente el comando remoto pierde el comportamiento que cambió el estado de la máquina local y eligió la ruta de red.

El defecto no consiste en que los scripts de shell estén prohibidos. Consiste en tratar una cadena de conveniencia local como si fuera parte del servicio remoto. La capacidad del agente de elegir `test-cache` seleccionó código local. Un agente malicioso podría buscar alias que coincidan con patrones amplios, pero un agente normal puede activar el mismo problema al adivinar un nombre que resulta existir.

Corrige la ruta real. Sustituye el comodín por destinos con nombre siempre que sea práctico. Sitúa el ayudante de relé en una ruta absoluta. Elimina las búsquedas mediante `PATH` dentro de él. Haz que el ayudante acepte únicamente los valores de host y puerto esperados. Saca la gestión de la conexión VPN de un comando proxy por solicitud si no necesita ejecutarse en cada acción SSH. Después, prueba la configuración final con la cuenta y el entorno que usará el agente.

## Aprueba el método de conexión, no solo el destino

Un flujo de aprobación debe mostrar información suficiente para que una persona reconozca la acción. `ssh deploy@build-private` ofrece un contexto incompleto cuando el alias inicia un ayudante proxy o se enruta mediante un bastión. Quien revisa necesita el destino resuelto, la cuenta, el puerto, el método proxy y la ruta de salto. De otro modo aprueba una etiqueta y confía en que la configuración local conserve el significado de la semana pasada.

Separa tres decisiones que los equipos suelen mezclar. Primera, ¿puede este proceso de agente realizar alguna acción SSH? Segunda, ¿puede usar esta identidad SSH específica? Tercera, ¿puede este destino usar este método de conexión local concreto? Un sí a las dos primeras no responde automáticamente a la tercera. El ayudante proxy puede alcanzar una zona de confianza diferente o provocar efectos locales que la conexión directa no provocaría.

Sallyport mantiene las credenciales SSH fuera del agente y usa su ayudante `sp-ssh` incluido para las acciones SSH, pero eso no convierte una configuración SSH local en una superficie de políticas segura. Mantén reducida la interfaz de destinos del agente e inspecciona toda configuración de cliente o proceso auxiliar que participe antes de que empiece la acción con credenciales.

Para rutas sensibles, exige aprobación en cada uso de la identidad SSH e incluye el destino y la ruta en la descripción de aprobación. Así se detectan un alias modificado, un bastión inesperado o un intento de uso fuera de la ruta habitual de mantenimiento. No hará útil una tarjeta de aprobación imprecisa. La información tiene que estar ahí.

## Prueba la ruta resuelta con credenciales desechables

Una revisión de configuración necesita una prueba de ejecución, pero hazla con credenciales que no puedan dañar producción. Usa un host de prueba, una cuenta temporal y un entorno controlado. Captura el árbol de procesos del lado del cliente y los destinos de red si tu sistema operativo lo permite. El objetivo es confirmar qué ejecutable se inicia, qué argumentos recibe y si crea conexiones fuera de la ruta esperada.

Una secuencia de revisión breve basta para la mayoría de los alias de host:

1. Ejecuta `ssh -G alias` y guarda los ajustes resueltos de `hostname`, `user`, `port`, `proxycommand`, `proxyjump` e identidad.
2. Sigue cada `Include` y cada bloque `Host` o `Match` coincidente que proporcionó esos valores.
3. Lee cada ejecutable local de la ruta proxy o de coincidencia, incluidos los scripts y sus archivos de configuración.
4. Ejecuta la conexión con credenciales desechables y verifica los hosts de salto reales, las claves de host y los procesos locales.
5. Revoca la credencial de prueba después de la prueba y registra la ruta esperada junto al alias.

No te bases únicamente en una conexión satisfactoria. El éxito demuestra que los bytes llegaron a un servidor SSH. No demuestra que los transportó el programa correcto, que los recibió el host previsto ni que antes no ocurrió ningún efecto local.

Un poco de fricción es apropiado aquí. Si nadie puede explicar el ejecutable detrás de un ProxyCommand, nadie debería autorizar a un agente a invocar ese alias. Sustitúyelo, elimínalo o mantén la ruta fuera del alcance del agente hasta que alguien se haga responsable.

## Una superficie SSH pequeña supera a una configuración de cliente ingeniosa

La configuración SSH más segura de cara a un agente tiene pocos destinos con nombre, identidades fijas, hosts de salto explícitos y ninguna opción de configuración arbitraria. No necesita un lenguaje de políticas de propósito general para lograrlo. Necesita una interfaz limitada y una configuración que siga siendo aburrida al inspeccionarla.

Vuelve a revisar cuando cambie cualquiera de estos elementos: una actualización del cliente SSH, un nuevo script de arranque, un despliegue de gestión de configuración, un directorio de inclusiones añadido, un nuevo entorno de ejecución de agentes o un nuevo host de salto. Estos cambios suelen llegar en repositorios distintos, precisamente por eso la ruta de conexión se deteriora sin que nadie lo note.

Mantén sencillo el criterio final: antes de que se ejecute un comando remoto, deberías poder nombrar cada ejecutable local que inicia SSH, cada host con el que contacta, cada identidad que usa y la persona que aprobó esa ruta. Si no puedes hacerlo para un alias, no está listo para uso autónomo.
