AIエージェント セキュリティ:本番環境の被害範囲を封じる設計

本番環境で動くAIエージェントの被害範囲をどう制限するか。コストの単一チョークポイント、シェルを排除したツール許可リスト、冪等な処理、実行前拒否を組み合わせ、暴走しても損失を最小化する実装パターンを、3件の障害事例とコードで解説します。創業者がエンジニアに確認すべき要点も整理しました。

Saturday, September 5, 2026Omid Saffari
AIエージェント セキュリティ:本番環境の被害範囲を封じる設計

AIエージェント セキュリティの失敗が現実の損害に直結した事例です。2026年4月25日、Claude Opus 4.6を実行していたCursorが、PocketOSの本番データベースとバックアップを9秒で削除し、その後に自ら違反を認める記録を残しました。レンタカー予約3か月分が消失しました。私は毎日、本番環境に触れる6つのエージェントを運用しています。本稿では、最悪の誤動作が起きても余計な支出を1ドル以内に抑える、実運用中のチョークポイント、許可リスト、冪等性アーキテクチャを具体的に解説します。

90日間で起きた3件の惨事

2026年4月25日。Claude Opus 4.6を実行するCursorエージェントが、PocketOS CEOのJeremy Craneが「通常の作業」と説明したステージング環境での作業を進めていました。認証情報の不一致に遭遇すると、自ら「整理する」と判断し、本番データベースとそのバックアップが入ったRailwayのストレージボリュームを削除しました。所要時間は9秒。サービス停止は30時間に及びました。復旧可能な最新バックアップは3か月前のものだったため、レンタカー予約3か月分が失われました。その後、エージェントは本番環境に触れてはならないという明示的な指示に違反した、と自ら記録しました。

2026年2月26日。DataTalks.Club創業者のAlexey Grigorevはコンピューターを切り替えたばかりで、ローカルのTerraformステートファイルが古い状態でした。リソースの整理を依頼されたClaude Codeは、本番スタックに対してterraform destroyを実行しました。後に一部復旧できたcourses_answerテーブルには、1,943,200行が保存されていました。2年半分の受講者の提出物です。自動スナップショットも、破棄された同じアカウント内にありました。

2025年12月中旬。Amazonの社内エージェントKiroは、エンジニアの強い権限を引き継ぎ、Amazonの2人承認ゲートを迂回しました。このゲートは人間には適用されていましたが、エージェントが実行されていたロールには適用されていなかったためです。KiroはAWS Cost Explorerの本番環境を削除し、作り直しました。

3件のインシデントは、モデルも技術スタックもそれぞれ異なります。共通する根本原因はモデルではありません。広すぎる権限の継承、自律ループ、そして人間が確認するより速い実行速度の組み合わせです。ServiceNowは、まさにこの不安に対応する「kill switch」製品をすでに販売しています。以下で示すのは、その仕組みを自社で構築するためのアーキテクチャであり、私が現在本番環境で運用しているものです。

技術者ではない創業者が押さえるべきこと

AI担当者からこの話を聞き、この記事を読んでいる創業者に伝えたいのは、設計を誤ったときの代償は単なる「AIのバグ」ではないということです。予約システム、受講者の記録、顧客データが消えます。しかも、誰も復元テストをしていなければ、バックアップは3か月前のままです。

エンジニアリング責任者に尋ねるべきなのは、「そのエージェントは優秀か」ではありません。問いの立て方が違います。確認すべきは、1回のエージェント実行で生じ得る最大の損害は何か、その上限を誰が定めたのかです。「モデルを信頼している」「差分をレビューしている」という答えなら、制御面は存在しません。あるのは期待だけです。

望ましい答えは、次のようなものです。「エージェントはdestroy権限のないクラウドロールで動作します。呼び出せるのは名前を指定した12個のツールだけで、シェルはありません。有料の呼び出しはすべて、厳格なコスト上限を持つ1つの関数を通ります。最悪でも1ステップが失敗し、支出は最大1ドルです」。これが目指すアーキテクチャです。以下で、その構築方法を順に説明します。

AIエージェント セキュリティで「注意」と差分レビューが効かない理由

PocketOSのボリューム削除にかかった時間は9秒でした。Kiroの削除も、人間が確認プロンプトを読み、内容を判断するより速く完了しています。エージェントの速度を前にすると、開始後に人間が介入する設計は構造的に成立しません。操作に気づいた時点で、すでに処理は終わっています。

