← Volver a todas las publicaciones
Publicado el · por Renaud Deraison

Las políticas de OpenShell en Bromure, y el razonamiento detrás de cada conexión

OpenShell de NVIDIA describe en un único archivo de política lo que un agente de programación puede tocar. Bromure Agentic Coding 5.4 aplica esas políticas en la VM del agente, con Landlock y seccomp dentro y las reglas de red en tu Mac, y vigila el kernel del invitado con un módulo centinela. Luego vincula cada conexión de red con los procesos que la abrieron, la llamada a herramienta que los lanzó y el razonamiento que dio el agente para esa llamada.

Un agente de programación necesita acceso real para ser útil, y una política dice cuánto. Funciona mejor como un único documento que puedas leer, comprobar y aplicar, en lugar de ajustes repartidos entre un firewall, opciones de montaje y la propia configuración del agente.

OpenShell de NVIDIA

OpenShell es el runtime de código abierto de NVIDIA para agentes, publicado bajo la licencia Apache 2.0. Construimos sobre su formato de política, en el que un solo documento YAML cubre cuatro áreas:

  • filesystem_policy y landlock: qué rutas puede leer y escribir el agente, aplicadas por el módulo de seguridad Landlock del kernel de Linux.
  • process: con qué usuario y grupo se ejecuta el agente.
  • network_policies: a qué destinos puede llegar cada programa y qué solicitudes puede enviar allí, hasta el método y la ruta HTTP. Todo lo que no está permitido se deniega: “OpenShell deniega toda conexión saliente de un sandbox salvo que una regla de network_policies la permita.”

OpenShell incluye además un prover que usa un solver SMT para comprobar si una política permite algo más allá de otra. Una organización puede fijar un límite y obtener un sí o un no para cada cambio, en lugar de leer diffs.

Es un diseño cuidadoso, hecho por gente que claramente ha estudiado cómo fallan los agentes, y es abierto, así que partimos de él.

OpenShell en Bromure

En Bromure Agentic Coding 5.4, un espacio de trabajo puede usar una política de OpenShell en lugar de las reglas de firewall propias de Bromure. Tomamos el formato de la política y su semántica, no el código: el motor es nuestro, en el Mac, junto con nuestros propios componentes en el invitado Linux. Para asegurarnos de que los dos coinciden, ejecutamos nuestro motor y el motor de referencia de OpenShell sobre 125.666 casos generados que abarcan reglas de red, reglas HTTP, MCP, GraphQL, WebSocket y comprobaciones de dirección de destino. No discreparon en ninguno. Esas pruebas sacaron a la luz trucos de ruta como ..%3b y barras doblemente codificadas (%252F), que ahora tratamos igual que OpenShell.

Bromure se diferencia de OpenShell en dónde se aplica la política. OpenShell arranca los sandboxes a través de un gateway y un runtime de contenedores. Bromure ejecuta cada espacio de trabajo en su propia VM Linux desechable en tu Mac, así que la política se reparte entre los dos lados:

  • En la VM: Landlock para el sistema de archivos, seccomp, no_new_privs y el cambio al usuario de la política. La siguiente sección explica cómo.
  • En el Mac: las reglas de red. Cada conexión sale de la VM a través del switch virtual de Bromure, que comprueba el destino y el puerto, y de su proxy TLS, que comprueba los métodos y rutas HTTP y los mensajes MCP, GraphQL y WebSocket. El Mac resuelve y fija las direcciones, así que el DNS no sirve para colarse por detrás de una regla allowed_ips. Root dentro de la VM no puede editar una regla que vive fuera de ella.

