# ¿Pueden los comandos SSH forzados contener una cuenta de servicio de IA?

Un agente de IA nunca debería recibir una credencial SSH que signifique «haz todo lo que la cuenta pueda hacer». Eso no es un límite de permisos. Es una invitación a encontrar una forma de superarlo, normalmente mediante un argumento que no esperabas, una conexión reenviada que olvidaste desactivar o un script de despliegue que confía demasiado en quien lo llama.

Los comandos SSH forzados permiten que el servidor remoto tenga la última palabra sobre lo que se inicia después de la autenticación. Funcionan bien para cuentas de despliegue y diagnóstico porque sustituyen una capacidad imprecisa, el acceso a un shell remoto, por una operación concreta que controlas y puedes inspeccionar. No sustituyen la aprobación humana del uso de la credencial. Mantén ambos controles separados: una persona decide si un agente puede usar la credencial y el servidor decide qué acción limitada puede realizar esa credencial.

## Un comando forzado limita la ejecución, no la autenticación

Un comando forzado indica a sshd que ejecute un programa elegido por el servidor incluso cuando el cliente solicita un shell o proporciona otro comando. El cliente sigue autenticándose primero. La diferencia parece obvia hasta que una cuenta de servicio aparece en la configuración de un agente y alguien empieza a tratar un inicio de sesión correcto como si fuera un despliegue aprobado.

OpenSSH admite este control en dos lugares. Puedes asociar `command="/path/to/wrapper"` a una clave pública en `authorized_keys`, o establecer `ForceCommand` en `sshd_config` para un usuario o grupo. En ambos casos, sshd registra el comando solicitado por el cliente en la variable de entorno `SSH_ORIGINAL_COMMAND` y, en su lugar, inicia el programa forzado.

El manual `sshd(8)` de OpenSSH es claro sobre el primer punto: una opción `command` fuerza la ejecución del comando especificado después de la autenticación. También documenta que el comando original sigue estando disponible para ese programa forzado. Ese segundo detalle es donde fallan muchos diseños débiles. El envoltorio recibe una cadena de un cliente no confiable. Debe analizarla como una solicitud, no enviarla a un shell.

Usa la configuración por clave cuando una cuenta tenga varias credenciales cuidadosamente separadas. Una credencial de publicación puede iniciar el envoltorio de despliegue, mientras que una credencial de operaciones puede iniciar un envoltorio de diagnóstico de solo lectura. Así la intención queda visible en `authorized_keys` y puedes eliminar una credencial sin cambiar el resto del acceso de la cuenta.

Usa `ForceCommand` cuando la cuenta nunca deba proporcionar un shell general, independientemente de cómo se autentique. Esto incluye una contraseña que olvidaste desactivar, una futura autoridad certificadora o un administrador que añada otra clave pública sin copiar las opciones necesarias. Un bloque `Match User deploy` hace que la regla sea difícil de pasar por alto durante una revisión.

No uses ninguna de las dos formas para convertir una cuenta de administrador humano en una cuenta de automatización. Tarde o temprano, las personas necesitan un shell real para reparar algo. Da a la automatización una cuenta Unix independiente, una credencial separada, un envoltorio de comandos distinto y límites de propiedad adecuados para la tarea.

## El servidor debe controlar el punto de entrada del despliegue

Una cuenta de despliegue debería entrar en un único script bajo tu control, no en un intérprete de comandos genérico. El script puede aceptar un vocabulario reducido de solicitudes, pero debe elegir por sí mismo la ruta del repositorio, el directorio de destino, la unidad de servicio y el ejecutable.

Esta entrada de `authorized_keys` limita una sola credencial a un envoltorio y rechaza funciones de conexión que no tienen lugar en una cuenta de despliegue:

```text
restrict,command="/usr/local/libexec/release-gate" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... release-agent
```

La opción `restrict` resulta útil porque OpenSSH la documenta como una abreviatura que desactiva el reenvío de puertos, el reenvío del agente, el reenvío X11 y la asignación de pseudoterminales. Su comportamiento exacto depende de las opciones de OpenSSH compatibles con el servidor, así que pruébala en la versión que utilices. Si tu infraestructura necesita opciones explícitas por motivos de revisión o compatibilidad, escríbelas completas:

```text
command="/usr/local/libexec/release-gate",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... release-agent
```

El envoltorio no debería aceptar un comando de despliegue libre. Ofrece verbos fijos y un valor limitado. Por ejemplo, los clientes pueden solicitar una publicación únicamente mediante una revisión inmutable:

```text
ssh release@deploy.example "release 9f2a7c6d1e4b8a03"
```

Un envoltorio seguro puede aceptar esa gramática y nada más:

