エアギャップ環境における安全な Tool Calling
最初の警告は 1 つの DNS パケットだった。たった 1 つ、隔離されているはずのラボから外へ出て、base32 の識別子のように見えるドメインへ向かっていた。オペレーターは、Ollama 上で動く Qwen 2.5 7B をベースとするローカルエージェントで監査をしており、そのエージェントには建前上、ラボ内部レンジ以外のネットワークアクセスはなかった。パケットが出てしまったのは、エージェントが呼び出せるtoolsのひとつが resolve_target だったからであり、モデルは、我々の RAG にインデックスされた SMB バナー経由で入ってきた汚染プロンプトを前にして、攻撃者がそのバナーに意図的に仕込んでおいた FQDN を解決することが論理的な次の一手だと判断したのだ。
これは APT ではない。自分たち自身に対して仕掛けた概念実証であり、おかげで、数か月ずっと気にかかっていたことに決着がついた。tool callingこそが新しい攻撃面であり、エアギャップ環境ではその攻撃面が、実際よりずっと安全そうに感じられるのだ。ネタばらしをすれば、安全ではない。
モデルに「手」を渡したとき何が起きるか
Tool calling は何もかも変えた。テキストを吐き出すだけのモデルから、nmap を走らせ、ファイルを読み、ベクトルを引き、油断すれば外向きリクエストまで投げてしまうエージェントへと我々は移った。Yao らはその点を function calling 経由の jailbreaking に関する研究の中でうまく描いており、GPT-4o、Claude 3.5 Sonnet、Gemini 1.5 Pro に対し、関数メカニズム自体を介して 90% を超える攻撃成功率を報告している (Wu et al., 2024)[1]。モデルはユーザーに対してアラインされているが、あなたが渡した tools に対してまでアラインされているとは限らない。
問題は構造的だ。LLM がどの tool を呼ぶかを決める瞬間、それはコンテキストを消費している。そしてそのコンテキストは、ペンテストにおいては定義上、信頼できないテキストだ。HTTP レスポンス、バナー、取得したファイル、スキャナの出力。Wang らは From Allies to Adversaries でそれを定式化し、攻撃者が manipulator tools を注入したり、レスポンスを汚染したりして、エージェントに呼ぶべきでない関数を呼ばせる手口を示した (Wang et al., 2024)[2]。「データ」と「指示」の境目はぼやけ、両者を区別するセマンティックパーサを持たないモデルは、毒をそのまま飲み込む。
エアギャップという神話
モデルが Ollama 上でローカル動作し、インターネット出口がなければ問題は消える、と多くの人は思い込む。問題は消えない、形を変えるだけだ。我々のラボでも、繰り返し現れる 3 つのパターンがあった:
- サイドチャネルでの exfiltration:内部 DNS、ARP、あとから同期されるログ、誰かが拾っていく一時ファイル。
- 内部 pivoting:エージェントが正当にクライアントのネットワークへアクセスできるため、攻撃者はそれをプロキシとして使う。
- RAG の自己汚染:エージェントが保存した観測内容を、将来のエージェントが権威ある文脈として読む。
最後のものが特に厄介だ。Shi らの Log-To-Leak の研究は、まさにこのベクトルを描いている。エージェントが一見無害なログ収集ツールを呼び、結果としてクエリ、レスポンス、内部状態を漏らしてしまう (Shi et al., 2025)[3]。しかもログは境界の内側に居続けるので、従来型の SIEM には何の異常も見えない。
実務でどう取り組んだか
はっきり言わせてもらおう:銀の弾丸は存在しない。我々が持っているのは複数の層で、各層は前の層が破られうると仮定している。社内スタックでは責務を 3 つに分けている。1 つのバイナリに混ぜ込むのは面倒を呼び寄せるだけだ。
一方の側で Gandalf はゲートウェイの役を担う。モデルともオペレーターとも口をきくのはこいつだけだ。Gandalf には Sentinel と呼ぶコンポーネントがあり、すべての tool call の前後でポリシーを適用する。引数が許可された CIDR スコープに収まっているか、コマンドが exfiltration シグネチャに一致していないか、エージェントが庭の外へ出ようとしていないかを検証する。怪しい匂いがすれば、kill-switch がセッションを切断し、コンテキストを捨て、アラートを上げる。
もう一方では Gwaihir が実行体(executor)として機能する。コールが Sentinel を通ると、それは子プロセスとして実体化され、その特定の tool が必要とする syscall のサブセットだけを許可する seccomp-bpf フィルタが付く。任意の connect() はなし、allowlist 外のバイナリへの execve() もなし。これは Wei らが Securing AI Agent Execution で提示している、執行体はプランの現在ステップに必要な権限だけを動的に付与されるべきだ、という主張から直接インスパイアされている (Wei et al., 2025)[4]。最小権限の原則を、ロール単位ではなく syscall 単位で適用する。
そして Beorn。これは運用ナレッジとして使っている HTB ベクトル(現時点で約 9115 件)を抱える RAG を管理する。Beorn はモデルから直接の入力を一切受け取らない。クエリは Gandalf を介し、そこで正規化される。エージェントから見れば RAG は読み取り専用であり、これによって先に触れた自己汚染のベクトルは封じられる。
スキャナ系 tool に対する Sentinel ポリシーの一例:
{
"tool": "nmap_scan",
"scope_cidr": ["10.10.11.0/24"],
"deny_flags": ["-oN", "--script=http-fetch", "-iL"],
"max_runtime_s": 120,
"seccomp_profile": "gwaihir/profiles/nmap.json",
"kill_switch": {
"on_outbound_dns": true,
"on_unexpected_egress": true,
"on_token_budget_exceeded": 4096
}
}SMB バナー事件を捕まえてくれたのは on_outbound_dns だった。ラボ内部ゾーンに収まらない解決要求はすべて、パケットがインターフェイスを出る前に kill-switch を発動させる。
ローカルモデル:なぜ Qwen、Llama、Phi なのか
ホステッドモデルを相手に運用することは、率直に言ってエアギャップとは相容れない。だがもうひとつ、もっと繊細な理由がある。商用モデルのtool callingは固定スキーマで学習されており、そこから外れると引数を幻覚しがちだ。Ollama 上で提供される Qwen 2.5、Llama 3.1、Phi-4 なら、GBNF 文法でstructured outputを強制でき、「クリエイティブな引数」の面積を大幅に縮められる。完璧ではないが、監査はできる。
トレードオフは現実だ。素の能力は失う。Qwen 7B は、tools を連結した 5 ステップの連鎖を Claude Opus のようには推論できない。それは、プランをより小さなステップに分解し、Sentinel に一つひとつ別々に検証させることで埋める。代わりに、完全なトレーサビリティ、予測可能なレイテンシ(トークンのコストはミリ秒であってドルではない)、必要があればモデルをビット単位で監査できる可能性を手に入れる。
隔離した結果として壊れるもの
得るものばかりではない。すべてを隔離したことで、こちらでは 3 つほど壊れた:
1 つめは 新興脅威の可視性。テレメトリがラボ外に出ないため、CTI フィードとリアルタイムで相関できない。我々は非対称チャネルで解決した。ラボから外への通信は行わないが、ラボ外のプロセスは X 分ごとに内部 bucket から引っ張って enrich できる。
2 つめは RAG の更新。Beorn に保持している 9115 件の HTB ベクトルは勝手には更新されない。外で reindex して、署名済みスナップショットを内側へ押し込み直す必要がある。運用上の摩擦は確かにあるが、想定可能だ。
3 つめは オペレーターの UX。大きいモデルとのチャットに慣れていると、オペレーターは時に Qwen が「バカっぽい」と感じる。ある意味そのとおりだ。だが、seccomp-bpf の内側で CIDR スコープと kill-switch に守られて動くバカなモデルの方が、syscall へ自由にアクセスできる賢いモデルよりはるかに危険が少ない。
持ち帰り
もしここから 1 つだけ持ち帰るなら、これにしてほしい。エアギャップは性質ではない、アーキテクチャだ。そしてそのアーキテクチャの中で、tool calling は信頼が最初に崩れる場所だ。一番安く一番効くものから、以下の順で始めよう:
- エージェント単位ではなく、セッション単位で明示的な CIDR スコープを定義する。すべてのコール前にそれを検証する Sentinel 相当のものを置く。
- すべての tool 実行を、syscall の allowlist 付き seccomp-bpf フィルタの裏側に置く。Gwaihir がなければ、bubblewrap、gVisor、nsjail を見るとよい。
- 怪しいコマンドだけでなく、想定外の egress に反応する kill-switch を実装する。モデルは tool の名前ではなく、引数で驚かせてくる。
- RAG のコンテキストを信頼できないデータとして扱う。そう、昨日あなた自身が入れたものも含めて。
あの DNS パケットはどこにも届かなかった。それでも Sentinel のあのログは我々のポストモーテムに残り続け、ローカルで動く、インターネットのない、善意のエージェントですら、半年前に誰かがバナーに書き込んだだけのデタラメなドメインへ話しかけようとしうるのだ、と思い出させてくれる。これが運用の現実だ。残りは演劇に過ぎない。
モデルを信頼するのは、仕事をさせるのに必要な分だけにとどめよ。そのコンテキストは常に疑え。
参考文献
- Wu, Z. et al. (2024). The Dark Side of Function Calling: Pathways to Jailbreaking Large Language Models. arXiv:2407.17915.
- Wang, H. et al. (2024). From Allies to Adversaries: Manipulating LLM Tool-Calling through Adversarial Injection. arXiv:2412.10198.
- Shi, Y. et al. (2025). Log-To-Leak: Prompt Injection Attacks on Tool-Using LLM Agents via Model Context Protocol. OpenReview.
- Wei, J. et al. (2025). Securing AI Agent Execution. arXiv:2510.21236.
- Patel, R. et al. (2025). Architecting Resilient LLM Agents: A Guide to Secure Plan-and-Execute Patterns. arXiv:2509.08646.