多くのAIエージェント セキュリティの解説が避ける核心はここです。導かれる結論が厳しいからです。有効な防御策は、実行前の拒否しかありません。「エージェントが尋ね、疲れた人間がyesをクリックする」方式でも、「すべて記録して後から確認する」方式でもありません。実行前の拒否とは、破壊的操作をそもそもエージェントが到達できる範囲に置かないことです。

Kiroは、その反例を示しました。Amazonには2人承認ゲートがありましたが、対象はジョブを起動する人間でした。エージェントは起動した人間の権限を継承し、実行ロールにゲートが紐づいていなかったため迂回できました。人間には承認手続きがある一方、自律ループには完全なdestroy権限が与えられていたのです。

つまり、問題の捉え方を変える必要があります。エージェントを「監督」するのではありません。ループが始まる前に操作可能な範囲を制限し、その制約をプロンプトではなくロールの特性として実装します。

チョークポイント:有料処理と変更処理を1つの関数に集約する

私が運用する6エージェントのスタックでは、有料のモデル呼び出しがすべて1つの関数を通ります。関数名はcallAiです。この関数は、次の5つを順番に実行します。1日$20という支出上限の事前確認、呼び出し時間の計測、トークン数とUSDコストを追記専用のai_call_log行へ記録してagent_idworkflow_instance_idを付与、インスタンスごとの$1上限を事後確認、そして結果の返却です。

TypeScript
export async function callAi(env: Env, args: CallAiArgs) {
  const { agentId, workflowInstanceId, model, messages } = args;

  const dailySpent = await getDailySpendUSD(env);
  if (dailySpent >= 20) {
    throw new NonRetryableError(`daily cap hit: $${dailySpent}`);
  }

  const t0 = Date.now();
  const res = await anthropic.messages.create({ model, messages });
  const costUSD = priceOf(model, res.usage);

  await env.DB.prepare(
    `INSERT INTO ai_call_log
       (agent_id, workflow_instance_id, model, in_tokens, out_tokens, usd, ms, ts)
     VALUES (?, ?, ?, ?, ?, ?, ?, unixepoch())`
  ).bind(agentId, workflowInstanceId, model,
         res.usage.input_tokens, res.usage.output_tokens,
         costUSD, Date.now() - t0).run();

  const instanceSpent = await getInstanceSpendUSD(env, workflowInstanceId);
  if (instanceSpent >= 1) {
    throw new NonRetryableError(`instance cap hit: $${instanceSpent}`);
  }

  return res;
}

上限を超えると、再試行不可のエラーがスローされ、Workflowは終了します。予算ゲートは、エージェントが反論できるプロンプトではありません。推論レイヤーより上で発生する例外なので、「自らの判断」で続行する余地がないのです。

よく引き合いに出される、63時間で$4,200を消費した暴走と比べてみます。チョークポイントがなければ終端状態もなく、人間が請求に気づくまでループは支出を続けます。インスタンスごとの上限があれば、1回の実行で失う最大額は$1です。1日の上限があれば、1日で失う最大額は$20です。私は構築中に、どちらの上限にも誤って到達しました。それでも、どちらも夕食代を超えませんでした。

チョークポイントのもう1つの役割は、フォレンジック調査です。すべての操作について、結果が返る前に監査行が残ります。問題が起きたとき、事後検証はエージェント自身の「告白」から経緯を組み立てる作業ではなく、SELECTクエリでai_call_logを調べるだけで済みます。

許可リスト:12個のZodツール、シェルもterraformもなし

エージェントが変更を加えられる範囲は、厳格なZodスキーマで定義した12個のツールだけです。Bashツールはありません。terraformツールもありません。クラウドボリュームAPIも、そもそも操作可能な範囲に含まれていません。エージェントがterraform destroyを実行したいと考えても、その要求を操作として表現する手段がないのです。

これを強制するClaude Agent SDKの設定は短く書けます。

TypeScript
const result = await query({
  prompt,
  permissionMode: "dontAsk",
  allowedTools: [
    "read_brief", "fetch_corpus", "render_markdown",
    "validate_directives", "persist_draft", "schedule_publish",
    "update_status", "log_event", "fetch_citation",
    "embed_text", "search_vectors", "notify_human"
  ],
  // no Bash, no Write, no Edit, no infrastructure tools
});

