← Voltar para todas as publicações
Publicado em · por Renaud Deraison

Políticas OpenShell no Bromure, e o raciocínio por trás de cada conexão

O OpenShell da NVIDIA descreve, num único arquivo de política, o que um agente de codificação pode tocar. O Bromure Agentic Coding 5.4 aplica essas políticas na VM do agente, com Landlock e seccomp do lado de dentro e as regras de rede no seu Mac, e vigia o kernel do convidado com um módulo sentinela. Depois, liga cada conexão de rede aos processos que a abriram, à chamada de ferramenta que os iniciou e ao raciocínio que o agente deu para essa chamada.

Um agente de codificação precisa de acesso real para ser útil, e uma política diz quanto. Ela funciona melhor como um documento único, que você consegue ler, verificar e aplicar, em vez de configurações espalhadas entre um firewall, opções de montagem e a configuração do próprio agente.

O OpenShell da NVIDIA

O OpenShell é o runtime de código aberto da NVIDIA para agentes, publicado sob a licença Apache 2.0. Construímos sobre o formato de política dele, em que um único documento YAML cobre quatro áreas:

  • filesystem_policy e landlock: quais caminhos o agente pode ler e escrever, aplicado pelo módulo de segurança Landlock do kernel Linux.
  • process: com qual usuário e grupo o agente roda.
  • network_policies: quais destinos cada programa pode alcançar, e quais requisições pode enviar a eles, até o método e o caminho HTTP. O que não é permitido é negado: “O OpenShell nega toda conexão de saída de uma sandbox, a menos que uma regra em network_policies a permita.”

O OpenShell também traz um provador que usa um solucionador SMT para verificar se uma política permite algo além do que outra permite. Uma organização pode definir um limite e receber um sim ou não para cada mudança, em vez de ler diffs.

É um projeto cuidadoso, de gente que claramente estudou como agentes falham, e é aberto, então partimos dele.

O OpenShell no Bromure

No Bromure Agentic Coding 5.4, um workspace pode usar uma política OpenShell no lugar das regras de firewall do próprio Bromure. Adotamos o formato da política e a semântica dela, não o código: o motor é nosso, no Mac, mais componentes nossos no convidado Linux. Para garantir que os dois concordam, rodamos o nosso motor e o motor de referência do OpenShell em 125.666 casos gerados, cobrindo regras de rede, regras HTTP, MCP, GraphQL, WebSocket e verificações de endereço de destino. Eles não discordaram em nenhum. Esses testes revelaram truques de caminho como ..%3b e barras com codificação dupla (%252F), que agora tratamos do mesmo jeito que o OpenShell.

O Bromure difere do OpenShell em onde a aplicação acontece. O OpenShell inicia sandboxes por meio de um gateway e de um runtime de contêineres. O Bromure roda cada workspace em sua própria VM Linux descartável no seu Mac, então a política se divide entre os dois lados:

  • Na VM: o Landlock para o sistema de arquivos, seccomp, no_new_privs e a troca para o usuário da política. A próxima seção explica como.
  • No Mac: as regras de rede. Toda conexão sai da VM pelo switch virtual do Bromure, que verifica o destino e a porta, e pelo proxy TLS dele, que verifica métodos e caminhos HTTP e mensagens MCP, GraphQL e WebSocket. O Mac resolve e fixa os endereços, então o DNS não pode ser usado para escapar de uma regra allowed_ips. O root dentro da VM não consegue editar uma regra que vive fora dela.

