Límites de seguridad de MCP stdio para agentes de IA locales
Seguridad de MCP stdio para agentes de IA locales: separa el contexto de las herramientas de las acciones con credenciales, aprobaciones, controles SSH y registros de auditoría resistentes a manipulaciones.

Los servidores MCP locales suelen considerarse inofensivos porque hablan mediante stdio y se ejecutan en el mismo Mac que el agente. Esa conclusión deja de ser válida en cuanto el servidor puede llamar a una API externa, usar SSH, leer un archivo de credenciales o lanzar un comando del shell con la autoridad del desarrollador. El transporte local elimina un salto de red. No reduce la autoridad del proceso que recibe las solicitudes.
El límite importante es sencillo: un servidor MCP debería exponer contexto y cálculos estrictamente acotados; una pasarela de acciones de confianza debería guardar las credenciales y ejecutar las acciones que salen del equipo. Mezclar esas funciones ofrece a un modelo de lenguaje una ruta cómoda desde instrucciones no confiables hasta una autoridad duradera. He visto este error aparecer disfrazado de una herramienta ordenada en un solo archivo y convertirse después en una mezcla de tokens, llamadas a subprocesos y excepciones que nadie sabe explicar durante un incidente.
Stdio es un transporte, no una decisión de confianza
La seguridad de MCP stdio empieza por aceptar que la entrada y la salida estándar no autentican la intención. El cliente MCP inicia el servidor e intercambia mensajes JSON-RPC mediante tuberías. La especificación de Model Context Protocol describe stdio como un transporte: el servidor lee mensajes de la entrada estándar y escribe mensajes en la salida estándar. No afirma que el transporte demuestre que una solicitud es segura, que la haya aprobado una persona ni siquiera que la haya producido el modelo esperado.
Un cliente local puede enviar directamente una solicitud tools/call. Puede saltarse el ciclo normal del modelo, repetir llamadas a la velocidad del equipo, elegir argumentos que la descripción de la herramienta desaconsejaba y conservar todos los resultados que recibe. Si una extensión comprometida del editor inicia el cliente, el servidor no tiene una forma mágica de saber que no era Claude Code u otro llamador esperado, a menos que el diseño del entorno le proporcione esa información.
El árbol de procesos habitual también ofrece menos aislamiento del que mucha gente supone. Si el agente inicia un servidor MCP con tu cuenta, ese servidor suele heredar tu identidad de usuario, directorio de trabajo, entorno, permisos de archivos, acceso de red y cualquier secreto que hayas dejado en variables de entorno. Una tubería no reduce esa autoridad.
Trata cada llamada a una herramienta como una solicitud no confiable procedente de un proceso que ha convencido al modelo, o se ha hecho pasar por él, para realizarla. Suena estricto porque lo es. También es la suposición que resiste la inyección de instrucciones, los clientes defectuosos, las configuraciones copiadas y las pruebas de un desarrollador que envía una solicitud con un script JSON-RPC directo.
El servidor debe detenerse antes de la autoridad reutilizable
Un servidor MCP debería terminar su trabajo antes de necesitar revelar o gestionar una credencial reutilizable. Sus tareas adecuadas incluyen buscar en un repositorio indexado, analizar la salida de una compilación, dar formato a una carga útil, leer un archivo del proyecto expuesto deliberadamente y producir un comando propuesto para revisión. Esas tareas todavía pueden causar daños si están mal escritas, pero no necesitan un secreto que siga siendo útil cuando termine la sesión.
Las acciones externas necesitan otro responsable. Una solicitud autenticada a una API, una conexión SSH, la publicación de un paquete, una consulta a producción o la actualización de una incidencia unen una entrada no confiable con una identidad que tiene consecuencias. Coloca la credencial en un componente que realice la solicitud y devuelve después un resultado limitado al servidor MCP o al agente.
Esta distinción se difumina a menudo porque ambos componentes pueden ser ejecutables locales. No son intercambiables:
- Un servidor MCP traduce una solicitud del agente en una operación limitada o en una solicitud para realizar una operación.
- Una pasarela de acciones posee la credencial, decide si este proceso puede usarla, ejecuta la llamada externa y registra lo ocurrido.
- El agente recibe la salida, no los medios para repetir la acción autenticada fuera de esa pasarela.
No envíes API_TOKEN=... como resultado de una herramienta. No envíes una referencia a una bóveda y la consideres segura. No expongas un comando que muestre una clave privada en stdout confiando en que el modelo apartará la vista. Una vez que el cliente obtiene un secreto, todos los controles posteriores son solo orientativos.
Una pasarela tampoco debería convertirse en una API de shell para todo. run(command) resulta tentador porque evita diseñar herramientas. También entrega el análisis de argumentos, el acceso a archivos, los destinos de red y a menudo el acceso a secretos a una única cadena opaca. Crea acciones limitadas, como get_deployment_status, create_issue, run_readonly_query o ssh_exec con un host identificado y una familia de comandos restringida. Las acciones limitadas hacen posible la validación y la revisión.
Los esquemas de las herramientas describen las llamadas, pero no las limitan
Un JSON Schema para una herramienta resulta útil para validar entradas, pero no es una autorización. La especificación de MCP exige que las herramientas publiquen esquemas de entrada, y los clientes pueden usarlos para formar llamadas. Un modelo todavía puede elegir cualquier valor válido según el esquema. Peor aún, las implementaciones descuidadas suelen aceptar una cadena válida según el esquema y después insertarla en un comando de shell o una URL, donde su significado cambia.
Considera una herramienta destinada a consultar el estado de un despliegue:
{
"name": "deployment_status",
"inputSchema": {
"type": "object",
"properties": {
"environment": {"enum": ["staging", "production"]},
"service": {"type": "string", "pattern": "^[a-z0-9-]{1,48}$"}
},
"required": ["environment", "service"],
"additionalProperties": false
}
}
Ese esquema impide un campo superior inesperado y rechaza la puntuación de shell evidente en service. No autoriza al llamador a inspeccionar producción, no demuestra que service pertenezca al repositorio actual ni limita el destino HTTP después de que el servidor construya una URL. Un validador de esquemas responde: «¿Tiene la forma correcta?». La autorización responde: «¿Puede este llamador realizar ahora esta acción con esta identidad?». Mantén separadas ambas preguntas en el código y en las revisiones.
Una implementación defectuosa suele parecerse a esto:
subprocess.run(
f"ssh {host} systemctl status {service}",
shell=True,
check=True,
)
Aunque host y service hayan pasado un esquema poco estricto, el análisis del shell crea otro lenguaje con otra superficie de ataque. Usa vectores de argumentos, rechaza los hosts desconocidos antes de abrir una conexión y haz que la pasarela seleccione la credencial mediante un identificador fijo, en lugar de aceptar una ruta o un nombre de token proporcionado por el agente.
Para HTTP, analiza la URL antes de conectarte, exige https, compara el nombre de host normalizado con una lista exacta de hosts aprobados y desactiva las redirecciones o vuelve a validarlas. Una redirección desde un host permitido hacia una dirección interna o un endpoint controlado por un atacante puede convertir una solicitud aparentemente inofensiva en una filtración de credenciales. No dependas de una comprobación de prefijo como url.startswith("https://api.example.com"); la información del usuario, los puertos y los nombres de host parecidos hacen que las comprobaciones de cadenas no sean fiables.
La identidad del proceso debe ser visible en el punto de aprobación
Un botón de aprobación humana solo ayuda cuando indica quién solicita la acción y qué autoridad concede esa aprobación. «Permitir acceso al agente» es una opción débil porque oculta el ejecutable que recibirá el permiso y la duración de ese permiso. Enseña a las personas a aprobar una categoría vaga de actividad.
Un diseño mejor identifica el proceso solicitante mediante su autoridad de firma de código, la relación con su proceso padre, la ruta del ejecutable y la duración del proceso. Así, la persona puede aprobar una ejecución de un cliente conocido en lugar de dar permiso permanente a una etiqueta. Cuando el proceso termina, la aprobación debe terminar también. Un proceso nuevo necesita una decisión nueva.
La firma de código no demuestra que todas las instrucciones o plugins dentro del cliente sean legítimos. Responde a una pregunta más limitada, pero útil: ¿qué ejecutable firmado solicitó la autoridad? La distinción importa cuando un programa local malicioso o alterado intenta reutilizar un nombre conocido. En macOS, el sistema operativo proporciona información de firma de código que una pasarela puede mostrar antes de permitir actuar a un proceso.
Sallyport usa esa identidad del proceso para autorizar cada sesión, mientras su bóveda bloqueada rechaza todas las acciones hasta que el usuario la abre mediante los controles del Mac protegidos por hardware. El modelo es deliberadamente pequeño: una bóveda bloqueada, aprobación para un proceso nuevo y aprobación opcional para cada uso de una credencial concreta.
No intentes resolver la fatiga de las aprobaciones con una política compleja en lenguaje natural. Las personas no pueden juzgar de forma fiable un conjunto de reglas denso después de que haya acumulado decenas de excepciones. Usa pocas decisiones que correspondan a cosas que un desarrollador pueda ver: si los secretos están disponibles, qué proceso puede actuar durante esa ejecución y qué credenciales necesitan una confirmación nueva cada vez.
El alcance de la aprobación debe seguir el daño que puede causar una acción
Una aprobación por sesión es adecuada para tareas repetitivas y de bajo impacto, como leer un gestor de incidencias o comprobar el estado de un servicio de desarrollo. Se vuelve peligrosa cuando la misma aprobación cubre en silencio cambios destructivos en bases de datos, publicación de paquetes, movimiento de dinero, comunicaciones con clientes o acceso SSH a producción.
Asigna a cada credencial su propia sensibilidad de aprobación. Un token de solo lectura puede funcionar después de que el proceso obtenga la aprobación de la sesión. Un token de escritura en producción o una clave SSH deberían exigir confirmación en cada uso. La pasarela debe mostrar suficiente contexto para que una persona pueda juzgar la acción: identidad de la credencial, host o servicio de destino, método o clase de comando y argumentos saneados. No muestres el secreto.
Una aprobación debería autorizar una solicitud concreta, no prometer que el agente se comportará bien más adelante. Si una llamada a una herramienta dice POST /releases, la vista de aprobación no debería reducirla a «usar la API de versiones». El método, el destino final y el nombre de la operación son los datos que distinguen una lectura inofensiva de una escritura irreversible.
La alternativa habitual es una lista de permisos amplia: aprobar un dominio, un binario del shell o un agente durante toda la jornada. Parece eficiente hasta que una instrucción inyectada dirige esa misma capacidad hacia otro repositorio, endpoint o argumento. Los permisos amplios reducen las interrupciones trasladando la carga de revisión al momento en que nadie puede ver la llamada real.
Usa vencimientos estrictos. Una autorización de sesión debe desaparecer cuando termine el proceso del cliente. Una decisión por llamada debe caducar después de esa operación. Si más adelante una pasarela necesita permisos más largos, haz explícitos el alcance y el vencimiento, en lugar de dejar que una aprobación almacenada se haga pasar por confianza permanente.
SSH necesita su propio límite, no una vía de escape al shell
SSH es el punto donde los diseños de agentes locales suelen perder disciplina. Los desarrolladores ya tienen un agente SSH, alias de hosts, claves reenviadas y la costumbre de escribir comandos arbitrarios en una terminal. La tentación es dejar que el servidor MCP llame a ssh con el entorno existente del usuario. Eso convierte al agente en un llamador de todas las identidades y reglas de host accesibles desde tu shell.
OpenSSH documenta una limitación grave del reenvío del agente: un usuario remoto que pueda acceder al socket del agente reenviado puede solicitar operaciones a tu agente local, aunque no pueda extraer las claves privadas. Eso basta para actuar como tú mientras dure el reenvío. Un agente autónomo no debería activar ese camino sin más, porque amplía la autoridad más allá del host original.
Usa una identidad SSH exclusiva para el trabajo del agente y vincúlala a un registro de host identificado. En el servidor, limita esa identidad con las opciones apropiadas para la cuenta, como un comando forzado y el reenvío desactivado cuando el caso de uso lo permita. En el lado local, selecciona el host y la identidad mediante una configuración que el agente no pueda controlar. El agente puede solicitar host: build-staging y una acción de comando limitada, pero no debería enviar un nombre de host arbitrario, una ruta de clave privada ni -o ProxyCommand=....
Esta es la forma mínima de una solicitud que una pasarela de acciones puede validar:
{
"action": "ssh_exec",
"host_id": "build-staging",
"command_id": "read_service_status",
"args": {"service": "worker"}
}
La pasarela asigna build-staging a su host conocido, su política de claves de host, su cuenta y su credencial exclusiva. Asigna read_service_status a un vector de argumentos fijo. No concatena esta carga útil en una cadena de shell. Una solicitud rechazada debería explicar el motivo en un registro de auditoría sin repetir material secreto ni datos potencialmente hostiles en una terminal.
Si necesitas diagnósticos remotos arbitrarios, conviértelos en una operación independiente y de alta fricción, con confirmación por llamada y límites de salida evidentes. No ocultes el acceso arbitrario al shell bajo un nombre amable como check_server.
Los registros de auditoría deben sobrevivir al actor que los causó
Un registro de texto escrito por el mismo proceso que realiza la acción solo es una prueba hasta que ese proceso quiere modificarlo. La actividad del agente necesita un registro que permita reconstruir tanto la ejecución como cada llamada externa y detectar después cualquier eliminación o edición.
Registra la identidad del proceso, el identificador de sesión, la hora, el tipo de acción, el identificador de la credencial, el destino aprobado, la forma saneada de la solicitud, el resultado de la aprobación, el estado de la respuesta y la categoría del error. Separa el diario de sesiones del diario de acciones. La vista de sesión responde: «¿Qué ejecución del agente tenía autoridad?». La vista de acciones responde: «¿Qué hizo con esa autoridad?». No obligues a un investigador a deducir una respuesta a partir de un flujo plano de líneas.
Una cadena de hashes ofrece una comprobación práctica de integridad. Para cada registro, calcula un resumen a partir del resumen del registro anterior y de los bytes canónicos del nuevo registro cifrado. Guarda el nuevo resumen junto al registro. Un verificador puede detectar un registro modificado, uno eliminado en medio de la secuencia o una secuencia reordenada sin necesitar el texto plano.
El verificador de auditoría debe ejecutarse de forma independiente del agente y no debería necesitar acceso a las credenciales. Una interfaz de comandos puede ser tan sencilla como esta:
$ sp audit verify
records: 184
first sequence: 1
last sequence: 184
chain: valid
Ese formato ofrece al operador algo concreto que capturar en una incidencia o en un registro de incidente. Si la verificación falla, el comando debería indicar la primera secuencia donde se rompió la continuidad y devolver un estado de salida distinto de cero. «Registro ilegible» es demasiado impreciso para investigar.
La evidencia de manipulación no equivale a la prevención de manipulaciones. Un usuario local con acceso suficiente todavía puede eliminar todo el registro o restaurar una versión anterior de su almacenamiento. Mantén visible esa limitación. Si necesitas protegerte contra la restauración local, exporta puntos de control firmados a otro sistema controlado. No afirmes que una cadena de hashes local resuelve una amenaza que no cubre.
Mantén los secretos fuera de las variables de entorno y de la salida de las herramientas
Las variables de entorno son prácticas para una sesión de shell humana y ofrecen un mal aislamiento para los agentes autónomos. Un proceso secundario las hereda por defecto. Los registros de depuración pueden imprimirlas. Un comando que enumere el entorno puede devolvérselas al modelo. Los informes de fallos, los paquetes de soporte, la inspección de procesos y las transcripciones de terminal copiadas han expuesto secretos de esta manera.
Un archivo de credenciales en el espacio de trabajo es aún peor. El modelo puede leerlo, una herramienta puede subirlo, un comando de Git puede prepararlo para una confirmación y un servicio de indexación puede conservar una copia. Mover el archivo a un directorio oculto cambia la frecuencia de los accidentes, pero no el límite de seguridad.
Guarda las credenciales en una bóveda controlada por la pasarela de acciones. La pasarela selecciona la credencial según una asignación fija de acciones y la inyecta únicamente en su propia operación HTTP o SSH. Devuelve el cuerpo de la respuesta después de filtrarlo solo cuando sea seguro para el agente. Un token bearer nunca debe pasar por el resultado de MCP, ni siquiera oculto parcialmente, porque los errores de redacción se convierten en una parte permanente de la transcripción.
Para HTTP, prefiere un contrato de respuesta en lugar de pasar la respuesta sin cambios. Una acción de estado de despliegue podría devolver esto:
{
"environment": "staging",
"service": "worker",
"state": "healthy",
"revision": "a1b2c3d4"
}
No debería devolver encabezados de respuesta que puedan contener identificadores de sesión, detalles de enrutamiento interno o un token nuevo. Decide qué campos necesita el agente antes de implementar. Un proxy de respuesta sin filtrar es otro atajo que después resulta costoso de eliminar.
Un límite pequeño es más fácil de operar bajo presión
Puedes revisar la integración de un agente local sin un lenguaje de políticas ni un programa de seguridad enorme. Empieza por el inventario de acciones y obliga a clasificar cada una en uno de dos grupos: lee o calcula contexto local sin autoridad reutilizable, o cruza hacia un servicio externo y necesita una pasarela.
Para cada acción externa, anota la identidad fija del destino, la credencial seleccionada por la pasarela, los argumentos exactos que el agente puede influir, el alcance de la aprobación y el registro de auditoría que se produce. Si alguna fila dice «comando arbitrario», «cualquier URL», «token del entorno» o «el agente elige la credencial», el límite aún no está terminado.
Realiza este ejercicio de fallos antes de entregar al agente un secreto útil:
- Envía una solicitud válida según el esquema que indique un destino inesperado o un argumento demasiado grande.
- Repite una solicitud aprobada anteriormente después de que termine el proceso del cliente.
- Intenta una solicitud HTTP redirigida y una solicitud SSH con opciones de reenvío.
- Bloquea la bóveda y confirma que todas las acciones fallan antes de abrir cualquier conexión de red.
- Modifica un registro de auditoría almacenado y verifica que el comando de auditoría identifica la cadena rota.
Estas pruebas detectan los errores de diseño que una demostración agradable oculta. Un modelo que sigue las instrucciones a la perfección no es una prueba de seguridad.
La arquitectura local más limpia mantiene el servidor MCP ordinario y desechable. Deja que exponga contexto útil y solicite operaciones diseñadas de forma estricta. Coloca los secretos, la aprobación consciente del proceso, la ejecución y un registro auditable detrás del límite de acciones. Cuando alguien pregunte por qué una herramienta no puede recibir simplemente el token de producción, la respuesta debería estar a la vista en el diseño: la herramienta nunca necesitó el token para hacer su trabajo.
FAQ
¿MCP stdio es seguro porque se ejecuta localmente?
No. stdio proporciona a un proceso local un flujo de bytes, no un límite de seguridad. El cliente inicia el servidor, puede enviar cualquier solicitud MCP válida y normalmente tiene el mismo acceso a nivel de usuario que recibe el proceso del servidor.
¿Qué debería poder hacer un servidor MCP?
Usa un servidor MCP para el contexto local, las transformaciones deterministas y las capacidades limitadas que no necesitan credenciales privilegiadas. Coloca las solicitudes HTTP autenticadas, las conexiones SSH, los pagos, los despliegues y otros efectos externos similares detrás de una pasarela de acciones independiente.
¿Debería recibir alguna vez un agente de IA una clave de API mediante MCP?
El agente debería recibir solo el resultado que necesita para su tarea, como un estado, determinados campos o la salida de un comando. Nunca debería recibir un token reutilizable, una clave privada, un marcador de credencial ni un archivo de configuración que le permita obtener uno más adelante.
¿Las descripciones de las herramientas MCP son una política de seguridad?
No. La descripción de una herramienta ayuda al modelo a elegirla, pero no restringe los argumentos maliciosos o equivocados. El ejecutable debe validar los argumentos y aplicar el límite de autoridad real.
¿Cuándo debería exigir aprobación para cada acción del agente?
La aprobación a nivel de proceso es útil cuando identifica el ejecutable que actuará y caduca al terminar ese proceso. Es demasiado amplia para las credenciales que pueden causar daños costosos o irreversibles. En esos casos, cada uso debería requerir una decisión independiente.
¿Cómo impido que la inyección de instrucciones cambie una llamada a una herramienta?
Los argumentos que indican un host, una URL, una ruta, una rama, un destinatario o una cuenta son entradas de seguridad. Valídalos con restricciones explícitas, resuelve las rutas antes de comprobarlas, rechaza las redirecciones que salgan de los hosts aprobados y registra el destino final.
¿Por qué las variables de entorno son un mal lugar para las credenciales de un agente?
Las variables de entorno se filtran fácilmente mediante procesos secundarios, diagnósticos, historial del shell, informes de fallos y salidas accidentales de comandos. Un intermediario local de credenciales reduce esa exposición al mantener el secreto en su propio proceso y ejecutar directamente la operación autenticada.
¿Qué debe contener un registro de auditoría de las acciones de un agente de IA?
Un registro útil vincula cada solicitud con el proceso que la invoca, el nombre de la herramienta, la autoridad aprobada, los argumentos saneados, el estado del resultado y la hora. Un registro de solo adición y con evidencia de manipulación es más sólido que un archivo de texto que el mismo usuario o proceso puede editar.
¿Es seguro el reenvío del agente SSH para agentes de programación?
No. El reenvío de SSH y el reenvío del agente pueden permitir que un host remoto solicite firmas a través de tu agente local. Usa una clave exclusiva con restricciones estrictas en el servidor o deja que la pasarela controle la operación SSH y solicite aprobación en el momento de usarla.
¿Necesito una pasarela de acciones para todas las herramientas locales de IA?
Una pasarela local de acciones es especialmente útil cuando los agentes necesitan autoridad externa real, pero no deben conservar secretos reutilizables. No hace falta para una herramienta de solo lectura que formatea texto o busca en un repositorio sin credenciales ni acceso a servicios externos.