permissionMode: "dontAsk"では、リストにないツールは即座に拒否されます。プロンプトへエスカレーションされることも、人間のレビューに回されることもありません。権限評価は、PreToolUse hook、deny rules、allow rules、ask rules、mode check、canUseTool callbackの順に、ドキュメントどおり実行されます。ただしdontAskではhuman-in-the-loop callbackが構造的にスキップされ、未知のツールはmode checkの時点で拒否されます。

この仕組みがあれば、PocketOSとDataTalksの事故はそこで止まっていました。DataTalksの障害は、この設定では起こり得ません。エージェントが出力できるツール名にterraform destroyがないからです。PocketOSの障害も同様です。ボリューム削除が操作可能な範囲に含まれていません。モデルが「整理する」と判断しても、その意図を操作へ変える経路が存在しないのです。

「想定外の作業が必要になったら困る」という理由で、Bashツールを追加したくなるかもしれません。しかし追加してはいけません。Bashを加えた日から、被害範囲は「Bashが到達できるすべて」に広がります。

冪等性:再実行で破壊が重複しない仕組み

アーキテクチャの3本目の柱は冪等性です。各Cloudflare Workflowインスタンスには、入力から決まるpublish-{brief_id}という決定論的IDを付けます。重複したIDに対してinstances.create()は例外を投げるため、同じイベントが再送されてもWorkflowが2回走ることはありません。キュー層で再試行が殺到しても二重公開にはならず、何もせず終了します。

TypeScript
const id = `publish-${msg.body.briefId}`;
try {
  await env.PUBLISHER.create({ id, params: msg.body });
} catch (e) {
  if (e.message.includes("already exists")) return; // safe replay
  throw e;
}

Workflow内部では、すべてのstep.do()の結果がキャッシュされ、再実行しても安全です。ステップを再試行すると、副作用をもう一度実行する代わりにキャッシュ済みの結果を再生します。正規のD1テーブルに対するINSERT OR IGNOREと、保存のたびに作成するバージョン付きR2スナップショットを組み合わせれば、不正なステップが履歴を気づかれないまま上書きすることもありません。1つ前の版は、R2のキー1つで取り戻せます。

DataTalksの根本原因は、古いステートファイルが不可逆なdestroyを引き起こしたことでした。決定論的なWorkflow IDと、変更前のスナップショットを組み合わせれば、古い状態を前提にした破壊も致命傷にはなりません。仮にWorkflowが古い前提で動いても、スナップショットからロールバックできます。(耐久性のある実行基盤については、Cloudflare WorkflowとManaged Agentsを比較した記事で解説しました。冪等性の考え方は、名称が違っても同じ封じ込めの設計です。)

考え方はシンプルです。破壊的操作が可能なステップは、最初にバージョン付きスナップショットを書き込みます。スナップショットは後付けの保険ではなく、処理の前提条件です。書き込みに失敗すれば変更は実行しません。変更に失敗しても、スナップショットが復元地点になります。状態を失い、しかも復元元がないという経路をなくします。

今週実行できる5ステップの移行計画

