iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
AI Security

Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統系列 第 16 篇

Day 16|Microsoft Foundry 的 AI Agent 攻防實戰:換掉共享 API Key

  • 分享至 

  • xImage
  •  

昨天的租戶過濾依賴 principal 提供正確的 tenant。今天就來補這個前提:principal 從哪裡來,系統如何確認它代表誰?

先用共享 API key 做對照。Alice 的程式和另一個拿到相同 key 的程式,都會留下同一個 actor,稽核紀錄無法區分來源。接著改用短效的合成 token,驗證 issuer、audience 與時間條件,再建立給業務政策使用的 Principal。

這一篇先用合成 token 建立本機認證流程。程式不登入 Entra,也沒有取得 Azure access token;後面提到的雲端身分,是我們要接入這套流程的位置。

先分清楚使用者與執行程式的身分

  • Principal(安全主體):可以被驗證、授權與稽核的身分,例如 Alice、FastAPI 服務或某個 Agent。
  • Workload identity(工作負載身分):給程式使用的身分;它證明「哪個服務在呼叫」,不等於證明「哪位使用者有權做這件事」。
  • Managed Identity(受控身分):Azure 代管憑證生命週期的 workload identity。它省去長效密碼,卻不會自動縮小 RBAC 權限。
  • Authentication/Authorization:前者確認「你是誰」,後者才判斷「你能不能做這件事」。登入成功不是業務操作的通行證。

兩個 Caller 使用同一把 Key,稽核會看到什麼?

先看兩個程式共用 credential 後,稽核紀錄還能不能分辨誰發起請求。

Identity comparison 顯示 Alice 與 Mallory caller 都被記為同一 legacy actor。

兩個 caller alias 最後都對應到同一個 legacy actor,side_effect_count=0。這裡使用合成 credential,沒有執行 Entra 認證;要觀察的是共享 key 讓來源身分混在一起。

工程司的舊 API 接受同一把 shared key。Alice 與另一個拿到 key 的程式,都能送出一般查詢;就算兩邊的訊息不同,認證紀錄仍只看得到同一個 actor。問題出在 HTTP 認證方式,和使用者怎麼問無關。

圖中的 K_shared 是測試框架持有的 credential 代號,不是 user message,也不會送進模型:

alice-client ──────┐
                    ├─ K_shared ─→ authentication boundary
mallory-client ────┘

兩次 request 的 claimed_caller 不同,舊版 authenticate_workload() 卻把兩筆 authentication outcome 都記成同一個 actor:

{
  "shared_key_attack": {
    "claimed_callers": ["alice-client", "mallory-client"],
    "distinct_audit_actors": 1,
    "reason_codes": [
      "LEGACY_SHARED_CREDENTIAL_ACCEPTED",
      "LEGACY_SHARED_CREDENTIAL_ACCEPTED"
    ]
  },
  "side_effect_count": 0
}

這裡的 ControlExecutionOutcome.receipt_count 記錄 authenticate_workload 的處理結果,沒有工具呼叫或副作用。測試要看的,是不同 caller 最後只剩同一個 audit actor。共用 key 一旦需要撤銷,正常程式也得一起輪替,因此還牽涉身分生命週期。

本篇的 key、token、tenant 與人名全部是本機 fixture。credential canary 只存在 authentication 測試與輸出掃描,不是使用者必須輸入的內容。程式不登入 Microsoft Entra ID、不取得 Azure access token,也不把任何 credential 傳進 prompt。

沿每一跳確認使用的是哪個身分

在 Foundry 架構裡,Managed Identity、project identity 與 Agent identity 很容易被寫成一團。先把今天需要的四個問題分開:

這一跳 要回答的問題 大魔術熊貓工程司的身分
使用者 → FastAPI 誰正在要求操作? Alice 的 delegated user principal
FastAPI → Foundry 哪個後端 workload 在呼叫? FastAPI 的 system-assigned 或 user-assigned MI
Foundry project 平台工作 哪個 project 執行平台層工作? project managed identity
Agent → 下游工具/資料 哪個 Agent 在執行? 這個 Agent 自己的 Agent identity(舊版 Agent Application 則視是否發布而定)

這一格要看 Agent 是用哪一種模型建立的。Microsoft 已改用新的 agent 物件模型:新建立的 Agent 一開始就有自己的 Agent identity 與 blueprint,發布到 Teams/Microsoft 365 也不會換掉它。舊的 Agent Application 模型則是另一套規則:未發布的 Agent 共用 project 內的 identity,發布時才建立綁定 Agent Application 的 distinct identity。在新模型建立之前就存在的舊 Agent,instance_identity 會是 null,仍使用共用的 project identity。Migrate from Agent Applications

不論哪一種,Agent identity blueprint 與 project managed identity 之間都可以使用 federated trust,但真正要在下游資源取得 RBAC 的 principal 是 Agent identity,不是因為 project MI 參與了信任鏈,就把下游角色全指派給 project MI。Agent identity concepts

有些 tool connection 仍使用 project managed identity。實際連線時,要保存 Agent 屬於新模型還是舊的 Agent Application、connection 的 authentication mode,以及最後使用哪個 principal 呼叫下游,再核對 allow/deny。只看架構圖上有 Agent identity,還無法確認這一跳用的是它。

FastAPI 收到 Token 後,先驗證再建立 Principal

接著看請求如何先通過認證,再進入業務授權。

Inbound auth matrix 顯示 unsigned headers、wrong audience、app-only 的 401,以及合法 delegated 案例的 200。

unsigned headers、wrong audience 與 app-only context 都回 401;合法 delegated 案例則回 200、POLICY_ALLOW,並留下 1 張唯讀 Receipt。這張圖沒有列出 403 案例。所有 token 都是合成測試資料,還不是 Microsoft Entra 的驗證紀錄。

FastAPI 是 access token 的 intended audience,所以驗證應發生在 API boundary。至少要驗 signature、issuer、audience、時間限制,以及 tid、subject 與 actor;delegated request 再依 scp 判斷,app-only request 則走獨立的 application policy。Microsoft 也明確提醒,access token 應由收到它的 Web API 驗證,client 不應自行把 token 解開後當授權結論。Claims validation

Lab 使用 SyntheticTokenIssuer 固定這套驗證規則。它是本機 HMAC 測試替身,沒有實作 JWT,也不能拿去代替 Entra middleware:

from datetime import timedelta

from magic_panda_agent.auth import SyntheticTokenIssuer
from magic_panda_agent.models import Principal

issuer = SyntheticTokenIssuer(key=b"SYNTH_MAGIC_PANDA_LOCAL_SIGNING_KEY_32B")

alice = Principal(
    subject="alice",
    tenant_id="bamboo-hq",
    roles=frozenset({"employee"}),
    auth_type="synthetic-delegated",
)

token = issuer.issue(
    alice,
    audience="api://magic-panda-local-lab",
    token_type="delegated",
    client_id="first-party-lab-client",
    scopes=frozenset({"Agent.Invoke"}),
    ttl=timedelta(minutes=2),
)

claims = issuer.validate(
    token,
    audience="api://magic-panda-local-lab",
    require_delegated=True,
)
principal = issuer.principal(claims)

驗證完成後,policy 與 executor 只接收 typed Principal。原始 token 不放入 AgentRequest、prompt、工具參數、trace 或錯誤訊息;request body 的 role="finance" 也不能取代簽章 claims。

負向案例要一次只改一個條件:

Matrix key 改動 預期 authentication outcome
missing 沒有 token AUTH_REQUIRED
wrong_audience audience 錯誤 TOKEN_WRONG_AUDIENCE
expired token 已過期 TOKEN_EXPIRED
future_issued iat 超出允許的 30 秒 clock skew TOKEN_ISSUED_IN_FUTURE
not_yet_valid nbf 尚未生效 TOKEN_NOT_YET_VALID
valid 短效 workload fixture TOKEN_VALID

HTTP route 另有一組測試:未簽署的 identity headers、錯誤 audience、app-only token、不在 allowlist 的 delegated client,以及缺少已簽 Agent.Invoke scope 的請求,都會被拒絕。Client 與 scope 都符合條件,才繼續進入 PEP。

認證失敗回 401,並帶上 RFC 6750 的 WWW-Authenticate: Bearer;身分已確認、但 client 或 scope 權限不足時回 403。這組 HTTP 測試與前面的認證矩陣分開計算。

Outbound:開發者登入與 production workload 明確分流

再看開發與正式環境各自選用哪個 credential class;這一步只選類別,不取得 token。

Credential class-selection UI 顯示 development、production、invalid 結果以及零 token 和 cloud calls。

