Les politiques OpenShell dans Bromure, et le raisonnement derrière chaque connexion
OpenShell, de NVIDIA, décrit dans un seul fichier de politique ce qu'un agent de codage a le droit de toucher. Bromure Agentic Coding 5.4 applique ces politiques dans la VM de l'agent, avec Landlock et seccomp à l'intérieur et les règles réseau sur votre Mac, et surveille le noyau invité avec un module sentinelle. Il relie ensuite chaque connexion réseau aux processus qui l'ont ouverte, à l'appel d'outil qui les a lancés et au raisonnement que l'agent a donné pour cet appel.
Un agent de codage a besoin d'un vrai accès pour être utile, et c'est la politique qui en fixe l'étendue. Elle fonctionne mieux sous la forme d'un seul document que l'on peut lire, vérifier et appliquer, plutôt que de réglages éparpillés entre un pare-feu, des options de montage et la configuration de l'agent lui-même.
OpenShell, de NVIDIA
OpenShell est le runtime open source de NVIDIA pour les agents, publié sous licence Apache 2.0. Nous nous sommes appuyés sur son format de politique, où un seul document YAML couvre quatre domaines :
filesystem_policyetlandlock: les chemins que l'agent peut lire et écrire, appliqués par le module de sécurité Landlock du noyau Linux.process: l'utilisateur et le groupe sous lesquels l'agent s'exécute.network_policies: les destinations que chaque programme peut joindre, et les requêtes qu'il peut y envoyer, jusqu'à la méthode et au chemin HTTP. Tout ce qui n'est pas autorisé est refusé : « OpenShell refuse toute connexion sortante d'un bac à sable, sauf si une règle de network_policies l'autorise. »
OpenShell fournit aussi un prouveur qui utilise un solveur SMT pour vérifier si une politique autorise quoi que ce soit au-delà d'une autre. Une organisation peut fixer une limite et obtenir un oui ou un non à chaque modification, au lieu de lire des diffs.
C'est une conception soignée, due à des gens qui ont manifestement étudié la façon dont les agents dérapent, et elle est ouverte : nous sommes donc partis de là.
OpenShell dans Bromure
Dans Bromure Agentic Coding 5.4, un espace de travail peut utiliser une
politique OpenShell à la place des règles de pare-feu propres à Bromure. Nous
avons repris le format de politique et sa sémantique, pas le code : le moteur
est le nôtre, sur le Mac, avec nos propres composants dans l'invité Linux. Pour
nous assurer que les deux concordent, nous avons fait tourner notre moteur et
le moteur de référence d'OpenShell sur 125 666 cas générés, couvrant les règles
réseau, les règles HTTP, MCP, GraphQL, WebSocket et les vérifications d'adresse
de destination. Ils n'ont divergé sur aucun. Ces tests ont fait apparaître des
astuces de chemin comme ..%3b et les barres obliques doublement encodées
(%252F), que nous traitons désormais comme OpenShell.
Bromure se distingue d'OpenShell par l'endroit où la politique est appliquée. OpenShell lance ses bacs à sable via une passerelle et un runtime de conteneurs. Bromure exécute chaque espace de travail dans sa propre VM Linux jetable sur votre Mac, la politique est donc répartie entre les deux côtés :
- Dans la VM : Landlock pour le système de fichiers, seccomp,
no_new_privset le passage à l'utilisateur de la politique. La section suivante explique comment. - Sur le Mac : les règles réseau. Chaque connexion quitte la VM par le
commutateur virtuel de Bromure, qui vérifie la destination et le port, et par
son proxy TLS, qui vérifie les méthodes et chemins HTTP ainsi que les
messages MCP, GraphQL et WebSocket. Le Mac résout et épingle les adresses, si
bien que le DNS ne peut pas servir à contourner une règle
allowed_ips. Root dans la VM ne peut pas modifier une règle qui vit en dehors d'elle.
La politique peut aussi désigner le programme autorisé à établir une
connexion. Avec le mode strict activé, Bromure vérifie quel binaire a ouvert la
connexion, par son hash : curl peut joindre api.github.com, un script
Python ne le peut pas. Combiné à une règle HTTP comme GET uniquement sur
api.github.com/repos/**, un DELETE est refusé au niveau du proxy. Landlock
ne peut rien faire de tout cela : il ne confine que les fichiers.
Quand une organisation fixe une limite depuis bromure.io, Bromure vérifie
chaque politique d'espace de travail par rapport à elle avec
l'openshell-prover de NVIDIA s'il est installé, et avec une vérification
intégrée plus stricte sinon. Une politique qui sort de la limite ne peut pas
être enregistrée.
Nous avons fait un changement délibéré : chaque section ne s'applique que si
elle est présente. Une politique sans filesystem_policy laisse Landlock
désactivé, et une politique sans process ne change pas d'utilisateur.
OpenShell remplit des valeurs par défaut parce que ses pilotes envoient
toujours un document complet. Dans Bromure, adopter OpenShell ne casse jamais
un espace de travail en silence. Cela donne trois niveaux :
| La politique contient | Appliqué dans la VM | Ce qui est garanti |
|---|---|---|
| Uniquement des règles réseau | Rien ; l'agent garde sudo | La politique réseau, et l'observation par la sentinelle noyau |
filesystem_policy | Landlock, appliqué tant qu'on est encore root | Un confinement du système de fichiers qui tient même face à root |
Une section process, ou le mode strict | Landlock, seccomp, no_new_privs, un utilisateur non root ; sudo et docker retirés | Tout ce qui précède, aucun chemin vers root, et aucun moyen d'arrêter les processus de Bromure |
Quelques champs sont acceptés sans être appliqués. Les principaux sont les champs de réécriture et de signature des identifiants : Bromure garde déjà les vrais identifiants sur le Mac et les substitue à la sortie, si bien que l'agent ne détient jamais que des substituts.
Dans la VM : Landlock, seccomp et un abandon de privilèges
La partie invitée est une réimplémentation du code d'application de la crate
sandbox-linux d'OpenShell. Elle est écrite en Python avec ctypes, parce
qu'elle doit s'exécuter dans la VM au démarrage, où il n'y a pas de chaîne
d'outils Rust. Elle fait les mêmes appels système que les crates Rust
landlock et seccompiler, et un banc de test différentiel compile les vraies
crates d'OpenShell et compare les deux chemin par chemin et appel système par
appel système.
Un superviseur root, bromure-sandboxd, lance chaque session d'agent. Il place
la session dans son propre cgroup, le cgroup du bac à sable, puis la confine en
trois étapes.
Landlock. Landlock permet à un processus non privilégié de se restreindre lui-même, ainsi que tous les processus qu'il lance, à une liste de chemins. Le superviseur :
- appelle
landlock_create_rulesetavec les droits que régit le jeu de règles. Sur le noyau 6.8 de l'invité (ABI Landlock 4), ce sont l'exécution, la lecture et l'écriture de fichiers, le listage des répertoires, la création et la suppression de chaque type de fichier, le renommage d'un répertoire à l'autre (REFER) et la troncature ; - ouvre chaque chemin de
read_onlyetread_writeavecO_PATHet l'ajoute aveclandlock_add_rule, sous forme de règlePATH_BENEATHportant les droits de lecture ou la totalité des droits ; - appelle
landlock_restrict_self.
Dès lors, tout accès que régit le jeu de règles et qu'aucune règle n'accorde
échoue avec EACCES, quel que soit le demandeur. Landlock vérifie la tâche,
pas l'utilisateur, donc sudo ne l'élargit pas. Un processus peut empiler
d'autres jeux de règles, mais jamais en retirer un, et c'est pourquoi un
changement sur le système de fichiers exige un redémarrage de la VM alors que
les règles réseau s'appliquent à chaud. landlock.compatibility décide de ce
qui se passe sur un noyau qui n'a pas l'ABI : best_effort continue avec ce
que le noyau propose, et hard_requirement refuse de lancer la session.
seccomp. En mode strict, ou quand la politique a une section process, le
superviseur active no_new_privs et installe les trois filtres seccomp
d'OpenShell :
- Le filtre principal autorise par défaut et bloque ce qui permet de
contourner le bac à sable. Cela inclut
ptrace,bpf,io_uring_setup,userfaultfd,perf_event_open,process_vm_readvetprocess_vm_writev, toute la famille des montages, etsetns. Il bloquememfd_createetexecveatavecAT_EMPTY_PATH, parce qu'exécuter du code depuis un fichier anonyme contourne Landlock. Il bloqueunshareetcloneavecCLONE_NEWUSER, parce qu'un nouvel espace de noms utilisateur distribue des capacités. Il bloque enfin les sockets des familles packet, Bluetooth et vsock, ainsi que netlink hors routage. - Un filtre de compatibilité répond à
clone3parENOSYS. Un filtre seccomp peut lire la valeur des registres, mais pas suivre des pointeurs, etclone3passe ses drapeaux par un pointeur.ENOSYSpousse la glibc à se rabattre surclone, dont le filtre principal peut vérifier les drapeaux. - Un filtre de durcissement empêche la session d'atteindre son superviseur, qui
s'exécute sous le même utilisateur : pas de
ptrace,process_vm_*,pidfd_getfdnikcmp, et aucun signal vers le processus ou le groupe du superviseur.
Nous nous écartons d'OpenShell à deux endroits, et uniquement sur ce qui est
signalé ou sur la cible des signaux. Nos filtres portent
SECCOMP_FILTER_FLAG_LOG, si bien qu'un appel système refusé atteint le point
d'audit du noyau, où la sentinelle le voit. Et la règle sur les signaux est
resserrée pour que le contrôle des tâches du shell (kill %1) fonctionne
toujours dans un terminal interactif.
L'abandon de privilèges. run_as_user fait passer la session sous
l'utilisateur de la politique, sans aucun des groupes de l'utilisateur par
défaut : donc ni sudo ni docker. Le répertoire personnel de l'utilisateur
est monté dans la session via un montage idmappé, si bien que les fichiers
créés par la session appartiennent toujours à l'utilisateur par défaut sur le
disque, et que la configuration de l'agent s'applique toujours.
Une sentinelle dans le noyau invité
Pour enregistrer ce qu'un agent a réellement fait, Bromure charge dans la VM son propre module noyau, la sentinelle. Sa conception part du principe que l'agent peut devenir root dans la VM : rien de ce que root pourrait arrêter ne compte comme preuve, et la capture, la mise en mémoire tampon et le transport s'exécutent tous dans le noyau.
Capture. La sentinelle enregistre des kprobes depuis l'espace noyau :
- les lancements de programmes, sur
security_bprm_committed_creds, avec la ligne de commande et l'heure de démarrage du processus ; - les changements d'identité : les familles
setuidetsetgid,capsetetcommit_creds; ptrace, les chargements et déchargements de modules,bpf,kexec_load,mount,unshareetsetns;- les nouveaux flux réseau : TCP à l'envoi du premier SYN (
tcp_connect), UDP au premier envoi vers une destination, ainsi que ICMP et les sockets bruts, chacun avec la chaîne d'au plus huit processus qui en est à l'origine ; - les appels système refusés, sur
audit_seccomp; - les opérations sur fichier refusées, via des kretprobes sur les points de
sécurité fichier du noyau :
security_file_open, et les pointssecurity_path_*pour mkdir, rmdir, unlink, symlink, link, rename et truncate. Quand l'un d'eux renvoieEACCESouEPERMpour un processus du cgroup du bac à sable, c'est un refus. L'exécution d'un fichier que Landlock n'autorise pas apparaît comme une ouverture refusée avecFMODE_EXEC.
Le superviseur donne une seule fois à la sentinelle l'identifiant du cgroup du bac à sable, par un paramètre de module que l'on peut définir mais jamais modifier. Root ne peut pas le faire pointer ailleurs pour que l'activité de l'agent soit étiquetée comme celle de Bromure.
Transport. Un thread noyau ouvre un socket AF_VSOCK vers le Mac avec
sock_create_kern. Le socket n'a pas de descripteur de fichier : il
n'apparaît sous aucun processus dans /proc et rien dans l'espace utilisateur
ne peut le fermer. Les événements passent par un anneau de 256 emplacements, et
chaque type est limité à 64 par seconde, avec des rafales jusqu'à 256. Chaque
trame est une longueur sur 4 octets suivie de JSON.
Rester chargée. Le module n'a pas de fonction de sortie, donc le noyau
refuse de le décharger, et il détient en plus une référence sur lui-même. Juste
après son chargement, le chargeur relève le verrouillage du noyau à
integrity, ou à confidentiality quand l'espace de travail exige la
sentinelle. Dès lors, root ne peut plus charger de module non signé, écrire
dans la mémoire du noyau via /dev/mem ni faire un kexec vers un noyau sans
la sentinelle.
Prouver qu'elle est en vie. Chaque seconde, la sentinelle envoie un battement de cœur avec un numéro de séquence, le nombre d'événements perdus et limités, et le nombre de sondes armées. Ce nombre seul peut être trompé : désactiver toutes les kprobes d'un coup via debugfs laisse intacts les drapeaux propres à chaque sonde, et le compte paraît sain alors que plus rien ne se déclenche. Pour le détecter, la sentinelle garde une kprobe sur une fonction vide qui lui appartient et l'appelle une fois par battement de cœur. Si le compteur de déclenchements ne bouge pas, les sondes ne se déclenchent pas, quelle qu'en soit la raison, et le battement de cœur le signale.
Prouver qu'elle est la sentinelle. Chaque chargement génère un secret de
32 octets. Le premier message de salutation le transporte, et le Mac l'épingle
pour ce démarrage. Une reconnexion ne répète jamais le secret. Elle envoie à la
place sha256(secret, boot_id, connection number), si bien qu'un processus qui
aurait lu le secret en mémoire après la première salutation ne peut toujours
pas se faire passer pour le module. Une deuxième sentinelle, un mauvais secret,
un numéro de connexion réutilisé, un trou dans la séquence ou 15 secondes de
silence comptent tous comme une altération. Il ne reste à root que la
possibilité de faire planter ou d'éteindre la VM, ce que le Mac voit comme un
silence.
Distribution. Les modules ne sont pas compilés dans la VM. La CI en construit un pour chacune des 20 versions du noyau Ubuntu 24.04, deux fois, et les deux builds doivent être identiques à l'octet près. Chaque module et le catalogue qui les liste sont signés avec une clé ed25519, et l'application refuse un module dont la signature ou le hash ne correspond pas.
Une leçon tirée de sa construction : pendant un temps, la sonde des refus sur
fichier comparait une valeur de retour non signée à -EACCES et ne s'est
jamais déclenchée une seule fois. Les tests vérifiaient seulement que la sonde
était enregistrée, ce qu'elle était toujours. Ils vérifient désormais qu'un
refus produit un événement.
Juger les événements. Sur le Mac, les événements sont jugés sur ce qu'ils signifient, pas sur l'appel système qui s'est déclenché. Chacun ajoute un poids à un score, et 20 en 60 secondes déclenchent l'alarme :
| Événement | Poids |
|---|---|
| Une altération de la sentinelle, ou root obtenu depuis un bac à sable strict | 20 |
Un module non signé, un chargement BPF, un kexec, une tentative d'abaisser le verrouillage ; un même programme refusé 20 fois en une minute | 10 |
Un ptrace, ou un appel système refusé par seccomp | 3 |
| Un montage ou un changement d'espace de noms ; des événements perdus | 2 |
| Une opération sur fichier refusée par le bac à sable | 1 |
Un sudo dans un espace de travail qui autorise sudo | 0, affiché dans la chronologie |
Une alarme qui se déclenche dès la première minute apprend aux gens à ignorer les vraies. Les tâches de maintenance de Bromure s'exécutent donc hors du bac à sable et n'apparaissent jamais comme des refus, et les événements liés à l'assemblage de la VM par Bromure au démarrage sont comptés, sans déclencher d'alarme, pendant une fenêtre de démarrage fixe. Quand l'alarme se déclenche, Bromure peut mettre l'espace de travail en quarantaine : il coupe le réseau de la VM et la met en pause.
La lignée : le raisonnement derrière chaque connexion
Un journal réseau montre que quelque chose dans la VM s'est connecté à un hôte, mais pas pourquoi. Quand c'est un agent qui fait le travail, c'est le pourquoi qu'il vous faut connaître.
Bromure peut vous le dire, parce qu'il voit les deux bouts. Son proxy IA se place entre l'agent et le fournisseur du modèle et lit chaque réponse avant que l'agent n'agisse : il connaît donc chaque appel d'outil, la commande qu'il va exécuter et ce que le modèle a écrit juste avant. La sentinelle signale chaque programme lancé dans la VM, avec sa ligne de commande, et chaque nouvelle connexion, avec la chaîne de processus qui en est à l'origine. Bromure relie les deux.
Imaginons que vous demandiez à l'agent d'ajouter un contrôle de santé qui
relance les requêtes vers votre API de paiement. Il décide qu'il lui faut un
client HTTP et exécute npm install axios. npm récupère le paquet sur
registry.npmjs.org, ce que la politique autorise. Puis un script postinstall
contenu dans le téléchargement démarre et tente de joindre
telemetry.example.net, ce qu'aucune règle n'autorise. La connexion est refusée
au niveau du proxy sur le Mac, et la lignée montre comment c'est arrivé :


L'agent n'a jamais tapé de commande qui ait joint telemetry.example.net. Un
simple journal réseau montrerait un processus Node parlant à un hôte inconnu ;
la lignée montre qu'il s'agissait du script d'installation d'une dépendance,
sous le npm install que l'agent a lancé pour une raison qu'il a donnée.
Le rattachement se fait en trois étapes :
- Appel d'outil. Le proxy IA conserve pendant dix minutes chaque appel
d'outil de la réponse du modèle : son identifiant (
tool_use_id, ou lecall_idd'OpenAI), la commande, et jusqu'à 2 000 caractères de ce que le modèle a écrit avant lui dans le même tour. - Shell. À partir des lancements de programmes signalés par la
sentinelle, le Mac tient une table des processus pour chaque VM, indexée par
pid et heure de démarrage, si bien qu'un pid réutilisé ne peut pas hériter
de l'historique d'un autre. Un shell lancé sous l'agent dans les 30 secondes
qui suivent la réponse est rattaché à l'appel d'outil quand la commande
apparaît dans sa ligne de commande, une fois défait l'échappement propre à
l'agent. Claude Code, par exemple, exécute les commandes sous la forme
bash -c '… eval '"'"'<command>'"'"' …'. C'est une correspondance exacte. Quand l'agent a réécrit la commande, Bromure se rabat sur le premier mot, la chronologie et l'ascendance, et marque la correspondance comme approximative. - Décision. Un flux est associé à l'autorisation ou au refus du commutateur d'après son protocole, sa destination, son port et son port source, dans une fenêtre de 5 secondes, et à la décision du proxy quand celui-ci a inspecté la requête.
Chaque connexion sous le shell hérite du lien. Cela inclut le trafic qui passe par le pont proxy de Bromure, qui autrement ressemblerait entièrement à des connexions vers localhost. Le pont commence chaque flux vers le Mac par une ligne indiquant le port source du client, ce qui permet au Mac de faire correspondre la connexion locale à la vraie destination.
Dans la fenêtre Sécurité, Show the Whole Chain sur une ligne réseau ouvre cette vue.
Certains modèles passent directement à l'appel d'outil sans rien écrire avant. La vue montre alors le prompt et l'intention déclarée de l'appel. Les services d'arrière-plan et tout ce qui a été lancé en dehors d'un appel d'outil sont marqués comme non rattachés à un agent.
La lignée change aussi ce qui se passe après un blocage. Un agent qui ne voit qu'une connexion réinitialisée ou une erreur de permission y voit une panne et essaie un autre hôte, un autre port ou un autre outil, ce qui, vu de l'extérieur, ressemble à un agent qui part en vrille. Bromure dit à l'agent, dans sa propre session, que la politique a refusé la requête et pourquoi, et qu'il doit vous demander plutôt que de la contourner. Le tiroir Sandbox d'un espace de travail en cours d'exécution liste ce qui a été bloqué, avec un bouton Autoriser pour chaque entrée, et le conseiller de politique rédige des règles à partir des refus, que vous approuvez ou non.


Le raisonnement de l'agent est toujours affiché dans votre propre Chronologie de sécurité. Il n'est envoyé à la console d'une organisation sur bromure.io que si cette organisation l'active, ce qui n'est pas le cas par défaut, et jamais pour les profils privés. Les jetons présents dans les lignes de commande sont masqués avant que quoi que ce soit ne quitte le Mac.
L'activer
Les politiques OpenShell, la sentinelle noyau et la lignée arrivent dans Bromure Agentic Coding 5.4. Elles sont désactivées par défaut. Pour les activer sur un espace de travail, ouvrez les Réglages, puis la Politique du bac à sable de l'espace de travail, et réglez Sandbox engine sur OpenShell + Sentry. Vous pouvez y écrire la politique, ou importer un fichier YAML OpenShell existant.


- Les règles réseau s'appliquent à chaud. Les changements sur le système de fichiers prennent effet au prochain démarrage de la VM, parce que Landlock ne peut que restreindre ce qu'un processus en cours d'exécution a le droit de faire.
- Par défaut, la sentinelle fonctionne en mode « au mieux ». Réglez-la sur Required et Bromure relève le verrouillage du noyau à son niveau le plus strict, et traite une sentinelle qui ne se connecte jamais comme une altération.
- Avec la sentinelle obligatoire, le shell d'un espace de travail est prêt en 4 secondes quand le module de son noyau est en cache, et en 6 secondes quand Bromure doit le télécharger.
Pour la voir à l'œuvre, regardez les trois épisodes de Claudia : elle écrit une politique, l'ajuste pendant qu'un agent travaille, et remonte une attaque de la chaîne d'approvisionnement survenue à 2 heures du matin jusqu'au raisonnement de l'agent.
Merci à l'équipe OpenShell de NVIDIA d'avoir publié en open source le format de politique, le moteur de référence et le prouveur. Nous nous sommes appuyés sur les trois.