昨天的租戶過濾依賴 principal 提供正確的 tenant。今天就來補這個前提:principal 從哪裡來,系統如何確認它代表誰?
先用共享 API key 做對照。Alice 的程式和另一個拿到相同 key 的程式,都會留下同一個 actor,稽核紀錄無法區分來源。接著改用短效的合成 token,驗證 issuer、audience 與時間條件,再建立給業務政策使用的 Principal。
這一篇先用合成 token 建立本機認證流程。程式不登入 Entra,也沒有取得 Azure access token;後面提到的雲端身分,是我們要接入這套流程的位置。
先看兩個程式共用 credential 後,稽核紀錄還能不能分辨誰發起請求。

兩個 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,還無法確認這一跳用的是它。
接著看請求如何先通過認證,再進入業務授權。

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 測試與前面的認證矩陣分開計算。
再看開發與正式環境各自選用哪個 credential class;這一步只選類別,不取得 token。

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 |
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 托管身分。
從節點與連線資料,查看身分如何經過 FastAPI 與 PEP。

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

D16-B02 保留 status、decision_reasons、receipt_count 與 side_effect_count,可以核對政策結果與零副作用。它仍是本機結果,沒有顯示 Foundry 呼叫、對外 credential 或逐跳 token audience,也沒有執行 Entra、Agent identity 或下游雲端服務。
下列六步是目標架構;本圖實測只涵蓋前 3 步與 local tool,沒有執行第 4、5 步的雲端 hop:
Principal。(tenant, subject, resource, action) 做決策。https://ai.azure.com/.default。閱讀這六步時,逐跳記下 actor、所代表的使用者、目標資源與操作。這樣接到下游 connection 時,才知道該核對哪個 principal 的權限;有 token 本身並沒有回答這些問題。
進入 day16/ 後執行:
uv run pytest tests/stages/day16/test_acceptance.py -q
這組驗收要確認:
alice-client 與 mallory-client 的 authentication outcome 只留下同一個 legacy actor。VERIFIED_WORKLOAD_IDENTITY_REQUIRED,兩顆不同的短效 workload fixture 留下兩個 actor。missing、wrong_audience 與 expired 具有固定的 authentication reason;互動 route 的 app-only token 則得到 USER_CONTEXT_REQUIRED。side_effect_count=0;不要把 authentication outcome 的 receipt_count 說成工具 Receipt。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
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 有沒有跟著走到下一跳。