Day 15 已經把 tenant_id 從使用者輸入拿掉,查詢 bamboo-hq 文件時,租戶條件由伺服器端的 principal 產生。這個修補有一個很現實的前提:principal 必須真的可信。
如果大魔術熊貓工程司前面仍放著一把所有人共用的 API key,系統頂多知道「有人帶了那把 key」,卻分不出是 Alice、FastAPI、排程工作,還是哪一隻半夜偷偷加班的 Agent。稽核紀錄裡大家穿同一件熊貓裝,最後當然只能認出熊貓,認不出誰動了竹筍採購單。
畫面判讀目標: 看見共享 credential 如何讓不同 caller 在 audit 中變成同一 actor。

共享 key 讓稽核失去誰真正發起請求的資訊。 可觀察狀態:兩個 caller alias 對應同一 legacy actor,side_effect_count=0。 Claim boundary:使用 synthetic credential;未執行 Entra authentication。
大魔術熊貓工程司的舊版採購 API 接受同一把 shared key。若前面還有聊天介面,Alice 只會說「幫我查一下我剛送出的費用申請」,拿到 key 的另一個人也只需送出一個普通請求,例如「幫我查一下 exp-bamboo-001 現在處理到哪裡了」。沒有人需要替測試程式背台詞;漏洞發生在 HTTP authentication boundary,不在這兩句話的寫法。
圖中的 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 是 cumulative executor 對 outcome 的記錄欄位;這兩筆的 action 都是 authenticate_workload,不是工具呼叫,也沒有副作用。攻擊成立的證據是兩個 caller 只剩一個 audit actor,不是 Agent 已經動過採購單。共享 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 在執行? | shared development Agent identity,或發布後的 distinct Agent identity |
截至 2026-08-03,Microsoft 文件說明:Foundry 的未發布/開發中 Agent 會共用 project 內的 Agent identity;發布 Agent 時,平台會建立綁定該 Agent application 的 distinct identity。Agent identity blueprint 與 project managed identity 之間可使用 federated trust,但真正要在下游資源取得 RBAC 的 principal 是 Agent identity,不是因為 project MI 參與了信任鏈,就把下游角色全指派給 project MI。Agent identity concepts(查閱:2026-08-03)
有些工具連線仍提供 project managed identity 作為認證模式,所以不能只看「Agent 有 identity」便猜測實際送到下游的是哪個 principal。正式驗收要保存 Agent 發布狀態、connection authentication mode、實際 principal alias 與下游 allow/deny。
畫面判讀目標: 確認 inbound authentication 與 business authorization 分層。

先驗 token 建立 principal,再進 business PEP。 可觀察狀態:401/403 reason 分開;只有簽章 client 與 scope 符合者進入 PEP。 Claim boundary:token 皆為合成 fixture,不是 Microsoft Entra validation receipt。
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(查閱:2026-08-03)
V2 Lab 使用 SyntheticTokenIssuer 固定這個契約。它是本機 HMAC fixture,不是 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)
Principal 才能進入 policy 與工具執行器;原始 token 不得進入 AgentRequest、model prompt、tool arguments、trace 或錯誤訊息。request body 自填的 role="finance" 只是未簽資料,不能取代 verified 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 另有一組測試:unsigned identity headers、wrong audience、app-only
token、不在 server allowlist 的 delegated client,以及缺少已簽 Agent.Invoke scope
都 fail closed;只有 client 與 scope 同時由簽章 claims 證明的 delegated token 才會進到
application PEP。401 回應帶 RFC 6750 WWW-Authenticate: Bearer challenge;已驗證身分但
client/scope 權限不足則回 403。這兩組矩陣不要混成同一批 case ID,
否則截圖看起來很完整,實際上對不到 structured capture JSON。
畫面判讀目標: 確認明示 environment 只選擇 credential class,且不請求 token 或呼叫雲端。

這張圖證明 class selection,不是雲端身分驗證。 可觀察狀態:development=AzureCliCredential、production=ManagedIdentityCredential、invalid fail closed,token_requests=0、cloud_calls=0。 Claim boundary:這是 class selection only;未取得 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(查閱:2026-08-03)
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")
這個選擇是為了防止 production process 不小心沿著寬鬆 credential chain 撿到開發者的 CLI 登入狀態。ManagedIdentityCredential 只解決 workload 如何取得與輪替 credential,RBAC scope 仍要在 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 的下游資料權 |
| distinct Agent identity | 指定工具與資源的最小 data role | 其他 Agent 的工具權限 |
| CI workload identity | 部署所需 control-plane action | runtime secret read |
截至 2026-08-03,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 runtime graph 的 nodes、edges、format 與 rendered flag的 canonical runtime fields。

