E2BとPythonで構築するAIエージェント向けセキュアなコードサンドボックス

AI tutorial - IT technology blog
AI tutorial - IT technology blog

深夜2時14分、クラスタを停止させたメモリ不足(OOM)アラート

深夜2時14分、PagerDutyの高優先度アラートで叩き起こされました。プライマリの32コアバックエンドノードが応答しなくなり、CPU使用率は100%に張り付き、ディスクI/Oは完全に停止していました。

踏み台サーバー経由でターミナル接続し、htop を確認したところ、原因は一目瞭然でした。自動データ分析エージェントが、4GBの顧客CSVファイルを解析しようとして、無制限の再帰的サブプロセスループを発生させていたのです。さらに最悪なことに、非圧縮のチャンクファイル18GBを /tmp に直接書き出したため、ルートパーティションの空き容量が100%枯渇していました。

アプリケーションサーバー上で標準のPython exec() を呼び出す行為は、モデルのハルシネーションをいつでもインフラ障害に直結させるリスクを孕んでいます。今回のケースでは、たった1つの不正なプロンプトによって、2つの重要な内部サービスが約45分間にわたりダウンしました。もしあなたのアプリケーションがモデルの生成したコードを実行しているなら、サンドボックス化は必須の要件です。

根本原因の分析:ローカル実行モデルが破綻する理由

暴走していた親プロセスに kill -9 を発行した後、ポストモーテム(事後検証)を実施し、ローカル実行における3つの構造的欠陥が浮き彫りになりました。

  • プロセスのエスケープとリソース枯渇: 信頼できないスクリプトを subprocess.Popen や eval() で実行すると、ホストのカーネル、スワップ領域、ネットワークインターフェースを共有することになります。制御を失った while True ループや、制限のないNumPy配列のメモリ確保が発生すると、隣接するワーカープレーンのスレッドが一瞬で枯渇します。
  • ASTフィルターの限界: os や sys などのモジュールを対象とした静的解析や構文ブラックリストは、ほとんど役に立ちません。攻撃者や独創的なLLMは、getattr() や __import__('sub' + 'process') のような動的インポート、あるいはBase64ペイロードを用いて、安易な文字列フィルターを容易にバイパスします。
  • 共有ステートの汚染: エージェントが実行Aで一時ファイルを作成したり環境変数を変更したりすると、実行Bがそれらのアーティファクトを引き継いでしまいます。このような状態の漏洩は実行の再現性を損ない、マルチテナント環境において深刻なセキュリティ上の盲点を生み出します。

実行戦略の比較検討:サブプロセスからMicroVMへ

本番アーキテクチャを決定するにあたり、エージェントのベンチマーク実行を500回実施し、3つの実行モデルを比較検証しました。

1. リソース制限付きのネイティブサブプロセス

import subprocess

# 危険: ホストのファイルシステム、メモリ空間、ネットワークを共有してしまう
res = subprocess.run(["python3", "-c", agent_code], capture_output=True, timeout=10)

Linuxのcgroupsや ulimit を使えばCPUサイクルやメモリヒープを制限できますが、完全な隔離を実現することは不可能です。不正な外部ネットワークへのリクエスト送信や、/etc 配下の機密ローカルファイルの読み取り、ディスク容量の枯渇攻撃などを防ぐことはできません。

2. エフェメラルなDockerコンテナ

タスクごとにコンテナ化すればファイルシステムの漏洩は防げますが、起動時間が実行ごとに1.5〜3.2秒ほどかかります。この遅延は、マルチターンのLLMループにおいてユーザー体験を著しく損ないます。さらに、Kubernetesのワーカーノード上でDocker-in-Dockerを実行するには特権アクセスが必要となり、コンプライアンス上の警告を引き起こし、攻撃対象領域を広げる要因になります。

3. E2BクラウドサンドボックスによるFirecracker MicroVM

E2Bは、隔離されたFirecracker MicroVMを約150〜250ミリ秒で高速起動します。各サンドボックスは、専用のCPU、メモリ、仮想ネットワークインターフェースを備えた最小限のLinuxカーネルを起動します。実行が完了すると環境はクリーンに破棄され、サーバー上にステートが残ることは一切ありません。