A política também pode nomear o programa autorizado a fazer uma conexão. Com o modo estrito ligado, o Bromure verifica, por hash, qual binário abriu a conexão: o curl pode alcançar api.github.com, e um script Python não pode. Combinado com uma regra HTTP como só GET em api.github.com/repos/**, um DELETE é recusado no proxy. O Landlock não consegue fazer nada disso: ele só confina arquivos.

Quando uma organização define um limite pelo bromure.io, o Bromure verifica cada política de workspace contra ele com o openshell-prover da NVIDIA, quando ele está instalado, e com uma verificação embutida mais rígida quando não está. Uma política que sai do limite não pode ser salva.

Fizemos uma mudança deliberada: cada seção só se aplica se estiver presente. Uma política sem filesystem_policy deixa o Landlock desligado, e uma sem process não troca de usuário. O OpenShell preenche valores padrão porque os drivers dele sempre enviam um documento completo. No Bromure, adotar o OpenShell nunca quebra um workspace em silêncio. Isso dá três níveis:

A política temAplicado na VMO que garante
Só regras de redeNada; o agente mantém o sudoA política de rede, e a observação pela sentinela do kernel
filesystem_policyLandlock, aplicado ainda como rootConfinamento do sistema de arquivos que vale até contra o root
Uma seção process, ou o modo estritoLandlock, seccomp, no_new_privs, um usuário não root; sudo e docker revogadosTudo o que está acima, nenhum caminho até o root e nenhum jeito de parar os processos do próprio Bromure

Alguns campos são aceitos e não são aplicados. Os principais são os campos de reescrita de credenciais e de assinatura: o Bromure já mantém as credenciais reais no Mac e as troca na saída, então o agente só tem substitutos nas mãos.

Dentro da VM: Landlock, seccomp e perda de privilégios

A metade do lado do convidado é uma reimplementação do código de aplicação do crate sandbox-linux do OpenShell. Ela é escrita em Python com ctypes, porque precisa rodar dentro da VM durante o boot, onde não há toolchain Rust. Ela faz as mesmas chamadas de sistema que os crates Rust landlock e seccompiler fazem, e um harness diferencial compila os crates reais do OpenShell e compara os dois caminho por caminho e chamada de sistema por chamada de sistema.

Um supervisor root, o bromure-sandboxd, inicia cada sessão do agente. Ele move a sessão para um cgroup próprio, o cgroup da sandbox, e depois a confina em três etapas.

Landlock. O Landlock permite que um processo sem privilégios restrinja a si mesmo, e a todo processo que ele iniciar, a uma lista de caminhos. O supervisor:

  1. chama landlock_create_ruleset com os direitos que o conjunto de regras controla. No kernel 6.8 do convidado (ABI 4 do Landlock), isso significa executar, ler e escrever arquivos, listar diretórios, criar e remover cada tipo de arquivo, renomear entre diretórios (REFER) e truncar;
  2. abre cada caminho de read_only e read_write com O_PATH e o adiciona com landlock_add_rule, como uma regra PATH_BENEATH que carrega os direitos de leitura ou todos eles;
  3. chama landlock_restrict_self.

A partir daí, qualquer acesso que o conjunto de regras controla e que nenhuma regra concede falha com EACCES, seja quem for que peça. O Landlock verifica a tarefa, não o usuário, então o sudo não o amplia. Um processo pode empilhar mais conjuntos de regras, mas nunca remover um, e é por isso que uma mudança no sistema de arquivos exige reiniciar a VM, enquanto as regras de rede se aplicam na hora. landlock.compatibility decide o que acontece num kernel sem a ABI: best_effort segue com o que o kernel oferece, e hard_requirement se recusa a iniciar a sessão.

seccomp. No modo estrito, ou quando a política tem uma seção process, o supervisor define no_new_privs e instala os três filtros seccomp do OpenShell:

  • O filtro principal permite por padrão e bloqueia o que pode contornar a sandbox. Isso inclui ptrace, bpf, io_uring_setup, userfaultfd, perf_event_open, process_vm_readv e process_vm_writev, toda a família mount, e setns. Ele bloqueia memfd_create e execveat com AT_EMPTY_PATH, porque rodar código a partir de um arquivo anônimo contorna o Landlock. Ele bloqueia unshare e clone com CLONE_NEWUSER, porque um novo namespace de usuário distribui capabilities. E ele bloqueia sockets das famílias packet, Bluetooth e vsock, e netlink que não seja de roteamento.
  • Um filtro de compatibilidade responde a clone3 com ENOSYS. Um filtro seccomp consegue ler valores de registradores, mas não seguir ponteiros, e o clone3 passa as flags por um ponteiro. O ENOSYS faz a glibc recorrer ao clone, cujas flags o filtro principal consegue verificar.
  • Um filtro de endurecimento impede a sessão de alcançar o seu supervisor, que roda como o mesmo usuário: nada de ptrace, process_vm_*, pidfd_getfd ou kcmp, e nenhum sinal para o processo ou o grupo do supervisor.

Divergimos do OpenShell duas vezes, e só no que é relatado ou em como os sinais são direcionados. Nossos filtros levam SECCOMP_FILTER_FLAG_LOG, então uma chamada de sistema recusada chega ao hook de auditoria do kernel, onde a sentinela a vê. E a regra de sinais foi estreitada para que o controle de jobs do shell (kill %1) continue funcionando num terminal interativo.

A perda de privilégios. run_as_user troca a sessão para o usuário da política, sem nenhum dos grupos do usuário padrão, então nada de sudo e nada de docker. A home do usuário é montada na sessão por uma montagem idmapped, então os arquivos que a sessão cria continuam pertencendo, no disco, ao usuário padrão, e a configuração do agente continua valendo.

Uma sentinela no kernel do convidado

Para registrar o que um agente de fato fez, o Bromure carrega na VM um módulo de kernel próprio, a sentinela. O projeto dela parte do princípio de que o agente pode virar root dentro da VM, então nada que o root consiga parar conta como prova: a captura, o buffer e o transporte rodam todos no kernel.

Captura. A sentinela registra kprobes a partir do espaço do kernel:

  • inícios de programas, em security_bprm_committed_creds, com a linha de comando e o horário de início do processo;
  • mudanças de credenciais: as famílias setuid e setgid, capset e commit_creds;
  • ptrace, carregamentos e descarregamentos de módulos, bpf, kexec_load, mount, unshare e setns;
  • novos fluxos de rede: TCP quando o primeiro SYN sai (tcp_connect), UDP no primeiro envio para um destino, e ICMP e sockets raw, cada um com a cadeia de até oito processos por trás dele;
  • chamadas de sistema recusadas, em audit_seccomp;
  • operações de arquivo recusadas, por kretprobes nos hooks de segurança de arquivos do kernel: security_file_open e os hooks security_path_* para mkdir, rmdir, unlink, symlink, link, rename e truncate. Quando um hook retorna EACCES ou EPERM para um processo no cgroup da sandbox, isso é uma negação. Executar um arquivo que o Landlock não permite aparece como uma abertura recusada com FMODE_EXEC.

O supervisor passa à sentinela o ID do cgroup da sandbox uma única vez, por um parâmetro de módulo que pode ser definido, mas nunca alterado. O root não consegue apontá-lo para outro lugar e fazer a atividade do agente passar por atividade do próprio Bromure.

Transporte. Uma thread do kernel abre um socket AF_VSOCK para o Mac com sock_create_kern. O socket não tem descritor de arquivo, então não aparece sob nenhum processo em /proc e nada no espaço de usuário consegue fechá-lo. Os eventos passam por um anel de 256 posições, e cada tipo é limitado a 64 por segundo, com picos de até 256. Cada quadro é um comprimento de 4 bytes seguido de JSON.

Continuar carregada. O módulo não tem função de saída, então o kernel se recusa a descarregá-lo, e além disso ele mantém uma referência a si mesmo. Logo depois de carregá-lo, o carregador eleva o lockdown do kernel para integrity, ou confidentiality quando o workspace exige a sentinela. A partir daí, o root não consegue carregar um módulo não assinado, escrever na memória do kernel por /dev/mem nem fazer kexec para um kernel sem a sentinela.

Provar que está viva. A cada segundo, a sentinela envia um sinal de vida com um número de sequência, contagens de eventos descartados e limitados, e quantas sondas estão armadas. Essa contagem sozinha pode ser enganada: desligar todas as kprobes de uma vez pelo debugfs deixa intactas as flags de cada sonda, então a contagem continua parecendo saudável enquanto nada dispara. Para pegar isso, a sentinela mantém uma kprobe numa função vazia dela mesma e a chama uma vez a cada sinal de vida. Se o contador de acertos não se mexe, as sondas não estão disparando, seja qual for o motivo, e o sinal de vida diz isso.

Provar que é a sentinela. Cada carregamento gera um segredo de 32 bytes. O primeiro hello o leva, e o Mac o fixa para aquele boot. Uma reconexão nunca repete o segredo. Ela envia sha256(secret, boot_id, connection number) em vez dele, então um processo que leu o segredo da memória depois do primeiro hello ainda assim não consegue se passar pelo módulo. Uma segunda sentinela, um segredo errado, um número de conexão reutilizado, um buraco na sequência ou 15 segundos de silêncio contam todos como adulteração. O que resta ao root é derrubar ou desligar a VM, o que o Mac vê como silêncio.

Distribuição. Os módulos não são compilados na VM. A CI compila um para cada uma das 20 versões de kernel do Ubuntu 24.04, duas vezes, e as duas compilações precisam bater byte a byte. Cada módulo e o catálogo que os lista são assinados com uma chave ed25519, e o app recusa um módulo cuja assinatura ou hash não confere.

Uma lição da construção: durante um tempo, a sonda de negação de arquivos comparava um valor de retorno sem sinal com -EACCES e nunca disparou nem uma vez. Os testes só verificavam se a sonda estava registrada, o que ela sempre estava. Agora eles verificam se uma negação produz um evento.

Julgar os eventos. No Mac, os eventos são julgados pelo que significam, não por qual chamada de sistema disparou. Cada um soma peso a uma pontuação, e 20 em 60 segundos disparam o alarme:

EventoPeso
Adulterar a sentinela, ou obter root de dentro de uma sandbox estrita20
Um módulo não assinado, um carregamento de BPF, um kexec, uma tentativa de baixar o lockdown; um programa recusado 20 vezes em um minuto10
Um ptrace, ou uma chamada de sistema recusada pelo seccomp3
Uma montagem ou uma mudança de namespace; eventos descartados2
Uma operação de arquivo recusada pela sandbox1
Um sudo num workspace que permite sudo0, mostrado na linha do tempo

Um alarme que dispara no primeiro minuto ensina as pessoas a ignorar os verdadeiros. Por isso, as tarefas de manutenção do próprio Bromure rodam fora da sandbox e nunca aparecem como negação, e os eventos do Bromure montando a VM durante o boot são contados, sem disparar o alarme, dentro de uma janela fixa de boot. Quando o alarme dispara, o Bromure pode pôr o workspace em quarentena: ele corta a rede da VM e a pausa.

Linhagem: o raciocínio por trás de cada conexão

Um log de rede mostra que algo na VM se conectou a um host, mas não por quê. Quando é um agente que faz o trabalho, o porquê é o que você precisa saber.

O Bromure consegue dizer, porque vê as duas pontas. O proxy de IA dele fica entre o agente e o provedor do modelo e lê cada resposta antes que o agente aja, então conhece cada chamada de ferramenta, o comando que ela vai rodar e o que o modelo escreveu logo antes. A sentinela relata cada programa que a VM inicia, com a linha de comando, e cada nova conexão, com a cadeia de processos por trás dela. O Bromure junta as duas coisas.

Digamos que você peça ao agente para adicionar uma verificação de saúde que tente de novo a sua API de pagamentos. Ele decide que precisa de um cliente HTTP e roda npm install axios. O npm baixa o pacote de registry.npmjs.org, que a política permite. Aí um script postinstall dentro do download começa a rodar e tenta alcançar telemetry.example.net, que nenhuma regra permite. A conexão é recusada no proxy, no Mac, e a linhagem mostra como isso aconteceu:

Visão de linhagem do Bromure para uma conexão bloqueada a telemetry.example.net: o prompt, o raciocínio e a intenção declarada do agente, a chamada de ferramenta Bash npm install axios marcada como correspondência exata, cinco processos de claude passando por bash, npm e sh até node rodando scripts/postinstall.js, o fluxo TCP e a decisão Blocked by the request policy, com o fluxo permitido para registry.npmjs.org listado abaixo
A linhagem de uma conexão bloqueada: o pedido, o raciocínio, a chamada de ferramenta, os cinco processos até o script postinstall, o fluxo e a decisão. O download permitido de registry.npmjs.org aparece sob a mesma chamada de ferramenta.

O agente nunca digitou um comando que alcançasse telemetry.example.net. Um log de rede comum mostraria um processo Node falando com um host desconhecido; a linhagem mostra que era o script de instalação de uma dependência, sob o npm install que o agente rodou por um motivo declarado.

A junção funciona em três etapas:

  1. Chamada de ferramenta. O proxy de IA guarda cada chamada de ferramenta da resposta do modelo por dez minutos: o ID dela (tool_use_id, ou o call_id da OpenAI), o comando e até 2.000 caracteres do que o modelo escreveu antes dela no mesmo turno.
  2. Shell. A partir dos inícios de programas relatados pela sentinela, o Mac mantém uma tabela de processos para cada VM, indexada por pid e horário de início, então um pid reutilizado não herda o histórico de outro. Um shell iniciado sob o agente até 30 segundos depois da resposta é ligado à chamada de ferramenta quando o comando aparece na linha de comando dele, depois de desfeitas as aspas que o próprio agente acrescentou. O Claude Code, por exemplo, roda comandos como bash -c '… eval '"'"'<command>'"'"' …'. Isso é uma correspondência exata. Quando o agente reescreveu o comando, o Bromure recorre à primeira palavra, ao momento e à ascendência, e marca a correspondência como aproximada.
  3. Decisão. Um fluxo é associado ao permitir ou negar do switch pelo protocolo, destino, porta e porta de origem, num intervalo de 5 segundos, e à decisão do proxy quando ele inspecionou a requisição.

Toda conexão abaixo do shell herda o vínculo. Isso inclui o tráfego que passa pela ponte de proxy do Bromure, que de outra forma pareceria todo feito de conexões para localhost. A ponte começa cada fluxo para o Mac com uma linha que indica a porta de origem do cliente, para que o Mac consiga associar a conexão local ao destino real.

Na janela de Segurança, Show the Whole Chain numa linha de rede abre essa visão.

Alguns modelos vão direto à chamada de ferramenta sem escrever nada antes. Nesse caso, a visão mostra o prompt e a intenção declarada da chamada. Serviços em segundo plano e qualquer coisa iniciada fora de uma chamada de ferramenta são marcados como não rastreados até um agente.

A linhagem também muda o que acontece depois de um bloqueio. Um agente que só vê uma conexão resetada ou um erro de permissão toma isso por uma falha e tenta outro host, outra porta ou outra ferramenta, o que, visto de fora, parece um agente saindo do controle. O Bromure avisa o agente, na própria sessão dele, que a política recusou a requisição e por quê, e que ele deve perguntar a você em vez de contornar. A gaveta Sandbox de um workspace em execução lista o que foi bloqueado, com um botão Permitir para cada item, e o assistente de políticas redige regras a partir das negações para você aprovar ou não.

A gaveta Sandbox de um workspace em execução usando OpenShell + Sentry, listando quatro destinos bloqueados com quantas vezes e há quanto tempo cada um foi tentado, um botão Permitir para cada um e o aviso de que mudanças de rede se aplicam na hora
A gaveta Sandbox lista os destinos bloqueados. Mudanças de rede se aplicam na hora.

O raciocínio do agente sempre aparece na sua própria Linha do tempo de segurança. Ele só é enviado ao console de uma organização no bromure.io quando essa organização ativa isso, o que não é o padrão, e nunca para perfis privados. Tokens em linhas de comando são ocultados antes que qualquer coisa saia do Mac.

Um loop de 24 segundos num workspace de demonstração. A Linha do tempo de segurança registra o que a sentinela e o Landlock recusaram: um processo Node tentando pegar chaves SSH e AWS, e um ptrace. Depois, a conexão bloqueada se abre na sua linhagem, lida do pacote de volta até o raciocínio do agente.

Como ativar

Políticas OpenShell, a sentinela do kernel e a linhagem chegam no Bromure Agentic Coding 5.4. Vêm desligadas por padrão. Para ativá-las num workspace, abra Ajustes, depois a Política de sandbox do workspace, e defina Sandbox engine como OpenShell + Sentry. Você pode escrever a política ali mesmo, ou importar um arquivo YAML do OpenShell que já tenha.

Ajustes de workspace do Bromure, painel Política de sandbox, com o motor de sandbox definido como OpenShell + Sentry e uma política OpenShell que permite github.com, acesso somente leitura a registry.npmjs.org e acesso somente leitura a pypi.org
Um workspace com OpenShell + Sentry. A política inicial permite o GitHub e acesso somente leitura ao npm e ao PyPI: instalações funcionam, publicação é bloqueada.
  • As regras de rede se aplicam na hora. Mudanças no sistema de arquivos valem a partir do próximo início da VM, porque o Landlock só consegue restringir o que um processo em execução pode fazer.
  • Por padrão, a sentinela roda em modo de melhor esforço. Defina-a como Required e o Bromure eleva o lockdown do kernel ao nível mais rígido, e trata uma sentinela que nunca se conecta como adulteração.
  • Com a sentinela obrigatória, o shell de um workspace fica pronto em 4 segundos quando o módulo do kernel dele está em cache, e em 6 segundos quando o Bromure precisa baixá-lo.

Para ver tudo funcionando, assista aos três episódios da Claudia: ela escreve uma política, ajusta a política enquanto um agente trabalha e rastreia um ataque à cadeia de suprimentos às 2 da manhã até o raciocínio do agente.

Obrigado à equipe do OpenShell na NVIDIA por publicar abertamente o formato de política, o motor de referência e o provador. Construímos sobre os três.