Bromure の OpenShell ポリシーと、すべての接続の背後にある推論
NVIDIA の OpenShell は、コーディングエージェントが触れてよい範囲を 1 つのポリシーファイルに記述します。Bromure Agentic Coding 5.4 はそのポリシーをエージェントの VM で適用します。Landlock と seccomp は VM の内側で、ネットワークのルールはあなたの Mac 側で動き、ゲストのカーネルはセントリーモジュールが見張ります。そのうえで、すべてのネットワーク接続を、それを開いたプロセス、そのプロセスを起動したツール呼び出し、そしてエージェントがその呼び出しについて述べた推論へと結び付けます。
コーディングエージェントが役に立つには本物のアクセス権が必要で、それをどこまで与えるかを示すのがポリシーです。ポリシーは、ファイアウォール、マウントオプション、エージェント自身の設定にばらばらに散らばった設定ではなく、読めて、検証できて、適用できる 1 つのドキュメントであるときに最もよく機能します。
NVIDIA の OpenShell
OpenShell は、NVIDIA がオープンソースで公開しているエージェント向けのランタイムで、Apache 2.0 ライセンスでリリースされています。私たちはそのポリシー形式をもとに構築しました。この形式では、1 つの YAML ドキュメントで 4 つの領域をカバーします。
filesystem_policyとlandlock:エージェントが読み書きしてよいパス。Linux カーネルのセキュリティモジュールである Landlock によって適用されます。process:エージェントをどのユーザーとグループで動かすか。network_policies:各プログラムがどの宛先に到達してよいか、そこへどのリクエストを送ってよいか。HTTP のメソッドとパスの単位まで指定できます。許可されていないものはすべて拒否されます。「network_policies のルールが許可しない限り、OpenShell はサンドボックスからのすべての外向き接続を拒否する」のです。
OpenShell には prover も付属しています。SMT ソルバーを使って、あるポリシーが別のポリシーの範囲を超えて何かを許可していないかを検証するものです。組織は境界を定めておけば、差分を読む代わりに、変更のたびにイエスかノーかの答えを得られます。
エージェントがどう失敗するかを明らかによく研究してきた人たちによる、丁寧な設計です。しかもオープンです。だから私たちはそこから出発しました。
Bromure の中の OpenShell
Bromure Agentic Coding 5.4 では、ワークスペースは Bromure 独自のファイアウォールルールの代わりに OpenShell のポリシーを使えます。私たちが取り入れたのはポリシー形式とそのセマンティクスで、コードではありません。エンジンは Mac 上で動く私たち独自のもので、Linux ゲスト側のコンポーネントも独自のものです。両者の判断が一致することを確かめるため、ネットワークルール、HTTP ルール、MCP、GraphQL、WebSocket、宛先アドレスのチェックにわたって生成した 125,666 件のケースで、私たちのエンジンと OpenShell のリファレンスエンジンを走らせました。食い違いは 1 件もありませんでした。このテストで ..%3b や二重エンコードされたスラッシュ(%252F)といったパスのトリックが見つかり、現在はこれらを OpenShell と同じように扱っています。
Bromure と OpenShell の違いは、適用がどこで行われるかです。OpenShell はゲートウェイとコンテナランタイムを通じてサンドボックスを起動します。Bromure は各ワークスペースを、あなたの Mac 上の使い捨ての Linux VM で動かします。そのため、ポリシーは 2 つの側に分けて適用されます。
- VM の中: ファイルシステムに対する Landlock、seccomp、
no_new_privs、ポリシーで指定されたユーザーへの切り替え。その仕組みは次のセクションで説明します。 - Mac の上: ネットワークのルール。すべての接続は Bromure の仮想スイッチを通って VM を出ます。仮想スイッチが宛先とポートをチェックし、TLS プロキシが HTTP のメソッドとパス、MCP、GraphQL、WebSocket のメッセージをチェックします。アドレスの解決と固定は Mac が行うため、DNS を使って
allowed_ipsルールをすり抜けることはできません。VM 内の root が、VM の外にあるルールを書き換えることはできません。
ポリシーでは、接続を行ってよいプログラムを指定することもできます。厳格モードを有効にすると、Bromure は接続を開いたバイナリをハッシュで確認します。curl は api.github.com に到達してよいが、Python スクリプトはだめ、といった具合です。api.github.com/repos/** には GET のみ、といった HTTP ルールと組み合わせれば、DELETE はプロキシで拒否されます。Landlock にはこうしたことはどれもできません。閉じ込められるのはファイルだけです。
組織が bromure.io から境界を設定すると、Bromure はすべてのワークスペースのポリシーをその境界に照らしてチェックします。NVIDIA の openshell-prover がインストールされていればそれを使い、なければより厳しい組み込みのチェックを使います。境界からはみ出すポリシーは保存できません。
私たちは 1 つだけ意図的な変更を加えました。各セクションは、存在する場合にのみ適用されます。 filesystem_policy のないポリシーでは Landlock はオフのままで、process のないポリシーではユーザーの切り替えを行いません。OpenShell がデフォルト値を補うのは、そのドライバーが常に完全なドキュメントを送るからです。Bromure では、OpenShell を採用したことでワークスペースが黙って壊れることはありません。これにより、3 つのレベルができます。
| ポリシーの内容 | VM の中で適用されるもの | 保証されること |
|---|---|---|
| ネットワークルールのみ | なし。エージェントは sudo を保持 | ネットワークポリシーと、カーネル・セントリーによる観測 |
filesystem_policy | Landlock(まだ root のうちに適用) | root に対しても有効なファイルシステムの閉じ込め |
process セクション、または厳格モード | Landlock、seccomp、no_new_privs、非 root ユーザー。sudo と docker は取り上げ | 上記すべてに加え、root に至る経路がなく、Bromure 自身のプロセスを止める手段もない |
受け付けはするものの適用しないフィールドもいくつかあります。主なものは認証情報の書き換えと署名のフィールドです。Bromure はもともと本物の認証情報を Mac に保持し、外へ出ていく途中で差し替えているので、エージェントが手にするのは常に代用品だけです。
VM の中:Landlock、seccomp、権限の降格
ゲスト側は、OpenShell の sandbox-linux クレートにある適用コードを再実装したものです。起動時に VM の中で動く必要があり、そこには Rust のツールチェーンがないため、ctypes を使って Python で書かれています。Rust の landlock クレートと seccompiler クレートが発行するのと同じシステムコールを発行し、差分テスト用のハーネスが OpenShell の本物のクレートをビルドして、パスごと、システムコールごとに両者を比較します。
root のスーパーバイザーである bromure-sandboxd が、エージェントのすべてのセッションを起動します。スーパーバイザーはセッションを専用の cgroup であるサンドボックス cgroup に移し、3 つの段階で閉じ込めます。
Landlock。 Landlock を使うと、特権を持たないプロセスが自分自身と、自分が起動するすべてのプロセスを、パスの一覧に制限できます。スーパーバイザーは次のことを行います。
- ルールセットが管理する権限を指定して
landlock_create_rulesetを呼び出します。ゲストの 6.8 カーネル(Landlock ABI 4)では、ファイルの実行、読み取り、書き込み、ディレクトリの一覧表示、各種ファイルの作成と削除、ディレクトリをまたぐ名前の変更(REFER)、切り詰めがこれにあたります。 read_onlyとread_writeにある各パスをO_PATHで開き、読み取り権限またはすべての権限を持つPATH_BENEATHルールとしてlandlock_add_ruleで追加します。landlock_restrict_selfを呼び出します。
それ以降、ルールセットが管理していて、どのルールも許可していないアクセスは、誰が求めても EACCES で失敗します。Landlock がチェックするのはユーザーではなくタスクなので、sudo を使っても範囲は広がりません。プロセスはルールセットを重ねることはできても、外すことは決してできません。ネットワークのルールは即時に反映されるのに、ファイルシステムの変更には VM の再起動が必要なのはそのためです。landlock.compatibility は、その ABI を持たないカーネルでどうするかを決めます。best_effort はカーネルが提供するもので続行し、hard_requirement はセッションの開始を拒否します。
seccomp。 厳格モードのとき、またはポリシーに process セクションがあるとき、スーパーバイザーは no_new_privs を設定し、OpenShell の 3 つの seccomp フィルターをインストールします。
- メインのフィルターはデフォルトで許可し、サンドボックスを迂回できるものをブロックします。そこには
ptrace、bpf、io_uring_setup、userfaultfd、perf_event_open、process_vm_readv、process_vm_writev、mount 系のシステムコールすべて、そしてsetnsが含まれます。匿名ファイルからコードを実行すると Landlock を迂回できるため、memfd_createと、AT_EMPTY_PATHを伴うexecveatをブロックします。新しいユーザー名前空間はケーパビリティを与えてしまうため、CLONE_NEWUSERを伴うunshareとcloneをブロックします。さらに、packet、Bluetooth、vsock の各ファミリーのソケットと、ルーティング以外の netlink をブロックします。 - 互換性フィルターは
clone3にENOSYSを返します。seccomp フィルターはレジスタの値を読めてもポインタをたどることはできず、clone3はフラグをポインタ経由で渡すからです。ENOSYSを返すと glibc はcloneにフォールバックし、そのフラグはメインのフィルターでチェックできます。 - ハードニングフィルターは、同じユーザーとして動くスーパーバイザーにセッションが手を出せないようにします。
ptrace、process_vm_*、pidfd_getfd、kcmpは使えず、スーパーバイザーのプロセスやプロセスグループにシグナルを送ることもできません。
私たちが OpenShell と異なるのは 2 点だけで、それも何が報告されるかと、シグナルがどこに向けられるかに限られます。私たちのフィルターには SECCOMP_FILTER_FLAG_LOG が付いているため、拒否されたシステムコールはカーネルの監査フックに届き、そこでセントリーがそれを捉えます。また、シグナルのルールは範囲を狭めてあり、対話的なターミナルでシェルのジョブ制御(kill %1)が引き続き使えます。
権限の降格。 run_as_user はセッションをポリシーで指定されたユーザーに切り替えます。デフォルトユーザーのグループはどれも引き継がないため、sudo も docker も使えません。ユーザーのホームは idmapped マウントでセッションにマウントされるため、セッションが作成したファイルはディスク上では引き続きデフォルトユーザーの所有となり、エージェントの設定もそのまま適用されます。
ゲストカーネルの中のセントリー
エージェントが実際に何をしたかを記録するため、Bromure は独自のカーネルモジュールであるセントリーを VM にロードします。その設計は、エージェントが VM の中で root になれることを前提としています。そのため、root が止められるものは何ひとつ証拠として扱いません。捕捉、バッファリング、転送はすべてカーネルの中で行われます。
捕捉。 セントリーはカーネル空間から kprobes を登録します。
- プログラムの起動。
security_bprm_committed_credsで、コマンドラインとプロセスの開始時刻とともに捕捉します。 - 認証情報の変更。
setuid系とsetgid系、capset、commit_credsです。 ptrace、モジュールのロードとアンロード、bpf、kexec_load、mount、unshare、setns。- 新しいネットワークフロー。TCP は最初の SYN が送出されたとき(
tcp_connect)、UDP はある宛先への最初の送信時に捕捉し、ICMP と raw ソケットも対象です。いずれも、背後にある最大 8 つのプロセスの連鎖とともに報告されます。 - 拒否されたシステムコール。
audit_seccompで捕捉します。 - 拒否されたファイル操作。カーネルのファイルセキュリティフックに対する kretprobes で捕捉します。対象は
security_file_openと、mkdir、rmdir、unlink、symlink、link、rename、truncate のsecurity_path_*フックです。サンドボックス cgroup 内のプロセスに対してフックがEACCESまたはEPERMを返せば、それが拒否です。Landlock が許可していないファイルの実行は、FMODE_EXEC付きの拒否された open として現れます。
スーパーバイザーは、サンドボックス cgroup の ID を、設定はできても変更はできないモジュールパラメーターを通じて、一度だけセントリーに渡します。root がそれを別の場所に向け、エージェントの活動を Bromure 自身のものとして記録させることはできません。
転送。 カーネルスレッドが sock_create_kern で Mac への AF_VSOCK ソケットを開きます。このソケットにはファイルディスクリプタがないため、/proc のどのプロセスの下にも現れず、ユーザー空間の何ものもそれを閉じることができません。イベントは 256 スロットのリングを通り、種類ごとに毎秒 64 件、バーストでは最大 256 件までにレート制限されます。各フレームは 4 バイトの長さと、それに続く JSON です。
ロードされたままでいる。 モジュールには exit 関数がないため、カーネルはアンロードを拒否します。そのうえモジュールは自分自身への参照を保持しています。ロード直後に、ローダーはカーネルのロックダウンを integrity に、ワークスペースがセントリーを必須にしている場合は confidentiality に引き上げます。それ以降、root は未署名のモジュールをロードすることも、/dev/mem を通じてカーネルメモリに書き込むことも、セントリーのないカーネルへ kexec することもできません。
生きていることの証明。 セントリーは毎秒ハートビートを送ります。そこにはシーケンス番号、破棄されたイベントとレート制限されたイベントの件数、有効になっているプローブの数が含まれます。root が debugfs ですべての kprobe を一度に無効にしても、有効なプローブの数にはそれが表れません。この操作は各プローブ自身のフラグには手を付けないため、数は健全に見えたままです。これを捉えるため、セントリーは自分自身の空の関数に kprobe を置き、ハートビートのたびに一度それを呼び出します。ヒットカウンターが動かなければ、理由が何であれプローブは発火しておらず、ハートビートがそれを伝えます。
セントリーであることの証明。 ロードのたびに 32 バイトのシークレットが生成されます。最初の hello がそれを運び、Mac はそのブートの間それを固定します。再接続のときにシークレットが再び送られることはありません。代わりに sha256(secret, boot_id, connection number) を送るため、最初の hello の後にメモリからシークレットを読み取ったプロセスでも、モジュールになりすますことはできません。2 つ目のセントリー、誤ったシークレット、再利用された接続番号、シーケンスの欠落、15 秒間の沈黙は、すべて改ざんとみなされます。root に残された手は VM をクラッシュさせるか電源を切ることですが、Mac にはそれが沈黙として見えます。
配布。 モジュールは VM の中ではビルドしません。CI は Ubuntu 24.04 カーネルの 20 のリリースそれぞれについてモジュールを 2 回ビルドし、2 つのビルドはバイト単位で一致しなければなりません。各モジュールと、それらを列挙するカタログは ed25519 鍵で署名され、アプリは署名やハッシュが合わないモジュールを拒否します。
開発中に得た教訓が 1 つあります。しばらくの間、ファイル拒否のプローブは符号なしの戻り値を -EACCES と比較していて、一度も発火しませんでした。テストはプローブが登録されていることしか確認しておらず、登録は常にされていました。現在のテストは、拒否が実際にイベントを生むことを確認しています。
イベントの判定。 Mac の上では、イベントはどのシステムコールが発生したかではなく、それが何を意味するかで判断されます。各イベントはスコアに重みを加え、60 秒以内に 20 に達するとアラームが作動します。
| イベント | 重み |
|---|---|
| セントリーの改ざん、または厳格なサンドボックスの内側からの root 取得 | 20 |
未署名のモジュール、BPF のロード、kexec、ロックダウンを下げる試み。1 つのプログラムが 1 分間に 20 回拒否されること | 10 |
ptrace、または seccomp に拒否されたシステムコール | 3 |
| マウントまたは名前空間の変更。破棄されたイベント | 2 |
| サンドボックスに拒否されたファイル操作 | 1 |
sudo を許可しているワークスペースでの sudo | 0(タイムラインには表示) |
最初の 1 分で鳴るアラームは、本物のアラームを無視するよう人に教えてしまいます。そのため Bromure 自身の保守処理はサンドボックスの外で動き、拒否として表示されることはありません。また、起動時に Bromure が VM を組み立てる際のイベントは、決められた起動期間内であれば数えられるだけで、アラームの対象にはなりません。アラームが作動すると、Bromure はワークスペースを隔離できます。VM のネットワークを切断し、VM を一時停止するのです。
リネージ:すべての接続の背後にある推論
ネットワークログからわかるのは、VM の中の何かがあるホストに接続したということで、なぜ接続したのかはわかりません。作業をするのがエージェントなら、知りたいのはまさにその「なぜ」です。
Bromure はこれに答えられます。両端が見えているからです。Bromure の AI プロキシはエージェントとモデルプロバイダーの間に位置し、エージェントが動く前に各レスポンスを読みます。そのため、すべてのツール呼び出し、それが実行するコマンド、そしてモデルが直前に書いた内容を把握しています。一方セントリーは、VM が起動するすべてのプログラムをそのコマンドラインとともに、すべての新しい接続をその背後のプロセスの連鎖とともに報告します。Bromure はこの 2 つを結び付けます。
たとえば、決済 API をリトライするヘルスチェックを追加するようエージェントに頼んだとします。エージェントは HTTP クライアントが必要だと判断し、npm install axios を実行します。npm は registry.npmjs.org からパッケージを取得します。これはポリシーで許可されています。続いて、ダウンロードしたパッケージの postinstall スクリプトが起動し、telemetry.example.net への到達を試みます。これを許可するルールはありません。接続は Mac 上のプロキシで拒否され、リネージはそれがどのように起きたのかを示します。


エージェントは、telemetry.example.net に到達するコマンドを一度も打っていません。通常のネットワークログなら、Node のプロセスが未知のホストと通信していると表示されるだけでしょう。リネージは、それが依存パッケージのインストールスクリプトであり、エージェントが理由を述べたうえで実行した npm install の下で動いていたことを示します。
結び付けは 3 つの段階で行われます。
- ツール呼び出し。 AI プロキシは、モデルのレスポンスに含まれるすべてのツール呼び出しを 10 分間保持します。保持するのは、その ID(
tool_use_id、OpenAI の場合はcall_id)、コマンド、そして同じターンでモデルがその前に書いた内容のうち最大 2,000 文字です。 - シェル。 Mac はセントリーが報告するプログラムの起動をもとに、VM ごとに pid と開始時刻をキーとするプロセステーブルを保持します。そのため、再利用された pid が別のプロセスの履歴を引き継ぐことはありません。レスポンスから 30 秒以内にエージェントの配下で起動したシェルは、エージェント自身のクォートを元に戻したうえでそのコマンドラインにコマンドが現れれば、そのツール呼び出しと結び付けられます。たとえば Claude Code は、コマンドを
bash -c '… eval '"'"'<command>'"'"' …'として実行します。これが完全一致です。エージェントがコマンドを書き換えた場合、Bromure は先頭の単語、タイミング、親子関係に頼り、その一致をあいまい一致と表示します。 - 判定。 フローは、プロトコル、宛先、ポート、送信元ポートをもとに 5 秒以内のスイッチによる許可または拒否と照合され、プロキシがリクエストを検査した場合はプロキシの判定とも照合されます。
シェルの配下にあるすべての接続がこの結び付きを引き継ぎます。Bromure のプロキシブリッジを経由するトラフィックも含まれます。これがなければ、そうした接続はすべて localhost への接続に見えてしまいます。ブリッジは Mac へのストリームをそれぞれ、クライアントの送信元ポートを示す 1 行で始めるため、Mac はローカルの接続を本当の宛先に対応付けることができます。
セキュリティ ウィンドウでネットワークの行の Show the Whole Chain を押すと、この表示が開きます。
モデルによっては、何も書かずにいきなりツール呼び出しに進むものもあります。その場合、表示には代わりにプロンプトと、呼び出しで表明された意図が示されます。バックグラウンドのサービスや、ツール呼び出しの外で起動されたものは、エージェントまでたどれないものとして示されます。
リネージは、ブロックの後に起こることも変えます。接続のリセットや権限エラーしか見えないエージェントは、それを障害と受け取り、別のホストやポートやツールを試します。外から見れば、エージェントが暴走しているように見える振る舞いです。Bromure はエージェント自身のセッションの中で、ポリシーがリクエストを拒否したこととその理由を伝え、回避しようとせずにあなたに尋ねるよう指示します。実行中のワークスペースの Sandbox ドロワーにはブロックされたものが並び、それぞれに 許可 ボタンが付いています。さらにポリシーアドバイザーが拒否の記録からルールの下書きを作り、承認するかどうかはあなたが決めます。


エージェントの推論は、あなた自身のセキュリティ タイムラインには常に表示されます。bromure.io 上の組織のコンソールに送られるのは、その組織がこれを有効にした場合だけです。デフォルトでは無効で、プライベートプロファイルについては決して送られません。コマンドライン上のトークンは、何かが Mac を出る前に伏せ字にされます。
有効にするには
OpenShell ポリシー、カーネル・セントリー、リネージは Bromure Agentic Coding 5.4 で導入されます。デフォルトではオフです。ワークスペースで有効にするには、設定 を開き、そのワークスペースの サンドボックスポリシー で Sandbox engine を OpenShell + Sentry に設定します。ポリシーはそこで書くことも、既存の OpenShell の YAML ファイルを読み込むこともできます。


- ネットワークのルールは即時に反映されます。ファイルシステムの変更が有効になるのは次回の VM 起動時です。Landlock は、実行中のプロセスにできることを狭めることしかできないためです。
- セントリーはデフォルトではベストエフォートモードで動きます。Required に設定すると、Bromure はカーネルのロックダウンを最も厳しいレベルまで引き上げ、一度も接続してこないセントリーを改ざんとみなします。
- セントリーを必須にした場合、ワークスペースのシェルの準備にかかる時間は、そのカーネル用のモジュールがキャッシュされていれば 4 秒、Bromure が取得する必要があれば 6 秒です。
実際の動きは Claudia の 3 つのエピソードでご覧ください。Claudia がポリシーを書き、エージェントが作業している間にそれを調整し、深夜 2 時のサプライチェーン攻撃をエージェントの推論までさかのぼって突き止めます。
ポリシー形式、リファレンスエンジン、prover をオープンに公開してくれた NVIDIA の OpenShell チームに感謝します。私たちはその 3 つすべての上に築きました。