ステップ別実装:E2Bによるエージェントワークロードの隔離

コード実行を隔離されたE2Bサンドボックスにオフロードしたことで、サーバーの安定性に関する課題は完全に解消されました。以下のステップに従って、Pythonで堅牢な実行ランナーを構築してみましょう。

ステップ1: SDKのインストールと認証情報の設定

E2Bコードインタープリターパッケージとpython-dotenvをインストールします。

pip install e2b-code-interpreter python-dotenv

シェル環境にAPIトークンを設定します。

export E2B_API_KEY="e2b_your_actual_api_key_here"

ステップ2: サンドボックス実行ラッパーの構築

この実装では、エージェントが生成した生のコードを隔離環境に渡し、標準ストリームをキャプチャし、厳格な実行タイムアウトを強制します。

import os
from e2b_code_interpreter import Sandbox

def run_untrusted_agent_code(script: str) -> dict:
    """
    隔離されたE2B MicroVM内で任意のPythonコードを実行します。
    ホストに触れることなくstdout、stderr、ランタイムエラーを返します。
    """
    try:
        # 新しいエフェメラルMicroVMを起動
        with Sandbox() as sandbox:
            execution = sandbox.run_code(
                script,
                timeout=30  # 30秒の厳格な上限
            )
            
            return {
                "status": "success",
                "stdout": [out.text for out in execution.logs.stdout],
                "stderr": [err.text for err in execution.logs.stderr],
                "error": execution.error.name if execution.error else None,
                "error_value": execution.error.value if execution.error else None
            }
    except Exception as exc:
        return {
            "status": "sandbox_failure",
            "error": str(exc)
        }

# テストペイロード: メモリ上で大量の計算を実行
risky_agent_payload = """
import sys
import time

print("セキュアなサンドボックス内で分析を実行中...")
data = [x ** 2 for x in range(500000)]
print(f"{len(data)} 件のアイテムを正常に処理しました。")
"""

result = run_untrusted_agent_code(risky_agent_payload)
print(result)

ステップ3: 安全なファイル取り込みとアーティファクトの取得

ホストディレクトリをエージェント環境に直接マウントしてはいけません。SDK経由でVMの仮想ディスクに入力ペイロードを書き込み、データを処理した上で、生成されたアーティファクトをアプリケーションのメモリにストリーミングして取得します。

from e2b_code_interpreter import Sandbox

with Sandbox() as sandbox:
    # サンプルデータセットをVM環境にアップロード
    csv_payload = "id,metric\n1,42\n2,85\n3,128\n"
    sandbox.files.write("/home/user/input.csv", csv_payload)
    
    # サンドボックス内でデータ変換を実行
    sandbox.run_code("""
import pandas as pd
df = pd.read_csv('/home/user/input.csv')
df['scaled'] = df['metric'] * 1.5
df.to_csv('/home/user/output.csv', index=False)
    """)
    
    # 処理された結果を文字列またはバイト列として取得
    processed_content = sandbox.files.read("/home/user/output.csv")
    print(processed_content)

エンジニアリングチーム向けの3つの本番ガードレール

エージェントによるコード実行を本番ユーザー向けにリリースする前に、以下のベースラインポリシーを徹底してください。

  1. 厳格なタイムアウトの設定: sandbox.run_code() には必ず明示的に timeout 引数(例: 30〜60秒)を渡してください。生成されたコードを無期限に実行させてはいけません。
  2. コンテキストクリーンアップの必須化: すべての呼び出しをPythonの with Sandbox() コンテキストマネージャーでラップしてください。これにより、終了時にMicroVMが確実に即時破棄され、放置されたインスタンスによる余計な課金を防ぐことができます。
  3. 外部ネットワーク送信(Egress)の制限: エージェントが静的なCSVやJSONデータセットの分析のみを行う場合は、サンドボックスのテンプレートで外部ネットワークアクセスを無効化してください。外部トラフィックを遮断することで、データの持ち出し(データ流出)や不正な外部リクエストを未然に防ぐことができます。
Share: