TL;DR: エージェントは2分間だけ生きます。資格情報(クレデンシャル)は60分間生きます。その不一致が攻撃面です。タスク単位で発行され、短命な資格情報を発行するブローカーなら、スパrawlが始まる前にそのギャップを埋められます。
AIエージェントはまだ新しい存在です。多くのチームは、ようやく最初のエージェントを大規模にデプロイし始めたところです。2026年は1年目。そして、アイデンティティに関する会話の多くは、すでに「混乱(メス)が存在する」という前提で進んでいます。レジストリ、インベントリ、権限(エンタイトルメント)の棚卸し、棚卸し結果のレビュー、後始末のワークフローなどです。
しかし、その混乱は避けられないものではありません。最初にあなたが選ぶものです。
すべてのエージェントがスポーン時に短命でタスクスコープの資格情報を得るブローカーから始めれば、個々のエージェントの資格情報が、永遠に追跡し続ける別の長寿命の“何か”にならずに済みます。
これが予防の論拠です。持続するものは統制するが、持続しないものには一時的な資格情報を発行する。
誰も語らない問題
いま現在、ほとんどのチームはエージェントへの資格情報付与を次の3通りのいずれかで行っています:
- 共有のサービスアカウントで静的APIキー。 すべてのエージェントが同じキーを使います。1つが侵害されたら、キーをローテーションしないといけなくなり、すべてが壊れます。
- TTLが15〜60分のOAuthトークン。 エージェントは短いタスクで動きますが、資格情報はそれよりずっと長い時間有効のままです。
- 「念のため」で広すぎるIAMロール。 起こり得るあらゆるタスクに対処できるだけ広くスコープします。エージェントが侵害されたとき、そのエージェントはすべてにアクセスできてしまいます。
共通点はこれです:資格情報が、実行(作業)より長生きする。 エージェントは一時的です。資格情報はそうではありません。この不一致が攻撃面になります。
資格情報露出(エクスポージャー)の数学
具体化してみましょう。
| アプローチ | エージェント稼働時間 | 資格情報の有効期間 | 露出ウィンドウ |
|---|---|---|---|
| 静的APIキー | 2分 | 永遠 | 永遠 |
| OAuthトークン | 2分 | 60分 | エージェント稼働時間の30倍 |
| ブローカー(タスクスコープ) | 2分 | 短いTTL+リリース/失効 | タスクの稼働時間に近い |
規模を大きくすると、その違いは学術的ではありません。正確な数字はワークロード、TTL、更新ポリシーによって変わりますが、リスクの“形”は同じです。
2分間のエージェント実行タスクを、60分間のトークンで裏付けると、盗まれた資格情報がまだ使える58分間が余計に残ります。これを何千ものエージェント実行に掛けると、毎日ものすごい量の不必要な資格情報の“有効期間”を生み出していることになります。
資格情報が盗まれたとき、攻撃者はエージェントが実際に行っていたことへのアクセスは得られません。代わりに、その資格情報ができてしまうはずのすべてに、資格情報が有効である限りアクセスできるようになります。
ブローカー対レジストリ:2つの思想
レジストリモデル: 永続的なシステム、アプリケーション、所有者、ポリシー、監査証跡を登録し、統制します。それは有用です。しかし、すべての短命なエージェントインスタンスまでもが永続的なアイデンティティ記録になるなら、何千ものアイデンティティ、エンタイトルメント、後始末タスクが蓄積されます。
その時点で、レジストリの価値提案は「スパrawlを管理するのを手伝います」というものになります。
ブローカーモデル: すべてのエージェントはスポーン時に資格情報を受け取ります。その資格情報は、そのタスクが必要とするものに正確にスコープされます。短いTTLを持ち、作業が終わったらリリースまたは失効できます。永続的な統治レイヤーはエージェントの上に依然として存在しますが、エージェントごとの資格情報が“待機状態の(常設の)エンタイトルメント”になるわけではありません。
ブローカーは、少なくともある程度のスパrawlは防止可能だと見なします。その価値提案は「そもそも長寿命なエージェント資格情報を作らない」です。
予防は、たいてい後始末より安上がりです。古くなったアイデンティティが少ない。定期的なアクセスレビューが少ない。「この古いエージェントはなぜまだアクセスできていたのか?」という事故も少ない。
コードだとどのように見えるか
同じエージェント、同じシステムプロンプト、同じLLM、同じ意思決定です。変わるのは資格情報だけです。
3つの例はすべてここから始まります:
import json
from openai import OpenAI
llm = OpenAI()
# システムプロンプトは、このエージェントが何者で、どのツールを呼び出せるかを定義します。
system_prompt = """あなたはカスタマーサポートのエージェントです。あなたには次のツールがあります:
- lookup_billing:顧客の請求履歴を取得する
- edit_account:顧客のアカウントを編集する'sアカウント情報
- lookup_billing_all:すべての顧客にわたる請求履歴を取得する
顧客の依頼に応じて適切なツールを使用してください'''"""
# リクエストが届きます。LLMがどのツールを呼び出すべきかを判断します。
response = llm.chat.completions.create(
model="gpt-4.1",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": "顧客12345の請求履歴は何ですか?"},
],
tools=tools, # lookup_billing, edit_account, lookup_billing_all
)# LLM は顧客 12345 に対して lookup_billing を選択しました。
# LLM の判断から customer_id を抽出します。
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
customer_id = args["customer_id"] # "12345"
# 次に: エージェントは、その呼び出しを行うための認証情報をどのように取得するのでしょうか?
静的 API キー(今日のほとんどのチームがやっている方法):
# 同じ LLM の判断。上で抽出した同じ customer_id。
# 認証情報は、環境に埋め込まれた共有キーです。
api_key = os.environ["SHARED_API_KEY"]
headers = {"Authorization": f"Bearer {api_key}"}
result = requests.get(
f"https://api.example.com/customers/{customer_id}/billing",
headers=headers,
)
# これは動きます。しかし、その同じキーで次のようにもできます:
# requests.get("https://api.example.com/customers", headers=headers)
# ...そしてすべての顧客を取得します。このキーは、LLM が 1 件だけ要求したことを
# 知らず、気にもかけません。そして有効期限もありません。
OAuth トークン(より良いが、それでも不一致):
# 同じ LLM の判断。 同じ customer_id。
# 顧客は 1 人で十分だと分かっています。しかし OAuth スコープは
# クライアント単位で定義され、タスク単位ではありません。別の OAuth クライアントを
# 顧客ごとに用意しない限り、「ただ顧客 12345 だけ」のトークンを発行できません。
token = get_oauth_token(client_id=os.environ["CLIENT_ID"],
client_secret=os.environ["CLIENT_SECRET"],
scope="read:customers:*") # 必要なので広めにしています
headers = {"Authorization": f"Bearer {token}"}
result = requests.get(
f"https://api.example.com/customers/{customer_id}/billing",
headers=headers,
)
# これも動きます。しかしトークンはすべての顧客を読み取れてしまい、
# 有効期限は 60 分です。エージェントは 2 で完了です。
ブローカー(タスクスコープ、エフェメラル):
from agentwrit import AgentWritApp
from agentwrit.errors import AuthorizationError
app = AgentWritApp(
broker_url=os.environ["AGENTWRIT_BROKER_URL"],
client_id=os.environ["AGENTWRIT_CLIENT_ID"],
client_secret=os.environ["AGENTWRIT_CLIENT_SECRET"],
)
返却形式: {"translated": "翻訳されたHTML"}# 同じLLMの判断。同じcustomer_id。
# ただし、今回の認証情報はLLMが要求した内容に厳密にスコープされています。
try:
agent = app.create_agent(
orch_id="billing-agent",
task_id=f"billing-{customer_id}",
requested_scope=[f"read:data:customer-{customer_id}"],
)
except AuthorizationError as e:
# ブローカーが「ダメ」と言う。スコープが、このアプリが発行してよい範囲を超えている。
print(e.problem.detail) # "scope exceeds app ceiling"
print(e.problem.error_code) # "scope_violation"
raise
# アプリ経由でトークンを検証する。ブローカーの署名付きクレームを返す。
check = app.validate(agent.access_token)
print(check.claims.scope) # ['read:data:customer-12345'] -- それ以外はなし
result = httpx.get(
f"https://api.example.com/customers/{customer_id}/billing",
headers=agent.bearer_header,
)
# これは動く。しかし、このトークンで「全顧客」を取得しようとする何かがあれば
# APIはスコープをチェックし、それを拒否する。
# 完了。今すぐブローカーでトークンを無効化する。58分後ではない。
agent.release()
3つの例はいずれも同じLLM、同じシステムプロンプト、同じツール呼び出し、同じcustomer_idを使っています。APIと通信するコードはほぼ同一です。違いは認証情報です。
静的キーとOAuthトークンでは、開発者は既に自分が必要なのは顧客12345だけだと分かっています。しかし、認証情報はそれを強制できません。タスクごとにスコープするには広すぎ、実行時にそれを絞り込む仕組みもありません。
ブローカーの場合、認証情報はLLMの実際の判断から構築されます。customer_idが直接requested_scopeに流れ込みます。トークンができるのは、LLMが要求したことだけで、それ以上はできません。LLMが別のツールや別の顧客を選んでいたなら、スコープも別になっていたはずです。そして要求されたスコープが、アプリが発行してよい範囲を超えているなら、エージェントがいかなるデータにも触れる前にブローカーが拒否します。
Multi-Agent Delegation: The Attack Vector Nobody Is Talking About
ここから面白くなります。
ほとんどの本格的なエージェントの導入では、複数のエージェントが協調して動きます。エージェントAが調査し、エージェントBが下書きを作り、エージェントCがレビューし、エージェントDが公開します。あるエージェントの出力が次のエージェントの入力になります。
問題: エージェントAは、どのようにしてエージェントBに自分の代わりに行動する権限を与えるのでしょうか?
素朴なアプローチ:エージェントAが認証情報をエージェントBと共有する。これでエージェントBはエージェントAの権限を持つことになります。もしエージェントAがすべての顧客を読み取れるなら、エージェントBもそうです。権限が拡大されます。これは認証情報の権限昇格であり、多くのエージェント構成では簡単に起こってしまいます。
レジストリのみのアプローチ:エージェントBは自分自身の恒常的なアイデンティティと権限を持ち、その権限が引き続き正しいことを後から統治(ガバナンス)で証明できるようにします。
ブローカーのアプローチ:委譲チェーンの検証。
エージェントAがエージェントBに委譲するとき、エージェントAは次のことを述べたトークンを渡します:
"エージェントAは、エージェントAの現在のスコープとまったく同じスコープで、エージェントBに自分の代わりに行動することを許可した。エージェントBは権限を昇格できない。委譲は暗号学的に署名され、かつ時間的に制限されている。"
もしエージェントAがread:data:customer-12345を持っているなら、エージェントBはread:data:customer-12345を受け取ります。read:data:*ではありません。write:data:customer-12345でもありません。エージェントAが持っていたものとまったく同じで、それ以上はありません。
委譲チェーンは、一連の署名付きトークンです。各リンクが直前のものに結び付けられているため、リソース層は、委譲されたトークンを互いに無関係なものとして扱うのではなく、系譜(lineage)を検証できます。
これは単なる機能ではありません。私が最も重視するセキュリティ特性です。委譲は権限を維持するか、減らすべきであって、決して増やしてはいけない、という性質です。
Why Now
2026年は、エージェントをスケールして導入するための第1年です。多くのチームは、まさに今これを突き止めようとしているところです。今後12か月で行うアーキテクチャ上の判断は、何年も持続します。
今日、長寿命のエージェント認証情報を仕込んでしまうと、今後2年間ずっと後片付けに追われることになります。アクセスレビュー。権限(entitlement)の監査。「誰がいつ何にアクセスしていたか」というフォレンジック。あるいは、まったく後片付けされない可能性すらあります。企業向けベンダーは、この混乱を管理するツールを売り込みます。なぜなら、その混乱は本当に起きるからです。
ブローカーから始めれば、エージェントの周辺にある永続的なシステムに対しては、やはりガバナンスが必要です。しかし、短寿命のエージェントインスタンスは、恒常的な認証情報を残しておく必要はありません。
レジストリのベンダーが「スパrawl(増殖)が問題だ」と言っているのは間違っていません。ですが、私は彼らがそれを「すべて必然だ」と考えすぎていると思います。
The Argument, Not the Pitch
私はあなたにAgentWritを売り込むためにここにいるのではありません。今日選ぶ認証情報モデルが、今後5年間のセキュリティ姿勢を決めるのだ、という主張をしに来ています。
長寿命の認証情報と、レジストリ管理によるスパrawlから始めるなら、あなたは「後片付け、監査、蓄積したリスク」の未来を選ぶことになります。
一時的で、タスクにスコープされた認証情報から始めるなら、あなたは「認証情報が作業を超えて生き残らない未来」「委譲が権限を昇格しない未来」「個々のエージェントインスタンスが恒久的な権限(entitlement)にならない未来」を選ぶことになります。
ブローカー・モデルは新しいものではありません。クラウドネイティブなシステムが、短命な計算(compute)を何年も前から扱ってきたやり方です。VMは起動時に資格情報(credentials)を受け取ります。コンテナは開始時に資格情報を受け取ります。サーバーレス関数は呼び出し(invocation)ごとに資格情報を受け取ります。資格情報は、その計算とともに死にます。
エージェントは、知的であること以外は単に計算(compute)です。同じ原則が当てはまります。
What I Built
私は、これが自分自身のエージェント導入のために必要だったので、AgentWritを作りました。これはAIエージェント向けのセルフホスト型資格情報ブローカーで、内部導入用のPolyForm Internal Useのもと、ソースは公開されています。
- エフェメラルなアイデンティティ: 各エージェントは、固有の暗号学的アイデンティティを持って生成されます
- タスクスコープのトークン: 広範なIAMロールではなく、必要なタスクに正確にスコープされます
- 短命な資格情報: トークンは分単位で期限切れになり、早期に解放または無効化できます
- 4段階の無効化: トークン、エージェント、タスク、または完全な委譲チェーン(delegation chain)
- 委譲チェーンの検証: 各ホップごとに権限が拡張することはできず、暗号学的に強制されます
Goで書かれています。Dockerで動作します。ブローカーはPolyForm Internal Use 1.0.0のもとでソース公開されています。Python SDKはMITライセンスで、PyPIで公開されています。
GitHub: https://github.com/devonartis/agentwrit
Python SDK: https://github.com/devonartis/agentwrit-python
セキュリティ・パターン(CC BY-SA 4.0): https://github.com/devonartis/AI-Security-Blueprints
このパターンは、OWASP Agentic Top 10(2026)、NIST IR 8596、IETF WIMSEに沿っています。単一の実装よりもアーキテクチャのほうが重要だからです。
完全なセキュリティ・アーキテクチャは、Zenodoのプレプリントとしても公開されています。
Try It in 10 Minutes
ブローカーのDockerイメージを取得し、Python SDKをインストールして、2つのデモのうちいずれかをエンドツーエンドで実行してください。
MedAssistはFastAPIの臨床アシスタントです。あなたは患者について平易な言葉で質問します。LLMがツール(記録、検査、請求、処方)を選択します。このアプリは要求に応じてブローカーのエージェントを生成し、それぞれが「1人の患者」と「1つのカテゴリ」にスコープされます。患者をまたぐ質問は拒否されます。処方の書き込みは委譲チェーンを通して流れます。
demo/README.mdには、実行手順、シナリオのプレイブック、およびコードマップがあります。
Support TicketsはFlask + HTMX + SSEで構築された3エージェントのパイプラインです。顧客チケットを処理する3つのLLM駆動エージェント(トリアージ、知識、応答)があります。匿名チケットはトリアージで停止します。delete_accountやsend_external_emailのような危険なツールはLLMのツール一覧には含まれますが、エージェントのスコープにはないため、決して実行されません。あるシナリオでは意図的にrelease()をスキップして、5秒のTTLが自分自身で死ぬ様子を観察します。
demo2/README.mdには、実行手順、5つのシナリオ、そしてコードマップがあります。
The Question
あなたは今日、どのようにエージェントに資格情報を付与していますか?
もし答えが共有APIキー、長寿命OAuthトークン、または広範なIAMロールを含むなら、あなたは「後でレジストリのベンダーがあなたに管理ツールとして売り込む」ような混乱を作っているかもしれません。
まずは予防から始めてください。後でクリーンアップするより、エージェントの資格情報を置かないほうが安く済みます。
Devon Artis。プリンシパル・セキュリティ・エンジニア。CSA AI Controls Matrixの貢献者。エフェメラル・エージェント・資格情報付与パターンをZenodoのプレプリントとして公開。AgentWritを開発。ひとり、VCなし。



