多個 Agent 共用 credential 時,企業會失去哪一些隔離、歸責與撤銷能力?
假設公司裡有兩個 AI Agent:
customer-research-agent 依照業務需求查詢 CRM,只需要 customer.read。support-ops-agent 根據客服案件更新 ticket,需要 ticket.read 與 ticket.update。兩者都是常駐服務,都會自主選擇 Tool、組合參數並連續執行 Action。為了部署方便,團隊讓它們共用 svc-agent-prod,再把同一個 API Token 放進兩個 runtime。
從維運角度看,這只少管一組 credential;從 Tool 的角度看,卻只剩一個 caller:svc-agent-prod。Tool 不知道這次 ticket.update 是 Support Agent 規劃的,還是 Research Agent 誤選了 Tool;更不知道是哪個部署版本、哪個 runtime instance 使用了那把 Token。
今天要回答的不是「Service Account 安不安全」,而是更精確的問題:Service Account 所代表的 identity boundary,是否和 Agent 真正的執行、授權與撤銷邊界一致?
先把本篇的三個核心提問說清楚:
一般後端服務也不該任意分享 privileged credential,這點並不新。Agent-specific 的差異,在於同一個 runtime 不是只執行開發者預先寫死的 API 路徑:Agent 會把自然語言目標轉成計畫,動態選 Tool、產生參數,還可能因 Tool 回傳結果繼續下一步。
因此,共用帳號壓平的不只是兩個 process,而是兩個具有不同目標、Tool 集合、模型/提示版本、資料範圍與行動風險的自主執行者。當 Research Agent 因錯誤 context 選到 ticket.update,安全控制不能把「它理論上不該選」當成 deny;Tool 只會根據可驗證的 caller 與 policy 判斷。若 caller 永遠都是 svc-agent-prod,PDP 就沒有足夠粒度區分兩個 Agent。
這也說明為什麼在 request header 多放一個 X-Agent-Name 不算修正。拿著共用 credential 的任何 process 都能自稱另一個 Agent;未被 issuer 或 runtime attestation 綁定的名稱只是 metadata,不是 identity evidence。
共用 Service Account 會同時造成四種結構性失敗。
共用 principal 若要讓兩個 Agent 都能工作,通常會拿到:
customer.read + ticket.read + ticket.update
這不是任何一個 Agent 的最小權限,而是整個 Agent pool 的權限聯集。Research Agent 一旦選錯 Tool、受到不可信內容影響,或程式遭入侵,就能使用原本只屬於 Support Agent 的 ticket.update。模型防護可以降低選錯機率,卻不能取代 Tool 端的 authorization。
Tool log 可能完整記下時間、Action 與參數,但 actor 永遠是 svc-agent-prod:
{
"actor": "svc-agent-prod",
"action": "ticket.update",
"resource": "TICKET-4821",
"decision": "allow"
}
事後只能證明「持有這組 credential 的某個 process」呼叫過 Tool,不能從該憑證證明是哪個 Agent。應用程式自行補上的 agent_name 可以協助除錯,但若 caller 能任意填寫,就不能成為可靠的歸責依據。
獨立 identity 也不會自動帶來法律或密碼學意義上的 non-repudiation;它只是讓簽章、受保護的 audit trail、policy version 與 deployment evidence 有一個正確的 principal 可以綁定。共享 bearer secret 則從源頭就消除了這個區分。
若發現 customer-research-agent:v17 行為異常,安全團隊面臨二選一:
svc-agent-prod,連健康的 Support Agent 一起中斷。即使緊急輪替 Token,只要新 Token 又同時散布到所有 Agent,identity boundary 仍沒有改變。Rotation 解決的是 key continuity,不是 principal separation。
RFC 6750 對 bearer token 的定義很直接:持有者不需要另行證明握有某把密鑰。當同一 Token 存在多個 image、環境變數、CI job 或 log path,任一處外洩都能冒充整個共用 principal。
對 Agent 而言,blast radius 不只涵蓋多個 API,也涵蓋多個 Agent 原本互相分離的行動空間。攻擊者不必控制每個 Agent;只要取得 Agent pool 的共用 authority 即可。
Service Account 可以是合理的 workload identity 載體。問題在於用同一個 principal 與同一份 credential代表安全屬性不同的 Agent。
Identity 粒度應對齊至少三種管理邊界:
| 邊界 | 要回答的問題 | 粒度不足的後果 |
|---|---|---|
| Policy boundary | 哪個 Agent 可對哪些 Resource 執行哪些 Action? | 只能授予權限聯集 |
| Revocation boundary | 哪個 Agent、deployment 或 instance 要被單獨停用? | 撤銷造成連帶中斷 |
| Evidence boundary | 事後要把 Action 關聯到哪個可驗證 executor? | 只能追到共用帳號 |
若兩個 Agent 的 owner、目的、Tool、資料敏感度、部署生命週期或 risk tier 不同,通常就不應共用 principal。這不是要求「每個 request 永遠建立新帳號」,而是要求 identity boundary 不得比 policy 與 incident response 所需的邊界更粗。
可以把 Agent Identity 拆成兩層:
agent/customer-research;適合作為 policy principal。同一版本、同一 owner、同一 policy 的水平擴充 replicas,可以共享邏輯 workload identity,但仍應各自取得短效 key material,並保留 instance correlation。反過來說,把 Research Agent 和 Operations Agent 只因為「都叫 Agent」而放進同一 Service Account,等於把分類標籤誤當 security boundary。
SPIFFE 提供一種 vendor-neutral 的參考:SPIFFE ID 用來明確識別 workload,SVID 則是 workload 證明該 identity 的短效文件;SPIFFE Workload API 由本地 endpoint 識別 caller,再交付對應的 identity material。重點不是一定要部署 SPIRE,而是「runtime 被辨識後取得自己的短效 credential」,而非由所有 Agent 讀取同一個預先放置的 secret。
獨立 Agent Identity 只解決「直接 caller 是誰」,不能單獨回答「代表誰」與「能做什麼」:
Agent Identity = 直接執行者,例如 agent/customer-research
Delegation Context = 若有代行,記錄 User、Task、purpose、期限與限制
Authorization = 對這一次 Action + Resource + context 的決策
因此,避免共用 Service Account 不是要發一張「萬能 Agent Token」給每個 Agent。Token 仍需限制 audience、期限、Action 與 Resource;若使用 OAuth access token,RFC 9700 建議限制最低必要 privilege、綁定特定 audience,並在適合的場景使用 sender-constrained token 降低外洩後的重放風險。