development 選 AzureCliCredential,production 選 ManagedIdentityCredential,無效環境則被拒絕。token_requests=0、cloud_calls=0,因為這裡只執行類別選擇,沒有取得 token 或呼叫 Azure,也無法確認 Managed Identity 與 RBAC 是否已設定好。

Foundry 的 Entra token scope 是 https://ai.azure.com/.default。Microsoft 建議 production workload 使用 Microsoft Entra ID;API key 雖可用於部分快速原型,卻不能表達個別使用者,授權粒度與稽核能力也較弱。Agents 等部分能力要求 Entra ID,不能把「Entra 失敗就改用 key」藏成 fallback。Foundry authentication and authorization

Lab 的 credential factory 刻意要求環境明確:

from magic_panda_agent.auth import EntraCredentialFactory

# APP_ENV=development:AzureCliCredential
# APP_ENV=production:ManagedIdentityCredential
# 未指定或其他值:fail closed
credential = EntraCredentialFactory.explicit()

# 不要 print token;這裡只展示 audience contract。
access_token = credential.get_token("https://ai.azure.com/.default")

Credential factory 讓開發與正式執行環境使用明確的身分來源,避免 production process 意外取得開發者的 CLI session。ManagedIdentityCredential 處理 workload 的憑證取得與輪替,角色範圍仍要在 Day 18 逐項確認。

正式環境至少應保留以下 separation of duties:

Principal 建議用途 不該因此取得
Alice user principal 呼叫 FastAPI 的 delegated scope Foundry project 管理權
FastAPI MI 呼叫指定 Foundry Agent endpoint subscription-wide Owner
project MI 明確的平台/connection 工作 所有 Agent 的下游資料權
每個 Agent 自己的 identity 指定工具與資源的最小 data role 其他 Agent 的工具權限
CI workload identity 部署所需 control-plane action runtime secret read

GA/Preview 邊界

Foundry readiness 頁把 Agents core、Entra authentication 與核心 RBAC 放在 GA 範圍;hosting 文件的粒度卻不完全一致:hosting 總覽稱 managed Hosted Agents GA,Agent Framework 的 Hosted Agents 子頁仍標 Preview,部分 session/API 也要求 preview surface。最安全的做法不是替整個「Hosted Agents」貼一張永久標籤,而是依實際使用的 managed service、session API、SDK/package 與 region 分別核對。本篇的本機 identity contract 不依賴任何 hosting package。

Managed Identity 也有環境前提:ManagedIdentityCredential 要在已配置 MI 的 Azure compute 上才是 MI 證據。在筆電以開發者 credential 取得 token,不會因為變數名稱寫成 managed_identity 就變成 Azure 托管身分。

實作後的資料流

Identity graph:nodes 與 edges

從節點與連線資料,查看身分如何經過 FastAPI 與 PEP。

identity runtime graph 的 nodes、edges、format 與 rendered flag。

graph_source 列出 delegated client、FastAPI 驗證、server principal、application PEP 與本機工具的節點和邊。format=node-edge-source、rendered=false,所以這裡看到的是流程資料,尚未繪成流程圖。圖中沒有 Foundry 那一跳、對外使用的 credential 或逐跳 token audience,也沒有執行 Entra、Agent identity 或下游雲端呼叫。

Identity graph:D16-B02 outcome

再把 D16-B02 的執行結果,對回前面的身分流程。

D16-B02 source outcome 的 status、decision reasons 與零副作用。

D16-B02 保留 status、decision_reasons、receipt_count 與 side_effect_count,可以核對政策結果與零副作用。它仍是本機結果,沒有顯示 Foundry 呼叫、對外 credential 或逐跳 token audience,也沒有執行 Entra、Agent identity 或下游雲端服務。

下列六步是目標架構;本圖實測只涵蓋前 3 步與 local tool,沒有執行第 4、5 步的雲端 hop:

  1. FastAPI 收到 bearer token,但不把它放進 Agent 輸入。
  2. authentication layer 驗證 token,建立 typed Principal。
  3. business PEP 依 (tenant, subject, resource, action) 做決策。
  4. FastAPI 以自己的 MI 取得 Foundry token,audience 固定為 https://ai.azure.com/.default。
  5. Agent 呼叫下游時,再依該 tool connection 的認證模式決定使用 Agent identity、project MI 或後續 Day 17 的 delegated 路徑。
  6. receipt 只記 principal alias、reason code、resource alias 與 count;不記 access token。