```sh
#!/bin/sh
set -eu

request=${SSH_ORIGINAL_COMMAND-}
case "$request" in
  "release "[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]* )
    revision=${request#release }
    case "$revision" in
      *" "*|*[!0-9a-f]*)
        echo "invalid revision" >&2
        exit 64
        ;;
    esac
    exec /usr/local/libexec/run-release "$revision"
    ;;
  *)
    echo "unsupported remote request" >&2
    exit 64
    ;;
esac
```

Este ejemplo aún necesita comprobar la longitud si el formato de revisión lo exige. Un envoltorio de producción debería aceptar un identificador de objeto inmutable completo o un identificador de publicación cuyo formato hayas definido. No aceptes un nombre de rama como `main` si el llamador puede moverlo entre la aprobación y el despliegue. Una rama es un puntero. Una revisión inmutable permite que el registro de aprobación, el registro de despliegue y el artefacto resultante se refieran a lo mismo.

El script `run-release` debería usar rutas absolutas y establecer su propio entorno. No dependas del `PATH`, el directorio de trabajo, la configuración regional, `GIT_DIR`, `GIT_SSH_COMMAND` o `LD_PRELOAD` proporcionados por el llamador. Un comienzo mínimo sería este:

```sh
#!/bin/sh
set -eu
PATH=/usr/sbin:/usr/bin:/sbin:/bin
export PATH
unset CDPATH ENV BASH_ENV GIT_DIR GIT_WORK_TREE GIT_SSH_COMMAND
cd /srv/release-repo

revision=$1
/usr/bin/git cat-file -e "$revision^{commit}"
/usr/local/libexec/build-and-activate "$revision"
```

La cuenta debería ser propietaria únicamente de los archivos que necesita modificar. Si tiene que reiniciar un servicio, concede un único comando limitado en `sudoers` con argumentos fijos, en lugar de acceso sin contraseña a un gestor de paquetes o shell general. Una cuenta de despliegue que pueda escribir su propio envoltorio, modificar su propio `authorized_keys` o editar la unidad que ejecuta su código normalmente puede recuperar un control amplio. Revisa esas rutas, no solo la configuración de SSH.

## `SSH_ORIGINAL_COMMAND` es una entrada, no una línea de comandos

El error más frecuente con los comandos forzados es esta línea:

```sh
sh -c "$SSH_ORIGINAL_COMMAND"
```

Esa línea anula el control que acabas de instalar. El cliente puede solicitar `release goodrev; curl ... | sh`, sustitución de comandos, salida redirigida o un argumento cuidadosamente entrecomillado que llegue a una herramienta con privilegios. Un envoltorio que invoque `eval`, `sh -c`, `bash -c` o una expansión sin comillas vuelve a crear el acceso a un shell remoto bajo otro nombre de archivo.

No intentes crear un analizador completo de shell. No lo necesitas. Define un protocolo deliberadamente pequeño y rechaza todo lo que quede fuera de él. Para una cuenta de despliegue, una solicitud puede constar de un verbo y un identificador. Para una cuenta de diagnóstico, puede ser una palabra exacta como `health` o `version`.

Un envoltorio de distribución para diagnósticos puede evitar el análisis por completo:

```sh
#!/bin/sh
set -eu

case "${SSH_ORIGINAL_COMMAND-}" in
  health)
    exec /usr/local/libexec/report-health
    ;;
  queue-depth)
    exec /usr/local/libexec/report-queue-depth
    ;;
  version)
    exec /usr/local/libexec/report-version
    ;;
  "")
    echo "a diagnostic name is required" >&2
    exit 64
    ;;
  *)
    echo "diagnostic is not allowed" >&2
    exit 64
    ;;
esac
```

Esos scripts de diagnóstico también deben controlar sus argumentos. `report-health` debería llamar a binarios fijos contra sockets locales fijos o nombres de servicio conocidos. No debería aceptar un parámetro de host y ejecutar `curl "$host"`, ni aceptar un filtro de registro y pasarlo a un shell. Un comando de solo lectura aún puede revelar credenciales de bases de datos, topología interna, valores del entorno o datos de clientes.

A menudo se dice que un comando de shell entrecomillado correctamente basta porque el agente que lo llama es confiable. La afirmación se desmorona cuando el agente sigue instrucciones hostiles de un repositorio, confunde un valor con una instrucción o comete un error corriente. El servidor remoto no puede saber si una solicitud peligrosa procede de una acción maliciosa o de una llamada de herramienta demasiado entusiasta. Solo ve una entrada. Haz que su decisión sea determinista.

Si necesitas entradas estructuradas, envía un formato limitado y analízalo con un analizador que rechace campos adicionales. JSON no es automáticamente más seguro porque un envoltorio de shell puede seguir tratándolo mal. Una solicitud pequeña como `release <64 lowercase hex characters>` es más fácil de validar, documentar, probar y auditar que un bloque JSON con campos opcionales.

## El reenvío puede eludir el espíritu de la restricción