下側架構把 Agent 名稱、執行目的與 runtime 差異留在 credential 之外。Gateway 驗到 Token 後只看見 svc-agent-prod,所以後面的 policy 再精細,也沒有可信的輸入可以區分 A1 與 A2。
上側架構的設計重點如下:
NIST SP 800-207A 將 cloud-native access control 的重點放在 application/service identity 與 API gateway、sidecar 等 enforcement infrastructure,而不只依賴網路位置。這與本篇的核心一致:Agent 所在的 subnet 或「內網服務」標籤,不能替代可驗證的 workload principal。
這個 flow 至少跨越四條 trust boundary:
role、agent_id 或 approved=true。一個最小 identity flow 可以是:
customer-research-agent 啟動,向 issuer 提交可驗證的 runtime evidence。agent/customer-research,發出短效且 audience-bound 的 credential。customer.read,並把 credential 交給 Tool Gateway。actor=agent/customer-research;若 Agent 代表 User,另行驗證 Delegation Context。PDP 應收到結構化且可驗證的 input,而不是只看到「某個有效 Token」:
{
"actor": {
"workload_id": "agent/customer-research",
"instance_id": "run-7f31",
"deployment": "v17"
},
"delegation": null,
"action": "customer.read",
"resource": "crm/customer/1234",
"context": {
"task_id": "task-9ab2",
"risk": "low"
}
}
Policy 可以允許 Research Agent 查詢 CRM,拒絕它更新 ticket;Support Agent 則得到另一組規則。若 instance_id=run-7f31 被隔離,PDP 或 issuer 也能只停止該 instance,而不是撤銷整個 Agent pool。
要注意:每個 Agent 一個 Token仍不是終點。如果 Token 是永久 bearer secret、可被複製、沒有 audience restriction,或 Gateway 不做逐 Action authorization,blast radius 只是從「全部 Agent」縮成「單一 Agent」,並沒有消失。
以下 Python 只用 standard library,而且刻意不用 JWT。它以記憶體中的 credential registry 模擬「Gateway 已驗證 Token 後得到的 principal」,只證明兩件事:audit attribution 的粒度,以及撤銷是否能對準單一 Agent。這不是 production authentication 實作。
shared = {
"token-pool": {
"principal": "svc-agent-prod",
"allow": {"customer.read", "ticket.update"},
}
}
isolated = {
"token-research": {
"principal": "agent/customer-research",
"allow": {"customer.read"},
},
"token-support": {
"principal": "agent/support-ops",
"allow": {"ticket.update"},
},
}
def call(registry, revoked, token, action, claimed_agent):
credential = registry[token]
principal = credential["principal"] # Gateway 能驗證的 actor
allowed = token not in revoked and action in credential["allow"]
print({
"actor": principal,
"claimed_agent": claimed_agent, # caller 可填,不能當 identity
"action": action,
"decision": "allow" if allowed else "deny",
})
print("-- shared credential --")
call(shared, set(), "token-pool", "customer.read", "research")
call(shared, set(), "token-pool", "ticket.update", "support")
call(shared, set(), "token-pool", "ticket.update", "research")
call(shared, {"token-pool"}, "token-pool", "ticket.update", "support")
print("-- isolated credentials --")
call(isolated, set(), "token-research", "ticket.update", "research")
call(isolated, {"token-research"}, "token-support", "ticket.update", "support")
預期輸出重點如下:
-- shared credential --
{'actor': 'svc-agent-prod', ..., 'action': 'customer.read', 'decision': 'allow'}
{'actor': 'svc-agent-prod', ..., 'action': 'ticket.update', 'decision': 'allow'}
{'actor': 'svc-agent-prod', ..., 'action': 'ticket.update', 'decision': 'allow'}
{'actor': 'svc-agent-prod', ..., 'action': 'ticket.update', 'decision': 'deny'}
-- isolated credentials --
{'actor': 'agent/customer-research', ..., 'action': 'ticket.update', 'decision': 'deny'}
{'actor': 'agent/support-ops', ..., 'action': 'ticket.update', 'decision': 'allow'}
前三筆 shared log 的 claimed_agent 雖然不同,可驗證的 actor 卻完全相同;Research Agent 也吃到了權限聯集。第四筆撤銷 pool Token 後,Support Agent 一起失效。
isolated 模式下,Research Agent 的越權 Action 被拒絕;即使它自己的 credential 已撤銷,Support Agent 仍可繼續工作。這正是 policy boundary 與 revocation boundary 對齊後的差異。
真實系統還需要 signature validation、secure key storage、expiry、audience、rotation、replay protection 與受保護 audit storage;不要把範例中的字串 Token 延伸成自製 authentication protocol。
把 Agent 拆成不同 identity 後,我們終於能回答「是哪個 Agent 動手」。但當 Agent 是受使用者要求執行任務時,只知道 executor 還不夠:Action 是使用者授意、Agent 自主推導,還是部署方政策允許的結果?
Day 04 將接著討論:Agent 代表使用者做事時,責任到底算誰的?