閱讀這六步時,逐跳記下 actor、所代表的使用者、目標資源與操作。這樣接到下游 connection 時,才知道該核對哪個 principal 的權限;有 token 本身並沒有回答這些問題。

先讓本機身分矩陣可以重跑

進入 day16/ 後執行:

uv run pytest tests/stages/day16/test_acceptance.py -q

這組驗收要確認:

  • 共享 key attack 讓 alice-client 與 mallory-client 的 authentication outcome 只留下同一個 legacy actor。
  • 修補後共享 key 得到 VERIFIED_WORKLOAD_IDENTITY_REQUIRED,兩顆不同的短效 workload fixture 留下兩個 actor。
  • missing、wrong_audience 與 expired 具有固定的 authentication reason;互動 route 的 app-only token 則得到 USER_CONTEXT_REQUIRED。
  • 共享 key replay 沒有工具呼叫,side_effect_count=0;不要把 authentication outcome 的 receipt_count 說成工具 Receipt。
  • response、event、fake outbox、error 與 trace 不出現 SYNTH_MAGIC_PANDA_* canary。

本機測試記錄為 9 passed。第一份 JSON 的重點是 actor 從混用的一個變成兩個、五個輸出 sink 的命中數從 5 降到 0,以及 side_effect_count=0。工具 receipt.count=0;before/fixed 的 authentication outcomes 分別為 2/3 筆,與業務工具 Receipt 分開計數。原始 renderer 的 external 與 cloud calls 為 0,UI 則顯示 null/未觀測;沒有網路 observer,不能把這些欄位當成網路量測。

先看兩個 caller 為什麼留下相同的 legacy actor。這份資料只有認證結果與零副作用,不顯示 token 值,也沒有 Entra 或 Foundry 的服務端紀錄。從 day16/ 執行:

PYTHONPATH=src:. uv run python scripts/render_stage_evidence.py --day 16 --evidence-id d16-workload-identity-attack-matrix

再走本機 FastAPI 路徑:unsigned headers、wrong audience 與 app-only context 回 401,合法的 delegated 案例則得到 POLICY_ALLOW 與 1 張唯讀 Receipt。使用的仍是合成 token,外部與雲端呼叫沒有觀測資料:

PYTHONPATH=src:. uv run python scripts/render_stage_evidence.py --day 16 --evidence-id d16-identity-runtime-graph

Managed Identity 的權限與撤銷仍需管理

Day 02 的 2026-09-25 新紀錄使用開發者的 Azure CLI 身分,在既有基準部署完成一次 Project endpoint 無工具呼叫。正式 FastAPI 改用 Managed Identity 時,要確認 Azure 看到的是 workload principal;Agent 自己的 identity 也要再確認 object ID 與下游角色。這三種身分即使都能呼叫成功,稽核與撤權仍是不同對象;本篇的合成 token 圖還不能替它們作證。

Managed Identity 的 token 仍可能被同一執行環境中的惡意程式取得並濫用;若 MI 在 resource group 或 subscription scope 擁有過大的角色,攻擊者根本不需要把長效秘密偷走。把舊的 Agent Application 或舊 Agent 換成新模型的 Agent 時,會拿到一個新的 identity,角色不會自動搬過去;要重新指派並重測下游角色,舊的共用 project identity 權限也不應照單全收。

NIST SP 800-207A 對雲端原生應用的 Zero Trust 思路,在這篇落成「每個 principal、resource 與 action 都單獨做決策」;ISO/IEC 27002:2022 則補上身分建立、權限覆核與撤銷的管理視角。localhost evidence 只驗 synthetic authentication contract,雲端 principal 建立、role assignment 與撤銷傳播仍未觀察。

明天要處理更麻煩的狀況:FastAPI、Agent 與 Alice 的身分都是真的,Agent 仍可能拿自己的權限替 Alice 讀到 Bob 的費用。那時候問題已經不是「誰登入」,而是使用者的 authority 有沒有跟著走到下一跳。

第一方參考資料


上一篇
Day 15|Microsoft Foundry 的 AI Agent 攻防實戰:跨租戶 RAG 的三個常見攻擊與修補
下一篇
Day 17|Microsoft Foundry 的 AI Agent 攻防實戰:三張識別證都是真的,Agent 為什麼仍替 Alice 越權?
系列文
Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言