id: ADR-014 title: プラグインインスタンスは、ホストが所有する単一のエグレス権限を通じてのみネットワークに到達する date: 2026-08-20 status: 提案中 relates-to:
- ADR-006
- ADR-009
- https://github.com/zeroclaw-labs/zeroclaw/issues/9395
- https://github.com/zeroclaw-labs/zeroclaw/issues/8850
- https://github.com/zeroclaw-labs/zeroclaw/issues/8398
- https://github.com/zeroclaw-labs/zeroclaw/pull/9580
- https://github.com/zeroclaw-labs/zeroclaw/pull/9137
- https://github.com/zeroclaw-labs/zeroclaw/pull/9582
- https://github.com/zeroclaw-labs/zeroclaw/pull/9584
- https://github.com/zeroclaw-labs/zeroclaw/pull/11098
- https://github.com/zeroclaw-labs/zeroclaw/issues/10996
- https://github.com/zeroclaw-labs/zeroclaw/pull/9126
- crates/zeroclaw-infra/src/net_guard.rs
- crates/zeroclaw-plugins/src/egress.rs
- crates/zeroclaw-plugins/src/component.rs
- crates/zeroclaw-config/src/schema.rs
- crates/zeroclaw-tools/src/helpers/domain_guard.rs
ADR-014: プラグインインスタンスがネットワークに到達できるのは、ホストが管理する単一の送信アクセス権限経由のみ
コンテキスト
#9582 で強制機能が導入される前、ZeroClaw は、プラグインのマニフェストが http_client パーミッションを要求した場合に、wasi:http インポートをプラグインストアにリンクしていました。このストアは wasmtime のデフォルトのリクエストフックをインストールしていた(crates/zeroclaw-plugins/src/component.rs)ため、ゲストが接続できる先を ZeroClaw 側で決定する仕組みはありませんでした。宛先の許可リスト、アドレス種別のガード、オペレーター向けの設定はいずれも存在しませんでした。マニフェストのパーミッションは要求にすぎず、ホストはすべての要求をそのまま許可していました(crates/zeroclaw-plugins/src/instance.rs)。一方で plugins.security.signature_mode のデフォルトは disabled でした。そのため、署名のないコンポーネントでも http_client を宣言して受け取ることができ、ループバックのサービス、ローカルゲートウェイ、クラウドのメタデータアドレスへフィルタなしで到達する踏み台になり得ました。バグ #9395 は、この過去の影響を記録したものです。現在出荷されている強制機能は、ホストが管理する宛先ポリシーを適用します。
ホスト側の姉妹実装はすでにフェイルクローズで動作しています。http_request ツールは、allowed_domains リストが空の場合にすべての呼び出しを拒否し、crates/zeroclaw-tools/src/helpers/domain_guard.rs および crates/zeroclaw-infra/src/net_guard.rs を通じて、宛先と解決されたすべてのアドレスをメタデータ、ループバック、プライベート範囲に対して検証します。1つのプロセス内にある2つの送信経路が、互いに正反対の保証を持っていました。
ADR-006 により、オプションのチャネルの対象がランタイムプラグインになり、すべてのメッセージングチャネルが egress consumer となるため、この境界にかかる圧力は高まっている。現在、wit/v0/inbound.wit はチャネルプラグインがネットワークもソケットもなしで実行されることを明示しており、公式プラグインリポジトリには、ホストが公開していないトランスポート向けのソケットインターフェース草案が wit/unstable/sockets.wit として存在する。次に導入されるトランスポートが何であれ、「このインスタンスはそのアドレスに到達してよいか」という問いに対して、新たな第三の答えではなく、既存の答えが必要である。
この方向性の一部はすでに出荷されています。#9580 では組み込みの HTTP egress を共有ネットワークガードへ移行し、#9137 ではプラグイン側の egress ポリシーの基盤を追加しました。#9582 では wasi:http 境界でポリシーを適用し、#9126 では型付きインスタンス設定を追加しました。オペレーターによる権限付与の手続きは、インストールと一覧表示について #11098 で導入され、これは #9584 を置き換えました。チャネルバインディングについては、#10996 の下で zeroclaw plugin bind を通じて導入されています。本記録は、これらの分割された実装が実現し、完成させる決定事項を述べたものです。RFC #8398 は包括的/置き換え済みの RFC としてクローズされています。本 ADR は、その分割後も残った、ネットワーク egress に関する個別の決定を記録するものです。すなわち、ネットワーク権限に関する Q1 と、ユーザーによる宛先権限の拡張に関する Q4 です。
代替案は3つあります。マニフェストの宣言そのものを許可として扱う方法、インストール済みのすべてのプラグインに単一のグローバル許可リストを適用する方法、そしてトランスポートごとに宛先ポリシーを個別に設定する方法です。マニフェストを許可とみなす方式は、署名のないパッケージに対する#9395の自己付与の経路を残し、デフォルト・クローズドの原則に反し、マニフェストが稼働中の権限に関する第二の正本になってしまいます。グローバル許可リストは、インスタンスごとの分離を損ないます。インストール済みの任意のプラグインが、他のどのプラグインが必要とするホストにも到達できてしまうためです。トランスポートごとのポリシーは、1つの判断に3つの設定項目を設けることになり、設定のずれを招きます。宛先の判断はトランスポートに依存しないはずだからです。
決定
共有ポリシー機構とデフォルト拒否を備えた単一の egress 権限を使用する
プラグインによるすべての外向きネットワークアクセスは、現在の wasi:http も、将来追加される任意のソケット、WebSocket、TLS プロファイルのインポートも含めて、ホストが管理する単一の egress 権限によって仲介されます。宛先マッチング、アドレス分類、NAT64 変換、名前解決後の判定は zeroclaw-infra::net_guard にあり、組み込みツールもこれを利用しています。crates/zeroclaw-plugins/src/egress.rs には、これらのプリミティブを利用するインスタンス単位のサービスがありますが、それらを再実装することはありません。また、この用途で zeroclaw-plugins が zeroclaw-tools に依存することもありません。プラグインと組み込みツールの間で、ある宛先に到達可能かどうかの判断が食い違ってはなりません。
許可された接続先を持たないプラグインインスタンスは、ネットワークに到達できません。http_client 単独で無制限の HTTP アクセスが与えられる互換モードは存在しません。権限は機能の範囲を付与し、接続先は運用者のリストによって付与されます。
サービスは、リンカーが承認済みインスタンスに付与されたインポートのみを公開している場合でも、操作の境界でトランスポートに必要な権限を再度チェックします。この重複したチェックは意図的な多層防御です。トランスポートアダプターが、リンクされているものの付与されていないインポートを通じてネットワーク到達性を得られてはなりません。
wasi:http 境界で、固定された送信パスを使って強制する
強制は転送境界で行われます。プラグインストアごとに保存された ZeroClaw のフック実装が wasmtime のデフォルトフックを置き換え、そのリクエストフックはゲストが発行するすべてのリクエストに対してポリシーを評価します。送信経路はホストが所有し、固定されています。宛先を一度だけ解決し、解決されたアドレスを共有ガードで検証したうえで、その正確なアドレスにのみ接続します。接続時には SNI と証明書検証にホスト名を使用します。再解決が行われないため、DNS の応答が確認と接続の間にクラスを変えることはありません。ResolvedDestination は、この性質を構造的に保証するために存在します。検証済みのアドレスを保持し、新たな名前解決へ戻る経路を提供しません。
ホストがゲストに代わってリダイレクトをたどることはありません。ゲストがリダイレクトをたどることを選択した場合は新しいリクエストを発行し、各ホップはそれぞれ独立に完全なポリシーを通過します。
拒否されると、ホストの内部情報ではなくポリシー名を示すマスクされたエラーがゲストに返され、その試行を正確なインスタンスに帰属させる構造化されたホスト側のログイベントが出力されます。
この seam は wasi:http 向けに #9582 で出荷済みです。残りのゲートは、交差部分、フィクスチャのカバレッジ、フリートへのロールアウトを対象としています。
Manifest で宣言し、オペレーターの設定で許可する
プラグインマニフェストは、パッケージが必要とする送信先を [egress] セクションで宣言できます。宣言できるのは、完全一致のホスト名か、明示的なサフィックスパターンです。この宣言は署名の対象となる要求であり、パッケージに付随する意図の表明です。リクエスト時に許可として参照されることは決してありません。
許可設定は、オペレーターの正規の設定ファイルに置かれます。インスタンスの [[plugins.entries]] 行に平文のフィールドとして記述し、接続先には egress_hosts、アドレスクラスの例外指定には egress_allow_private を使います。両者は #[secret] の config マップと意図的に並列に配置されており、その内側に置かれることはありません。許可リストはオペレーターが監査する対象であるため、監査するファイル内で読める状態を保つ必要があるからです。
インスタンスの実効的な権限範囲は、そのマニフェストが要求する内容と、オペレーターが付与した内容の共通部分です。宣言だけでは何の権限も与えられず、またオペレーターによる付与は、パッケージが必要だと宣言した範囲を超えることはできません。付与の経路は次の2つが正式にサポートされています:
- 以下の授与式での宣言からシードされました。
- オペレーターが直接作成するものであり、宛先自体がインスタンス設定となるプラグイン(セルフホスト型のGitea、Matrixホームサーバー、LAN内のNextcloudなど)にとっては、これが唯一取りうる経路です。作成者はホストを知り得ないため、設定を行うオペレーターがそれを許可します。
ポリシーは、インスタンスの状態にスナップショットとして保持するのではなく、リクエストごとに正規の設定から読み取られます。そのため、オペレーターによる編集は再起動ではなく次の接続から適用されます。ADR-012(まだ提案中)は、関連する世代単位の適用モードについて記述しています。ここでは参考情報であり、権威ある根拠ではありません。
オペレーターのフィールド、リクエストごとの読み取り、マニフェストの [egress] 宣言はマージ済みです。積集合はまだ実装されていないため、このリリースではオペレーターの許可のみが適用されます。この点はゲートG1でカバーされています。
移行先の文法に全許可形式を含めないでください
エントリは、完全一致のホスト、または明示的な *.suffix パターンのいずれかです。素のドメインがそのサブドメインを暗黙的に含むことはなく、サフィックスパターンが apex(ドメインそのもの)に一致することもありません。エントリなしは「どこでも」を意味します。検証では素の * は拒否されます。検証されていないエントリは、文字どおり * という名前のホストにしか一致しないため、検証をすり抜けたリストであってもフェイルクローズ(安全側に失敗)します。IP リテラルのホストは、完全に一致するエントリによってのみ照合され、サフィックスパターンによって照合されることは決してありません。アドレスにはサブドメインの構造がなく、ドット区切りの名前として扱うと *.0.0.1 が 127.0.0.1 に一致してしまうためです。空のリストはどれにも一致しません。ポートはエントリの一部ではありません。ホストを許可すると、そのホストのすべてのポートが許可されます。
アドレスクラスをallowlistより上でブロックする
クラウドのメタデータアドレスは、両方のアクセスモードで無条件に拒否されます。そのため、プライベート宛先に対するオペレーターの明示的な許可によって、これらが再び許可されることはありません。ループバック、リンクローカル、プライベートレンジの宛先は、パターンに一致する場合でも、エントリが egress_allow_private にそのホストを指定していない限り拒否されます。この例外規定が緩和するのはアドレスクラスの制限のみです。egress_hosts に含まれていないホストは、egress_allow_private に記載されていても引き続き拒否されます。この挙動は、組み込みツールがすでに使用している allowed_private_hosts のセマンティクスを踏襲しており、両方の利用側が同じ用語体系を保てるようになっています。
両方の信頼ゾーンにまたがる応答セットは、プライベートアクセスが許可されている場合でも、一方のゾーンにまとめられることなく拒否されます。そうしなければ、リゾルバーの順序や接続のフォールバックによって、接続がどちらのゾーンに着地するかが暗黙のうちに決まってしまいます。
運用者に NAT64 プレフィックスの宣言を必須とし、不正な形式のリストはフェイルクローズで失敗させる
security.nat64_prefixes は、運用者がネットワークで実際に使用している変換を宣言する場所です。NAT64 トランスレータは、設定されたプレフィックス内にあるすべての IPv6 宛先を、その宛先に埋め込まれた IPv4 アドレスへ配送します。プレフィックスはデプロイ時に選択されるものであり、アドレス自体からそれを判別する手段はありません。そのため、ホスト名の DNS 応答を制御できる攻撃者は、一見グローバルな IPv6 アドレスを返すことで、ローカルのトランスレータが 10.0.0.1 や 169.254.169.254 へ配送するよう仕向けることができます。エントリは、RFC 6052 セクション 2.2 で定められたプレフィックス長 6 種類すべてを用い、<ipv6>/<length> の形式で記述します。プレフィックス長より後ろのビットは設定しないでください。
以下の2つのルールは、一般的な実装よりも厳格です。第一に、不正なエントリが1つでもあればリスト全体を拒否し、整形式のサブセットに縮小することはしません。解析の結果「プレフィックスなし」となったリストは、トランスレータを使用しないデプロイメントと全く同じに見えてしまい、シグナルなしにネットワーク固有の分類を無効化してしまうためです。第二に、宣言されたプレフィックスは重複することがあり、その場合1つのアドレスが各プレフィックスの下で異なるIPv4宛先にデコードされます。それらの宛先はすべて到達可能であるため、アドレスは、一致するすべての宣言済み変換先が許容可能な場所に行き着く場合にのみ受け入れられます。たまたま最初に許容可能と判定された変換だけで判断するのではありません。
権限付与を明示的な手順にする
zeroclaw plugin install は、パッケージが宣言している送信先を表示し、すでに作成されているエントリにそれらを初期値として登録します。チャンネルエイリアスのように後から作成されるバインディングを持つインスタンスは、そのバインディングが作成される時点で、同じ宣言から、同じ表示内容とともに送信エグレスのエントリを受け取ります。インストールとバインディングの作成は、オペレーターによる明示的な操作であり、表示され永続化された許可リストがその記録となります。zeroclaw plugin list は各インスタンスに付与された送信先を表示するため、設定ファイルを読まなくても到達範囲を監査できます。
宣言に送信先を追加するパッケージのアップグレードは、既存のエントリを拡張しません。CLI が差分を表示し、オペレーターがそれを意図的に適用します。エントリがない場合、egress は拒否されます。
インストール時のシーディング、アップグレード時の差分、そして対応する zeroclaw plugin list のレポートは #11098 で実装され、#9584 を置き換えました。バインド時のシーディングは #10996 で zeroclaw plugin bind とともに実装されており、zeroclaw plugin install –channel-alias はインストール中に同じ手順を実行します。オペレーターがその時点で –channel-alias のエイリアスを指定するため、バインドの手順では付与内容を明示的な選択として扱います。--egress declared を指定すると、インストール時と同じ出力を伴って宣言がシードされ、--egress none を指定すると付与が空のエントリが作成されます。どちらのフラグも指定しない場合、空でない宣言からエントリを作成するバインドは拒否されます。どちらのコマンドも既存のエントリを拡張することはありません。
Future transport のすべてに同じポリシーオブジェクトをバインドする
Socket、WebSocket、TLS プロファイルのインポートは、ホストが採用する場合、同じインスタンス単位のポリシーを利用します。ホストとポートへのソケット接続または WebSocket アップグレードは、同じ許可リストと同じアドレスクラスのルールを通過します。TLS プロファイルは信頼素材を選択しますが、宛先ポリシーを迂回することは決してありません。宛先の判断をこのサービスを経由させられないトランスポートのインポートは取り込まれません。
このレコードでは、ホストにWASIソケット機能が存在するまで、Raw-TCPチャネルは対象外です。それまでの間、プロトコルがHTTPではないチャネルにはプラグイン側の経路がなく、ADR-006のcapability-exceptionルールに基づきネイティブのまま残ります。Matrixはそのような例外の一つですが、本レコードではこれを廃止しません。不足しているホスト機能として明記されているのはエンドツーエンド暗号化であり、プラグイン境界がそれを扱えるようになるまで、ネイティブ実装は残ります。これはADR-006におけるcapabilityの同等性(パリティ)に関する問題であり、本レコードはそれを変更するものではありません。
グラントはプロトコルに依存しません。ホストにグラントを付与するオペレーターは、そのインスタンスの権限で許可されている任意のトランスポートに対してグラントを付与するため、別個の平文の軸は存在せず、egress_allow_plaintext フィールドもありません。機密性は、宛先リストの二つ目ではなく、トランスポートと通信相手との間の契約です。プロトコルがインバンドで TLS をネゴシエートする場合、フェーズの移行はホストが管理し、アップグレードが開始された後は平文へのフォールバックを一切許可しません。
Per-instance の接続バジェットは、各トランスポートではなく基盤に属します。plugins.limits.max_connections_per_instance は、1 つの論理インスタンスに属するすべてのトランスポートおよびすべてのストアで共有され、認可されたリクエストは接続のライフタイムの間そのリースを保持します。
信頼されていないマニフェスト入力を、同じ境界で制限する
マニフェストは攻撃者が制御しうる入力であり、egress の宛先と同じ信頼境界に位置します。そのため、マニフェストのわずかなバイト数によって高コストなホスト側の処理が引き起こされてはなりません。型付きインスタンス設定の作業は #9126 で提供されており、これに従ってマニフェストで指定されたスキーマのパターン検証に上限を設けています。パターンは線形時間エンジン上でコンパイルされ、明示的な 256 KiB のコンパイル済みプログラム上限と 1 MiB の DFA 上限が適用されます。いずれかを超えるパターンはコンパイルされず、無効なマニフェストとして拒否されます。パッケージを拒否する方が安全な方向です。そこまで高コストなパターンが必要なプラグイン作者は、より小さいパターンを書けます。一方、コンパイルを開始してしまったホストは、スケジューラを取り戻せなくなるおそれがあります。本書に記録するのは、本 ADR が管理する境界を強化するものだからであり、egress の経路上にあるからではありません。
受け入れゲート
この ADR は、以下の条件をすべて満たすまで提案中のままです:
- 共有ガードプリミティブが
zeroclaw-infra::net_guardにあって両方の利用側がそれを使用し、ストアごとのフックと固定された送信パスがプラグインストア向けに提供され、マニフェストの[egress]宣言が解析、検証、署名のカバレッジとともに存在し、有効なグラントがその宣言とオペレーターのエントリの積集合となり、インストール時とバインディング時のシーディングおよびアップグレード差分の手続きが機能します (G1); - 必須の CI が、実際のコンポーネントフィクスチャを使って境界を証明します: エントリがない場合はデフォルトで拒否され、シードされたエントリを通じて許可され、一致する許可リストがあってもメタデータアドレスとプライベートアドレスは拒否され、許可されたホストからブロック対象のクラスへのリダイレクトを追うコンポーネントは 2 回目のリクエストが拒否されます (G2);
- #8850 で選定された最初のチャネルプラグインは、そのAPIホストのシード済みエントリ(G3)の下で実行されます。また、
- 既存のフリートへのロールアウトは完了しています。ホストでの強制が有効になる前に、公式レジストリの
http_clientパッケージは再公開されたバージョンで[egress]宣言を含むようになりました。アップグレード時の診断では、インストール済みの各インスタンスについて拒否された宛先と正確なシーディングコマンドが一覧表示されます。強制を有効にするリリースでは、変更履歴に今回の破壊的変更が明記されます。#9395 は強制適用のスライス(G4)によって既にクローズされています。
共有ガード、平文のオペレーターフィールド、リクエストごとのポリシー読み取り、厳格な宛先文法、NAT64 境界、接続バジェット、マニフェストの [egress] 宣言、アップグレード差分を伴うインストール時およびバインディング時のシーディングは実装済みです。G1 の残り(その宣言とオペレーターのエントリの交差部分)と G2 から G4 は未実装です。
結果
肯定的な結果:
- #9395のピボットは、署名ポリシーのみに頼るのではなく、設計上の仕組みによって閉じられています。署名のない、または悪意のあるコンポーネントは、マニフェストの自己要求をネットワーク到達性へと変換できないためです。
- Operatorは、インスタンスごとに監査可能なプレーンテキストの設定項目を1つ持ち、変更は再インスタンス化せずに次の接続から適用されます。
- パッケージのアップグレードによって、外部への通信許可範囲が暗黙のうちに広がることはありません。
- 組み込みツールとプラグインは、互いに乖離していくのではなく、単一のガード用語体系に収束します。
- 新しいトランスポートは、送信先の決定を再検討せず、それを引き継ぎます。
否定的な結果:
- これは、インストール済みのすべての
http_clientプラグインに対する破壊的変更です。そのエントリがシードまたは作成されるまで、egressは許可されません。G4はこの理由から存在しており、この変更は移行診断と再公開された宣言が揃って初めて提供されます。 - セレモニーには、インストール時のステップとバインディング時のステップが追加されるほか、エントリごとのポリシー状態も加わります。
- ユーザー指定の URL など、動的な送信先を必要とするプラグインには、ワイルドカードの egress は許可されません。そうしたプラグインは、オペレーターが受け入れるサフィックスパターンを宣言するか、オペレーターが作成したエントリに依存するか、ホストが仲介するフェッチサービスの提供を待つことになります。これは #8398 の Q4 への回答に伴う、意図的なトレードオフです。
- NAT64 変換器の背後にあるデプロイメントでは、境界を正確に保つためにプレフィックスを宣言する必要があり、重複するプレフィックスの保守的な評価によって、オペレーターが正当と見なす宛先が拒否される場合があります。
- ポリシーの読み込みはインスタンス化ごとではなくリクエストごとに行われるため、設定リゾルバは接続の経路上にあります。
参照
- バグ #9395: プラグインの
wasi:http送信に宛先ポリシーも設定項目もない - Migration tracker #8850
- RFC #8398: プラグインの権限、設定、シークレットモデル (クローズ済み/置き換え済みの文脈)
- PR #9580: 共有ネットワークガードにおける組み込み HTTP 送信の強化(マージ済み)
- PR #9137: 共有 egress ポリシーの基盤 (マージ済み)
- PR #9582: プラグインの
wasi:httpにホスト管理の egress ポリシーを適用(マージ済み) - PR #9584: plugin の install と list における egress 許可の手続き (クローズ済み、#11098 に置き換え)
- PR #11098: インストール時に egress を許可し、プラグインが読み込まれるかどうかを報告 (マージ済み)
- Issue #10996: チャネルインスタンスの設定と権限付与の初期投入 (バインディング時の初期投入)
- PR #9126: 型付きインスタンス設定の検証 (マージ済み)
- ADR-006: ランタイムチャネルプラグイン
- ADR-009: WIT と wasmtime プラグインの実行
- ADR-012: 世代スコープのライブ設定適用
- セキュリティモデル
crates/zeroclaw-infra/src/net_guard.rscrates/zeroclaw-plugins/src/egress.rscrates/zeroclaw-plugins/src/component.rscrates/zeroclaw-config/src/schema.rs- crates/zeroclaw-tools/src/helpers/domain_guard.rs
crates/zeroclaw-tools/src/http_request.rs