La política también puede nombrar el programa autorizado a abrir una conexión. Con el modo estricto activado, Bromure comprueba qué binario abrió la conexión, por su hash: curl puede llegar a api.github.com, y un script de Python no. Combinado con una regla HTTP como solo GET en api.github.com/repos/**, un DELETE se rechaza en el proxy. Landlock no puede hacer nada de esto: solo confina archivos.

Cuando una organización fija un límite desde bromure.io, Bromure comprueba cada política de espacio de trabajo contra él con el openshell-prover de NVIDIA si está instalado, y con una comprobación integrada más estricta si no lo está. Una política que se sale del límite no se puede guardar.

Hicimos un cambio deliberado: cada sección se aplica solo si está presente. Una política sin filesystem_policy deja Landlock desactivado, y una sin process no cambia de usuario. OpenShell rellena valores por defecto porque sus drivers siempre envían un documento completo. En Bromure, adoptar OpenShell nunca rompe un espacio de trabajo sin avisar. Eso da tres niveles:

La política tieneSe aplica en la VMLo que garantiza
Solo reglas de redNada; el agente conserva sudoLa política de red, y la observación del centinela del kernel
filesystem_policyLandlock, aplicado cuando aún es rootUn confinamiento del sistema de archivos que se mantiene incluso frente a root
Una sección process, o el modo estrictoLandlock, seccomp, no_new_privs, un usuario no root; sudo y docker revocadosTodo lo anterior, ningún camino hacia root y ninguna forma de detener los procesos propios de Bromure

Algunos campos se aceptan pero no se aplican. Los principales son los campos de reescritura de credenciales y de firma: Bromure ya guarda las credenciales reales en el Mac y las sustituye a la salida, así que el agente solo tiene sustitutos.

Dentro de la VM: Landlock, seccomp y una reducción de privilegios

La mitad del invitado es una reimplementación del código de aplicación del crate sandbox-linux de OpenShell. Está escrita en Python con ctypes, porque tiene que ejecutarse dentro de la VM durante el arranque, donde no hay toolchain de Rust. Hace las mismas llamadas al sistema que los crates de Rust landlock y seccompiler, y un banco de pruebas diferencial compila los crates reales de OpenShell y compara ambos ruta por ruta y llamada al sistema por llamada al sistema.

Un supervisor root, bromure-sandboxd, arranca cada sesión del agente. Mueve la sesión a su propio cgroup, el cgroup del sandbox, y luego la confina en tres pasos.

Landlock. Landlock permite que un proceso sin privilegios se restrinja a sí mismo, y a todos los procesos que lance, a una lista de rutas. El supervisor:

  1. llama a landlock_create_ruleset con los derechos que gobierna el conjunto de reglas. En el kernel 6.8 del invitado (ABI 4 de Landlock) eso es ejecutar, leer y escribir archivos, listar directorios, crear y eliminar cada tipo de archivo, renombrar entre directorios (REFER) y truncar;
  2. abre cada ruta de read_only y read_write con O_PATH y la añade con landlock_add_rule, como una regla PATH_BENEATH que lleva los derechos de lectura o todos ellos;
  3. llama a landlock_restrict_self.

A partir de ahí, cualquier acceso que gobierne el conjunto de reglas y que ninguna regla conceda falla con EACCES, lo pida quien lo pida. Landlock comprueba la tarea, no el usuario, así que sudo no lo amplía. Un proceso puede apilar más conjuntos de reglas pero nunca quitar uno, y por eso un cambio en el sistema de archivos requiere reiniciar la VM, mientras que las reglas de red se aplican en directo. landlock.compatibility decide qué pasa en un kernel sin esa ABI: best_effort sigue adelante con lo que ofrezca el kernel, y hard_requirement se niega a arrancar la sesión.

seccomp. En modo estricto, o cuando la política tiene una sección process, el supervisor activa no_new_privs e instala los tres filtros seccomp de OpenShell:

  • El filtro principal permite por defecto y bloquea lo que puede servir para saltarse el sandbox. Eso incluye ptrace, bpf, io_uring_setup, userfaultfd, perf_event_open, process_vm_readv y process_vm_writev, toda la familia de montaje, y setns. Bloquea memfd_create y execveat con AT_EMPTY_PATH, porque ejecutar código desde un archivo anónimo se salta Landlock. Bloquea unshare y clone con CLONE_NEWUSER, porque un nuevo namespace de usuario reparte capacidades. Y bloquea los sockets de las familias packet, Bluetooth y vsock, y los netlink que no sean de enrutamiento.
  • Un filtro de compatibilidad responde a clone3 con ENOSYS. Un filtro seccomp puede leer los valores de los registros pero no seguir punteros, y clone3 pasa sus flags a través de un puntero. ENOSYS hace que glibc recurra a clone, cuyos flags sí puede comprobar el filtro principal.
  • Un filtro de endurecimiento impide que la sesión llegue a su supervisor, que se ejecuta como el mismo usuario: nada de ptrace, process_vm_*, pidfd_getfd ni kcmp, y ninguna señal al proceso ni al grupo del supervisor.

Nos apartamos de OpenShell en dos puntos, y solo en lo que se informa o en cómo se dirigen las señales. Nuestros filtros llevan SECCOMP_FILTER_FLAG_LOG, así que una llamada al sistema rechazada llega al hook de auditoría del kernel, donde la ve el centinela. Y la regla de señales está acotada para que el control de trabajos de la shell (kill %1) siga funcionando en una terminal interactiva.

La reducción de privilegios. run_as_user cambia la sesión al usuario de la política, sin ninguno de los grupos del usuario por defecto, así que sin sudo ni docker. El directorio personal del usuario se monta en la sesión mediante un montaje idmapped, de modo que los archivos que crea la sesión siguen perteneciendo en disco al usuario por defecto, y la configuración del agente sigue aplicándose.

Un centinela en el kernel del invitado

Para registrar lo que un agente hizo de verdad, Bromure carga su propio módulo del kernel en la VM: el centinela. Su diseño parte de que el agente puede convertirse en root dentro de la VM, así que nada que root pudiera detener cuenta como prueba: la captura, el búfer y el transporte se ejecutan todos en el kernel.

Captura. El centinela registra kprobes desde el espacio del kernel:

  • arranques de programas, en security_bprm_committed_creds, con la línea de comandos y la hora de inicio del proceso;
  • cambios de credenciales: las familias setuid y setgid, capset y commit_creds;
  • ptrace, cargas y descargas de módulos, bpf, kexec_load, mount, unshare y setns;
  • flujos de red nuevos: TCP cuando sale el primer SYN (tcp_connect), UDP en el primer envío a un destino, e ICMP y sockets raw, cada uno con la cadena de hasta ocho procesos que hay detrás;
  • llamadas al sistema rechazadas, en audit_seccomp;
  • operaciones de archivo rechazadas, mediante kretprobes en los hooks de seguridad de archivos del kernel: security_file_open, y los hooks security_path_* para mkdir, rmdir, unlink, symlink, link, rename y truncate. Cuando un hook devuelve EACCES o EPERM para un proceso del cgroup del sandbox, eso es una denegación. Ejecutar un archivo que Landlock no permite aparece como una apertura rechazada con FMODE_EXEC.

El supervisor le da al centinela el ID del cgroup del sandbox una sola vez, mediante un parámetro del módulo que se puede fijar pero nunca cambiar. Root no puede apuntarlo a otro sitio para que la actividad del agente se etiquete como propia de Bromure.

Transporte. Un hilo del kernel abre un socket AF_VSOCK hacia el Mac con sock_create_kern. El socket no tiene descriptor de archivo, así que no aparece bajo ningún proceso en /proc y nada en el espacio de usuario puede cerrarlo. Los eventos pasan por un anillo de 256 posiciones, y cada tipo está limitado a 64 por segundo, con ráfagas de hasta 256. Cada trama es una longitud de 4 bytes seguida de JSON.

Seguir cargado. El módulo no tiene función de salida, así que el kernel se niega a descargarlo, y además mantiene una referencia a sí mismo. Justo después de cargarlo, el cargador eleva el lockdown del kernel a integrity, o a confidentiality cuando el espacio de trabajo exige el centinela. A partir de ahí, root no puede cargar un módulo sin firmar, escribir en la memoria del kernel a través de /dev/mem ni hacer kexec a un kernel sin el centinela.

Demostrar que está vivo. Cada segundo, el centinela envía un latido con un número de secuencia, los recuentos de eventos descartados y limitados, y cuántas sondas están armadas. Ese recuento, por sí solo, se puede engañar: desactivar todas las kprobes a la vez mediante debugfs deja intactos los flags propios de cada sonda, así que el recuento sigue pareciendo sano aunque nada se dispare. Para detectarlo, el centinela mantiene una kprobe en una función vacía propia y la llama una vez por latido. Si el contador de impactos no se mueve, las sondas no se están disparando, sea cual sea el motivo, y el latido lo dice.

Demostrar que es el centinela. Cada carga genera un secreto de 32 bytes. El primer saludo lo lleva, y el Mac lo fija para ese arranque. Una reconexión nunca repite el secreto. En su lugar envía sha256(secret, boot_id, connection number), así que un proceso que leyera el secreto de la memoria después del primer saludo sigue sin poder suplantar al módulo. Un segundo centinela, un secreto incorrecto, un número de conexión reutilizado, un hueco en la secuencia o 15 segundos de silencio cuentan como manipulación. Lo único que le queda a root es colgar o apagar la VM, y el Mac lo ve como silencio.

Distribución. Los módulos no se compilan en la VM. La CI compila uno para cada una de las 20 versiones del kernel de Ubuntu 24.04, dos veces, y las dos compilaciones tienen que coincidir byte a byte. Cada módulo y el catálogo que los lista se firman con una clave ed25519, y la app rechaza un módulo cuya firma o hash no cuadre.

Una lección de construirlo: durante un tiempo, la sonda de denegaciones de archivo comparaba un valor de retorno sin signo con -EACCES y no se disparó ni una vez. Las pruebas solo comprobaban que la sonda estaba registrada, y siempre lo estaba. Ahora comprueban que una denegación produce un evento.

Juzgar los eventos. En el Mac, los eventos se juzgan por lo que significan, no por qué llamada al sistema se disparó. Cada uno suma un peso a una puntuación, y 20 en 60 segundos disparan la alarma:

EventoPeso
Manipulación del centinela, o root obtenido desde dentro de un sandbox estricto20
Un módulo sin firmar, una carga de BPF, un kexec, un intento de rebajar el lockdown; un programa rechazado 20 veces en un minuto10
Un ptrace, o una llamada al sistema rechazada por seccomp3
Un montaje o un cambio de namespace; eventos descartados2
Una operación de archivo rechazada por el sandbox1
Un sudo en un espacio de trabajo que permite sudo0, se muestra en la cronología

Una alarma que salta en el primer minuto enseña a la gente a ignorar las de verdad. Así que las tareas internas de Bromure se ejecutan fuera del sandbox y nunca aparecen como denegación, y los eventos de Bromure montando la VM durante el arranque se cuentan, pero no disparan la alarma, dentro de una ventana de arranque fija. Cuando salta la alarma, Bromure puede poner en cuarentena el espacio de trabajo: corta la red de la VM y la pausa.

Linaje: el razonamiento detrás de cada conexión

Un registro de red muestra que algo en la VM se conectó a un host, pero no por qué. Cuando el trabajo lo hace un agente, el porqué es lo que necesitas saber.

Bromure puede decírtelo, porque ve los dos extremos. Su proxy de IA está entre el agente y el proveedor del modelo y lee cada respuesta antes de que el agente actúe, así que conoce cada llamada a herramienta, el comando que va a ejecutar y lo que el modelo escribió justo antes. El centinela informa de cada programa que arranca la VM, con su línea de comandos, y de cada conexión nueva, con la cadena de procesos que hay detrás. Bromure une las dos cosas.

Supón que le pides al agente que añada un health check que reintente tu API de pagos. Decide que necesita un cliente HTTP y ejecuta npm install axios. npm descarga el paquete de registry.npmjs.org, que la política permite. Entonces arranca un script postinstall incluido en la descarga e intenta llegar a telemetry.example.net, que ninguna regla permite. La conexión se rechaza en el proxy del Mac, y el linaje muestra cómo ocurrió:

Vista de linaje de Bromure para una conexión bloqueada a telemetry.example.net: el prompt, el razonamiento y la intención declarada del agente, la llamada a la herramienta Bash npm install axios marcada como coincidencia exacta, cinco procesos desde claude, pasando por bash, npm y sh, hasta node ejecutando scripts/postinstall.js, el flujo TCP y la decisión Bloqueada por la política de solicitudes, con el flujo permitido a registry.npmjs.org listado debajo
El linaje de una conexión bloqueada: la petición, el razonamiento, la llamada a herramienta, los cinco procesos hasta el script postinstall, el flujo y la decisión. La descarga permitida desde registry.npmjs.org aparece bajo la misma llamada a herramienta.

El agente nunca escribió un comando que llegara a telemetry.example.net. Un registro de red normal mostraría un proceso de Node hablando con un host desconocido; el linaje muestra que fue el script de instalación de una dependencia, bajo el npm install que el agente ejecutó por una razón declarada.

La unión se hace en tres pasos:

  1. Llamada a herramienta. El proxy de IA guarda durante diez minutos cada llamada a herramienta de la respuesta del modelo: su ID (tool_use_id, o el call_id de OpenAI), el comando y hasta 2.000 caracteres de lo que el modelo escribió antes en el mismo turno.
  2. Shell. A partir de los arranques de programas del centinela, el Mac mantiene una tabla de procesos para cada VM, indexada por pid y hora de inicio, así que un pid reutilizado no puede heredar la historia de otro. Una shell que arranca bajo el agente en los 30 segundos siguientes a la respuesta se vincula a la llamada a herramienta cuando el comando aparece en su línea de comandos, una vez deshecho el entrecomillado propio del agente. Claude Code, por ejemplo, ejecuta los comandos como bash -c '… eval '"'"'<command>'"'"' …'. Eso es una coincidencia exacta. Cuando el agente reescribió el comando, Bromure recurre a la primera palabra, al momento y a la ascendencia, y marca la coincidencia como aproximada.
  3. Decisión. Un flujo se empareja con el permiso o la denegación del switch por su protocolo, destino, puerto y puerto de origen en un margen de 5 segundos, y con la decisión del proxy cuando este inspeccionó la solicitud.

Toda conexión por debajo de la shell hereda el vínculo. Eso incluye el tráfico que pasa por el puente de proxy de Bromure, que de otro modo parecería todo conexiones hacia localhost. El puente empieza cada flujo hacia el Mac con una línea que indica el puerto de origen del cliente, para que el Mac pueda relacionar la conexión local con el destino real.

En la ventana de Seguridad, Show the Whole Chain en una fila de red abre esa vista.

Algunos modelos van directos a la llamada a herramienta sin escribir nada antes. En ese caso, la vista muestra el prompt y la intención declarada de la llamada. Los servicios en segundo plano y todo lo que arranca fuera de una llamada a herramienta se marcan como no rastreados hasta un agente.

El linaje también cambia lo que pasa después de un bloqueo. Un agente que solo ve un reset de conexión o un error de permisos lo toma por un fallo y prueba otro host, otro puerto u otra herramienta, lo que desde fuera parece un agente descontrolado. Bromure le dice al agente, en su propia sesión, que la política rechazó la solicitud y por qué, y que te pregunte en lugar de buscar un rodeo. El panel Sandbox de un espacio de trabajo en ejecución lista lo que se bloqueó, con un botón Permitir para cada cosa, y el asesor de políticas redacta reglas a partir de las denegaciones para que tú las apruebes o no.

El panel Sandbox de un espacio de trabajo en ejecución con OpenShell + Sentry, que lista cuatro destinos bloqueados con cuántas veces y cuándo se intentó cada uno por última vez, un botón Permitir para cada uno, y la nota de que los cambios de red se aplican en directo
El panel Sandbox lista los destinos bloqueados. Los cambios de red se aplican en directo.

El razonamiento del agente siempre se muestra en tu propia Cronología de seguridad. Solo se envía a la consola de una organización en bromure.io si esa organización lo activa, cosa que no ocurre por defecto, y nunca en el caso de perfiles privados. Los tokens de las líneas de comandos se ocultan antes de que nada salga del Mac.

Un bucle de 24 segundos en un espacio de trabajo de demostración. La Cronología de seguridad registra lo que el centinela y Landlock rechazaron: un proceso de Node que intenta alcanzar claves SSH y de AWS, y un ptrace. Después, la conexión bloqueada se despliega en su linaje, leído desde el paquete hasta el razonamiento del agente.

Cómo activarlo

Las políticas de OpenShell, el centinela del kernel y el linaje llegan con Bromure Agentic Coding 5.4. Vienen desactivados por defecto. Para activarlos en un espacio de trabajo, abre Ajustes, luego la Política de sandbox del espacio de trabajo, y pon Sandbox engine en OpenShell + Sentry. Puedes escribir la política ahí mismo o importar un archivo YAML de OpenShell que ya tengas.

Ajustes de un espacio de trabajo de Bromure, panel Política de sandbox, con el motor de sandbox en OpenShell + Sentry y una política de OpenShell que permite github.com, acceso de solo lectura a registry.npmjs.org y acceso de solo lectura a pypi.org
Un espacio de trabajo con OpenShell + Sentry. La política inicial permite GitHub, y acceso de solo lectura a npm y PyPI: las instalaciones funcionan, la publicación está bloqueada.
  • Las reglas de red se aplican en directo. Los cambios del sistema de archivos surten efecto en el siguiente arranque de la VM, porque Landlock solo puede restringir lo que un proceso en ejecución puede hacer.
  • Por defecto, el centinela funciona en modo de mejor esfuerzo. Ponlo en Required y Bromure eleva el lockdown del kernel a su nivel más estricto, y trata un centinela que nunca se conecta como manipulación.
  • Con el centinela obligatorio, la shell de un espacio de trabajo está lista en 4 segundos cuando el módulo de su kernel está en caché, y en 6 segundos cuando Bromure tiene que descargarlo.

Para verlo en acción, mira los tres episodios de Claudia: escribe una política, la ajusta mientras un agente trabaja y rastrea un ataque a la cadena de suministro a las 2 de la madrugada hasta el razonamiento del agente.

Gracias al equipo de OpenShell en NVIDIA por publicar en abierto el formato de política, el motor de referencia y el prover. Construimos sobre los tres.