7 min de lectura

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

Los comandos SSH forzados limitan las cuentas de servicio controladas por IA a acciones concretas del servidor, mientras la aprobación humana permanece en el límite de la credencial.

¿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:

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:

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:

ssh [email protected] "release 9f2a7c6d1e4b8a03"

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

#!/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:

#!/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 -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:

#!/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

Coloca la aprobación antes de los comandos forzados
Sallyport aprueba el uso de la credencial antes de que el comando forzado del servidor decida qué puede ejecutarse.

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

Da a los agentes una puerta de acciones
Claude Code y otros agentes compatibles con MCP se conectan mediante el adaptador sp mcp stdio incluido.

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:

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.

ssh [email protected] "release 9f2a7c6d1e4b8a03"
ssh [email protected]
ssh [email protected] "id"
ssh [email protected] "release 9f2a; id"
ssh -N -L 15432:db.internal:5432 [email protected]
ssh -tt [email protected] "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

Aprueba el proceso que solicita acceso
Un nuevo proceso de agente muestra su autoridad de firma de código antes de realizar su primera llamada SSH.

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.

FAQ

¿Qué es un comando SSH forzado?

Usa una cuenta SSH cuya entrada en authorized_keys incluya la opción command="...", o utiliza ForceCommand en sshd_config cuando todos los métodos de autenticación de esa cuenta deban seguir la misma regla. El servidor ejecuta tu envoltorio en lugar del comando proporcionado por el cliente y pasa la solicitud original mediante SSH_ORIGINAL_COMMAND.

¿Bastan los comandos SSH forzados para proteger una cuenta de servicio de IA?

No. Un comando forzado limita lo que el servidor SSH inicia después de una autenticación correcta, pero no decide quién puede usar la credencial. Combínalo con un límite de credenciales que exija que una persona apruebe una nueva ejecución del agente o un uso sensible. Después, limita la cuenta del servidor para que esa aprobación tenga consecuencias acotadas.

¿Debo usar ForceCommand o el comando de authorized_keys?

Por lo general, no los combines. Una clave específica en authorized_keys establece una relación clara entre una credencial y una regla de comando forzado, mientras que una configuración compartida de la cuenta puede afectar a personas y automatizaciones por igual. Usa ForceCommand cuando quieras deliberadamente que todas las vías de acceso a una cuenta restringida pasen por el mismo envoltorio.

¿Cómo recibe un comando forzado el comando SSH original?

OpenSSH guarda el comando solicitado en SSH_ORIGINAL_COMMAND cuando ejecuta un comando forzado. Trata ese valor como una entrada hostil: rechaza los metacaracteres del shell, rechaza las opciones que no hayas diseñado y acepta únicamente un conjunto reducido de formas de comando exactas.

¿command= en authorized_keys desactiva el reenvío de puertos SSH?

No. La opción command="..." por sí sola no desactiva el reenvío de puertos, el reenvío del agente, el reenvío X11 ni la asignación de una pseudoterminal. Añade restrict cuando corresponda, o desactiva cada capacidad explícitamente y prueba el resultado desde un cliente.

¿Cómo limito una cuenta SSH exclusivamente a despliegues?

Una cuenta de despliegue debería ejecutar un único script bajo tu control, con rutas y directorio de trabajo fijos, un entorno restringido y una lista permitida de destinos de despliegue. No aceptes una rama, un host, una ruta o un fragmento de shell arbitrarios para pasarlos a git, rsync, sudo o a un shell.

¿Qué debe rechazar un envoltorio de comando forzado?

Devuelve un estado distinto de cero ante una solicitud vacía, un shell, un subcomando desconocido, argumentos mal formados o una solicitud con espacios en blanco o metacaracteres inesperados. Registra el rechazo junto con la cuenta autenticada y la información de origen, pero nunca guardes secretos transmitidos mediante variables de entorno o texto de comandos.

¿Pueden los agentes de programación de IA usar comandos SSH forzados sin interacción?

Los comandos forzados funcionan con SSH porque el servidor, no el proceso de IA, decide qué programa se inicia después de la autenticación. No necesitan un aviso interactivo en el host remoto, por lo que sirven para ejecuciones de agentes sin interacción siempre que mantengas la aprobación en el límite de la credencial.

¿Cómo audito las acciones realizadas mediante una cuenta SSH con comandos forzados?

Conserva los registros del envoltorio remoto, los registros de despliegue y los registros de autenticación SSH, y relaciónalos con el registro de uso de la credencial. Un registro local de acciones con evidencias de manipulación es útil porque muestra qué proceso del agente solicitó usar la credencial antes de que el servidor remoto recibiera la conexión.

¿El acceso SSH de despliegue y diagnóstico debería usar cuentas separadas?

Usa una cuenta independiente cuando la tarea de diagnóstico tenga un objetivo, unos comandos permitidos o unas consecuencias diferentes de los del despliegue. Combinar ambas funciones en un solo envoltorio suele ampliar tanto la lista permitida que termina convirtiéndose en un shell remoto accidental.

Sallyport

Sallyport ejecuta llamadas de API y comandos SSH por tu agente de IA. Las claves se quedan en una bóveda local de tu Mac; tú apruebas cada ejecución y cada acción queda en un registro sellado.

© 2026 Sallyport · Código abierto bajo Apache-2.0 · Oleg Sotnikov