先讀 identity/data flow graph 本身。 可觀察狀態:graph_source 顯示 workload identity 從來源到 downstream resource 的節點與邊,rendered=true。 Claim boundary:畫面沒有 Foundry hop、outbound credential 或逐 hop token audience;也未執行 Entra、Agent identity 或 downstream cloud calls。 此子畫面只涵蓋identity runtime graph 的 nodes、edges、format 與 rendered flag。
畫面判讀目標: 核對D16-B02 source outcome 的 status、decision reasons 與零副作用的 canonical runtime fields。

再用 bounded outcome 驗證 graph 中的政策結果。 可觀察狀態:D16-B02 顯示 status、decision_reasons、receipt_count 與 side_effect_count。 Claim boundary:畫面沒有 Foundry hop、outbound credential 或逐 hop token audience;也未執行 Entra、Agent identity 或 downstream cloud calls。 此子畫面只涵蓋D16-B02 source outcome 的 status、decision reasons 與零副作用。
下列六步是目標架構;本圖實測只涵蓋前 3 步與 local tool,沒有執行第 4、5 步的雲端 hop:
Principal。(tenant, subject, resource, action) 做決策。https://ai.azure.com/.default。看到第五步可能開始有點昏,先抓住一句就好:每一跳都要重新回答「誰在替誰,對哪個資源做哪個動作」。只有「它有一顆 token」不算答案。
進入 day16/ 後執行:
uv run pytest tests/stages/day16/test_acceptance.py -q
V2 驗收必須確認:
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。本次本機 QA 的結果是 9 passed。第一個 JSON slot 的重點不是總receipt.count=4,而是 distinct_audit_actors=1、修補後distinct_verified_actors=2、五個輸出 sink 從命中 5 次降為 0,以及side_effect_count=0。external_calls 與 cloud_calls 都是 null:renderer 沒有
network observer,不能把「測試程式沒打算連線」冒充成量到零。那個總 receipt 是
四筆 control-executor authentication outcome 的累計,不是四次業務工具執行。
攻擊/before-state UI 要證明共享 credential 造成 actor 混淆;圖片需顯示兩個 caller alias、同一個 legacy actor、authentication action 與零 side effect,不能顯示 token 值。這張圖沒有 network observer 或雲端 receipt,外部/雲端呼叫只能標成未觀察;它不代表 Entra 或 Foundry 已接受這些身分。從 day16/ 執行:
PYTHONPATH=src:. uv run python scripts/render_stage_evidence.py --day 16 --evidence-id d16-workload-identity-attack-matrix
修補/test UI 使用實際本機 FastAPI path,應顯示 unsigned headers、wrong audience 與 app-only context 都是 401,delegated positive control 則留下 POLICY_ALLOW 與一筆 read-only Receipt。它仍是 synthetic token,不是 Entra 實測;也沒有 network observer 或雲端 receipt,所以外部/雲端呼叫維持未觀察:
PYTHONPATH=src:. uv run python scripts/render_stage_evidence.py --day 16 --evidence-id d16-identity-runtime-graph
這些 structured capture 命令只重現 disclosure-safe JSON;正式發布時,本篇 required app UI 圖
必須由目前相關原始碼經 repository UI capture pipeline 產生,schema 3 manifest 必須以 source-tree SHA-256
與檔案數綁定輸入並交由 strict verifier 重算。正式圖仍只支持 synthetic identity/本機 executor 證據,
不是 Entra、Managed Identity 或 Foundry cloud receipt。
本篇尚未在 V2 重新執行以下雲端案例:
ManagedIdentityCredential 實際取 token。因此 cloud_validation 維持 PENDING-CLOUD。本機 synthetic token 通過,不代表 Microsoft Entra、Managed Identity 或 Foundry endpoint 已經通過。
Managed Identity 的 token 仍可能被同一執行環境中的惡意程式取得並濫用;若 MI 在 resource group 或 subscription scope 擁有過大的角色,攻擊者根本不需要把長效秘密偷走。發布 Agent 產生 distinct identity 之後,也要重新指派並重測下游角色;舊的 shared development 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 有沒有跟著走到下一跳。