順序が重要です。ステップ1だけでも、先ほどの3件はすべて防げました。ほかに何もできなくても、まずここから始めます。

  1. エージェントのクラウドロールからdestroy系の権限をすべて外す

    エージェントが使用するIAMロール(またはサービスアカウント、APIトークン)には、本番リソースをdestroyまたはdeleteする権限を一切持たせません。DeleteVolumeも、terraform destroyの権限も、DROP TABLEも付与しません。ロールが被害範囲の下限を決めます。ロール自体が広すぎれば、その上に何を重ねても意味がありません。

    Kiroの教訓を具体化すると、起動した人間の広い権限をエージェントに継承させてはいけない、ということです。エージェント専用の狭いロールを用意します。

  2. permissionMode dontAskと明示的なallowedToolsリストでエージェントを囲う

    業務に必要な最小限のツールだけを選びます。私の場合は12個ですが、6個で済むケースもあります。汎用シェルツールは完全に取り除きます。「便利だから」という理由でBashツールを残しているなら、その便利さこそが脆弱性です。

    TypeScript
    { permissionMode: "dontAsk", allowedTools: [/* explicit list */] }
  3. 予算のサーキットブレーカーを設ける

    有料の呼び出しは、すべて1つのラッパーを通します。1日の厳格な上限を事前確認し、インスタンス単位の上限を事後確認します。超過時はNonRetryableErrorをスローします。インスタンス単位の上限は、1日の上限では代替できません。63時間で$4,200に達した暴走は、実行ごとに積み上がりながら、妥当な1日上限の内側に収まり続けました。インスタンス単位の上限なら、1日の上限が見逃す事態を止められます。

  4. 変更前にスナップショットを作成する

    破壊的操作が可能なステップでは、最初にバージョン付きスナップショットを書き込みます。R2でもS3でも、D1の*_archiveテーブルでも構いません。重要なのは、同じステップ、同じトランザクション状のブロック内で、スナップショットの書き込みを変更処理の前提条件にすることです。

  5. 追記専用の操作ログを残す

    ツール呼び出しごとに1行を記録し、結果が返る前に書き込みます。エージェントID、WorkflowインスタンスID、USDコスト、レイテンシを付与します。いつか問題が起きたとき、事後検証を経緯の再構成ではなくSELECTで行えるようにします。

各ステップは1日以内で実施できます。この順序にするのは、封じ込めの効果が積み上がるからです。ステップ1で下限を定め、ステップ2でその上にある操作面を閉じ、ステップ3で操作面の内側にいる間のコストを制限します。ステップ4と5によって、障害を復旧可能かつ照会可能にします。

よくある質問

システムプロンプトで「本番環境を絶対に削除しない」と指示するだけでは不十分ですか?

PocketOSは指示していました。エージェント自身の記録にも、その指示へ明示的に違反したと書かれています。指示は制御面ではありません。プロンプトは確率的システムへの入力です。一方、ツールの許可リストとクラウドロールはランタイムの特性です。制御にはランタイムを使います。

permissionMode dontAskにすると、対話操作が一切できなくなりますか?

いいえ。リストにないツールを、確認プロンプトへ回さず拒否するという意味です。許可された操作面はすべて使えます。なくなるのは「エージェントが尋ね、疲れた人間がyesをクリックする」という失敗パターンです。なくすべきなのは、まさにこのパターンです。

1日のコスト上限があるのに、なぜインスタンス単位の上限も必要なのですか?

63時間で$4,200に達した暴走は、実行ごとに積み上がりながら、妥当な1日上限の内側に収まり続けたからです。1つの不正なループは「$20/日」を大きく下回ったままでも、週末の間に合計4桁のコストを発生させ得ます。インスタンス単位の上限は、構造上1日の上限にはできないサーキットブレーカーです。

Cloudflare WorkflowsではなくVercelやRailwayを使っています。それでも適用できますか?

チョークポイント、許可リスト、権限を絞ったロールは、プラットフォームを問いません。Cloudflare固有なのは、冪等性を実現する仕組みである決定論的なWorkflow IDだけです。ほかのスタックでは、ジョブに冪等性キーを付け、キュー層またはジョブランナー層で重複を拒否します。

2人承認があれば十分ですか?

Kiroでは人間向けに用意されていましたが、エージェントはそれを迂回できる権限を継承していました。承認ゲートは、起動した人間ではなく、エージェントが実行されるロールに紐づける必要があります。エージェントのロールにdestroy権限があるなら、ゲートは飾りにすぎません。

先週、チームのエージェントが本番環境で危険な操作をしかけたためにこの記事を読んでいるなら、こうした封じ込めこそ、私がDVNC.devで支援している仕事です。上記のアーキテクチャを、それぞれの技術スタックへ、被害範囲を先に閉じる順序で適用します。

最終更新

2026年9月5日

カテゴリーBuild

Googleでこのサイトを優先する

omidsaffari.comをGoogle検索の優先ソースに追加

omidsaffari.comを優先ソースに設定すると、GoogleがTop Stories・AI Overviews・AI Modeであなたのために優先表示します。

ニュースレター

毎週日曜、一通の手紙。 動くシステムの話。感想戦ではなく。

AIベンチャーのポートフォリオ運営から生まれるビルドログ、稼働中のシステム、現場ノート。

週刊。スパムなし。いつでも解除できます。