Un comando forzado no impide automáticamente que un cliente autenticado use SSH como túnel. El manual de OpenSSH trata la ejecución de comandos y el reenvío como controles separados. Si añades únicamente `command="..."`, el cliente aún puede pedir a sshd que reenvíe un puerto local a un servicio interno, según el resto de la configuración del servidor.

Esto importa porque una cuenta restringida puede tener un acceso de red que el agente no debería tener. Un agente que no puede ejecutar `/usr/bin/ps` en el host remoto aún podría llegar a un puerto de base de datos a través de ese host si el reenvío sigue abierto. En ese caso, la cuenta deja de ser una identidad de despliegue y se convierte en un punto de pivote de red.

Para una cuenta que no necesita una sesión interactiva, deniega todas estas funciones salvo que puedas explicar por qué necesita alguna:

- Reenvío TCP
- Reenvío del agente
- Reenvío X11
- Asignación de pseudoterminal
- Variables de entorno controladas por el usuario

`restrict` gestiona las cuatro primeras categorías en las implementaciones modernas de OpenSSH. Si tu cuenta necesita legítimamente una excepción, no elimines todo el conjunto de restricciones. OpenSSH admite opciones como `permitopen="host:port"` para limitar el destino del reenvío. Trátalo como un diseño de acceso separado y prueba tanto los destinos permitidos como los rechazados.

Revisa también el acceso de red saliente del envoltorio. Un script de despliegue que pueda obtener URL arbitrarias, clonar repositorios arbitrarios o enviar datos arbitrarios al exterior mantiene abierto un canal amplio aunque el reenvío SSH esté desactivado. Las fuentes de artefactos fijas y las revisiones fijadas reducen esa exposición. Las reglas del cortafuegos o las credenciales específicas del servicio tendrán que cubrir el resto.

## La aprobación debe ocurrir antes de abrir la conexión

Un comando forzado reduce el daño que puede causar un uso SSH aprobado. No responde a la pregunta de si el proceso actual del agente debería usar la credencial. Esa decisión corresponde al límite de la credencial, antes de que el agente cree una conexión SSH.

Esto es especialmente importante para los agentes autónomos de programación. Un repositorio puede indicar al agente que ejecute un comando de despliegue. La salida de una herramienta puede solicitarlo. Una dependencia comprometida puede orientarlo hacia esa acción. Si la credencial está en el entorno o en el sistema de archivos del agente, este puede usarla sin que nadie vea el momento exacto de uso.

Mantén las claves privadas SSH fuera del proceso del agente y solicita aprobación cuando una nueva ejecución del agente pida acceso por primera vez. Para cuentas de alto impacto, solicita aprobación cada vez que se use la credencial. El comando forzado remoto establece entonces un límite firme sobre la acción que autoriza esa aprobación.

Sallyport aplica esta separación al conservar las claves SSH en su almacén cifrado, autorizar por defecto los nuevos procesos de agentes por sesión y ejecutar SSH mediante su asistente `sp-ssh` en lugar de entregar la clave al agente.

No confundas una tarjeta de aprobación con la autorización del servidor. La aprobación responde: «¿Puede este proceso usar ahora esta credencial?». El servidor responde: «¿Qué puede hacer esta credencial después de iniciar sesión?». Necesitas ambas respuestas porque fallan de forma distinta. La aprobación puede detener un proceso inesperado. Los comandos forzados pueden impedir que un proceso aprobado convierta una credencial de publicación en un shell.

Haz que la descripción de aprobación sea útil. Indica el entorno y la acción en la etiqueta de la credencial, por ejemplo, `production release` o `staging diagnostics`. Una etiqueta como `deploy-key-2` obliga al revisor a recordar el historial durante una interrupción. Así las aprobaciones rutinarias terminan convirtiéndose en clics automáticos.

## Separa el despliegue del diagnóstico antes de ampliar la lista permitida

El despliegue y el diagnóstico parecen similares porque ambos necesitan SSH, pero tienen flujos de datos y modos de fallo distintos. Siempre que puedas, colócalos detrás de cuentas separadas o de credenciales con comandos forzados independientes.

Una cuenta de despliegue cambia el estado. Puede obtener una revisión fija, compilar un artefacto, sustituir un directorio de publicación y reiniciar un servicio. Su salida debería informar de la revisión, el destino, el estado de salida y un mensaje breve de error. No necesita acceso arbitrario a registros, inspección de procesos ni consultas a bases de datos.

Una cuenta de diagnóstico lee el estado. Puede informar del resultado de un endpoint de salud, de un recuento limitado de elementos en cola, de la versión de un servicio o del final de un registro local filtrado cuidadosamente. No debería reiniciar servicios, rotar archivos, consultar todos los procesos ni leer rutas arbitrarias. En cuanto un envoltorio de diagnóstico acepte un nombre de archivo, unidad, host u opción de comando proporcionados por el usuario, vuelve a revisar su modelo de entrada.

Una cuenta combinada puede empezar con una lista inocente:

```text
release <revision>
health
logs <service>
restart <service>
```

Después alguien necesita `logs api --since`, otra persona necesita un reinicio de emergencia y el envoltorio empieza a pasar argumentos a `journalctl` o `systemctl`. Pronto el código contiene casos especiales que nadie sabe explicar. Separa las cuentas antes de llegar a ese punto. Las credenciales independientes permiten exigir una aprobación más estricta para los cambios en producción y mantener un flujo de diagnóstico de menor riesgo.

Cada acción debería generar un registro que indique qué aceptó el envoltorio, no solo la cadena opaca del comando SSH. Para una publicación, registra la revisión inmutable y el nombre del destino. Para un diagnóstico, registra el diagnóstico elegido y si tuvo éxito. Mantén los valores secretos fuera de los argumentos y de los registros. Si una acción necesita un secreto, el script remoto debería obtenerlo mediante su propio mecanismo controlado, no recibirlo del cliente SSH.

## Prueba las rutas de denegación desde un cliente desechable

Una cuenta restringida solo está realmente restringida después de probar las solicitudes que debe rechazar. Ejecuta estas comprobaciones desde una cuenta desechable o un host de prueba antes de confiar en la configuración en un entorno de producción. Los ejemplos suponen que la credencial ya está instalada en el servidor.

```sh
ssh release@deploy.example "release 9f2a7c6d1e4b8a03"
ssh release@deploy.example
ssh release@deploy.example "id"
ssh release@deploy.example "release 9f2a; id"
ssh -N -L 15432:db.internal:5432 release@deploy.example
ssh -tt release@deploy.example "health"
```

El primer comando debería llegar únicamente al envoltorio de publicación si la revisión cumple sus reglas. Los tres siguientes deberían fallar con el mensaje de denegación del envoltorio y un estado de salida distinto de cero. El intento de reenvío de puertos debería fallar antes de establecer un escuchador. La solicitud de terminal debería fallar o ejecutarse sin terminal, según cómo informe tu cliente de la asignación denegada.

Después prueba los casos menos evidentes. Intenta usar espacios iniciales y finales, tabuladores, un comando entre comillas vacío, saltos de línea, un argumento muy largo, espacios Unicode, sustituciones de comandos, redirecciones y argumentos duplicados. Si el shell o el envoltorio normalizan alguno de ellos hasta convertirlo en una solicitud aceptada, ajusta la gramática.

Comprueba también los permisos y la propiedad de los archivos de la cuenta. Un atacante que pueda sustituir `/usr/local/libexec/release-gate` no necesita eludir SSH. Una cuenta que pueda modificar su propia fuente de despliegue también puede cambiar el código que se ejecuta con más privilegios. Inspecciona toda la cadena: `authorized_keys`, la configuración de sshd, el envoltorio, los scripts de despliegue, las definiciones de servicios, los directorios modificables y cualquier entrada de `sudoers`.

## Audita la solicitud a ambos lados del límite

Los registros remotos explican qué aceptó el servidor. Los registros del límite de credenciales explican qué proceso local solicitó la capacidad de conectarse. Conserva ambos porque ninguno puede responder a la pregunta del otro.

En el lado remoto, registra el éxito de la autenticación, la operación aceptada por el envoltorio forzado, la revisión inmutable o el nombre del diagnóstico, un identificador de solicitud y el estado final. Envía esos registros a un lugar que la cuenta de servicio no pueda reescribir. No registres el `SSH_ORIGINAL_COMMAND` sin procesar si los clientes pueden incluir material secreto, y no permitas que el protocolo acepte material secreto desde el principio.

En el lado local, conserva la identidad de la sesión y la solicitud individual de acción SSH. Sallyport registra las ejecuciones de los agentes y las llamadas individuales en diarios separados dentro de un registro de auditoría cifrado y encadenado mediante hashes. `sp audit verify` puede verificar esa cadena sin conexión y sin una clave del almacén.

Una cadena de hashes no convierte un permiso incorrecto en uno correcto. Sí facilita detectar modificaciones posteriores del historial registrado. Eso resulta útil tras un despliegue fallido, una aprobación discutida o una solicitud de comando inesperada. También impone una disciplina beneficiosa: define pronto el vocabulario de acciones para que los registros digan algo que una persona pueda entender.

La primera implementación debería ser aburrida. Crea una cuenta Unix dedicada, un único envoltorio forzado, una operación con una gramática de entrada fija, el reenvío desactivado y una prueba que demuestre que `ssh account@host` no proporciona un shell. Añade capacidades solo cuando puedas especificar sus entradas, salidas, acceso a archivos, acceso de red y la persona que debería aprobarlas.
