iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
AI Security

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

Day 19|Microsoft Foundry 的 AI Agent 攻防實戰:Secret 搬進 Key Vault,舊影印本為什麼還能用?

  • 分享至 

  • xImage
  •  

Day 18 已分開管理 vault 與讀取 secret 的角色。今天處理一個仍需要密碼的舊供應商 API:當 secret 從 version 1 換成 version 2,應用程式會不會繼續選到舊版本?

把值存進 Key Vault 以後,adapter、cache 與輸出紀錄仍然需要自己的控制。我們先用本機 store 重現 disabled version 被接受的情況,再把 reference 綁過 proposal、policy 與 adapter,最後檢查五個輸出位置有沒有留下合成值。

真實供應商是否已撤銷舊 credential、Azure Key Vault 的輪替與 cache 失效,這次都還沒有執行。

Reference、Version 與實際值各自放在哪裡

  • Secret reference:只描述 vault、名稱與版本的指標,不包含真正的敏感值。
  • Version:同一個 secret 每次輪替後的特定版本;latest 是會移動的選擇,不是固定版本。
  • Resolve:adapter 在最後必要時刻,依已授權的 reference 取回值。
  • Zero-read deny:若政策拒絕或 binding 被竄改,secret store 的讀取次數必須是 0;「先讀再丟掉」仍然越過了資料邊界。

輪替需要一起觀察三種狀態

輪替作業若只確認「Version 2 已建立」,會漏掉三個需要分開觀察的狀態:Version 1 是否 disabled、adapter 是否仍可選到它,以及 raw output 是否還留著舊值。今天的本機 fixture 就固定這三個狀態,不假設 Azure Key Vault 已經替應用清掉 cache。

重現 Adapter 接受 Disabled Version

先把 Version 1 停用,看看舊 adapter 還會不會選到它。

Secret adapter UI 顯示 disabled Version 1 被脆弱流程接受並產生一張 Receipt。

selected_version=1、selected_enabled=false,流程卻回傳 LEGACY_DISABLED_SECRET_ACCEPTED,並留下 1 張 Receipt。這表示本機 adapter 仍接受已停用版本。畫面不公開 secret value,也不能證明 proposal 取得 canary;這次沒有呼叫 Azure Key Vault。

真實使用者碰到的通常不是 version number,而是「供應商那邊剛換過密碼,怎麼還是連不上?幫我看看。」這句話不含 secret 名稱、vault URL、工具函式或預期結果;版本選擇是受信任 adapter 與部署設定的責任。

本機 Lab 不連 Azure,也不放任何真實 secret。測試框架另外準備兩個明顯的 canary,專門檢查舊版本是否仍被選到,以及值是否流進輸出 sink。它們不屬於 user message:

version 1 = SYNTH_MAGIC_PANDA_VENDOR_V1
version 2 = SYNTH_MAGIC_PANDA_VENDOR_V2

attack fixture 先建立 version 1,再 rotate 到 version 2。rotation 會把 version 1 標記為 disabled;before profile 仍容許 adapter 解析這個 disabled reference,因此留下:

{
  "action": "use_secret_version",
  "actor": "vault-agent",
  "allowed": true,
  "resource_id": "vendor-token:v1",
  "reason_code": "LEGACY_DISABLED_SECRET_ACCEPTED",
  "receipt_count": 1,
  "side_effect_count": 0,
  "details": {
    "selected_version": 1,
    "selected_enabled": false
  }
}

這張 Receipt 表示本機 adapter 接受了已停用的 reference,問題先出在應用程式選版本的這一步。至於供應商是否仍接受舊密碼,還要對實際服務測試,不能由這張紀錄推定。

這裡也要分清楚 lab 與 Azure 的差別。本機 store 在輪替時會自動停用 version 1;Azure Key Vault 建立新版本時,舊版本預設仍是 enabled,要由輪替流程自己停用。版本真的停用後,Azure 也不會再回傳它的值。所以在雲端上,這個漏洞比較常見的樣子是應用程式快取了舊值、設定檔釘住舊版本,或供應商那邊根本沒有撤銷舊密碼,而不是 Key Vault 把停用的版本交出來。

同一個 canary 還會刻意進入五個 raw fixture:

  1. response:tool error 把值回給使用者。
  2. event:安全事件直接保存 exception payload。
  3. fake outbox:通知 body 帶到值。
  4. error:adapter diagnostic 拼入值。
  5. trace:span attribute 在 export 前收到值。

這裡沿用 Day 14 的 serializer。before scan 必須真的看到 canary,after-export scan 才要求五個 sink 全為 0;如果測試資料從頭就沒有 canary,零命中只是分母消失。

先確認 Key Vault 的管理與讀取權限

Azure Key Vault 的 control plane 與 data plane 獨立授權。Key Vault Contributor 用來管理 vault control plane,在 Azure RBAC permission model 下不能讀 secret value;只需讀值的 runtime 使用 Key Vault Secrets User,管理 secret 版本與生命週期的 operator 才需要 Key Vault Secrets Officer。Key Vault RBAC guide

工作 建議角色 不代表
建立/設定 vault 適當 control-plane role 可讀 secret value
runtime 讀既有 secret Key Vault Secrets User 可 set/delete/rotate
secret lifecycle operator Key Vault Secrets Officer 可管理 role assignment
Foundry BYO Key Vault connection resource MI 使用官方要求的最小 connection role 等同應用 runtime reader

legacy access-policy model 要另外小心。Microsoft 文件提醒,若 principal 擁有包含 Microsoft.KeyVault/vaults/write 的 Contributor 類 control-plane 權限,它可能替自己加入 access policy,進而取得 data-plane 權限。使用 Azure RBAC permission model 時,Key Vault Contributor 本身仍不能讀值;role assignment 又是另一項權限。

使用 Key Vault control-plane API version 2026-02-01 或更新版本建立新 vault,若沒有明確指定,預設 access-control model 是 Azure RBAC;既有 vault 不會因為換 API version 更新就自動從 access policy 遷移。所有早於 2026-02-01 的 Key Vault control-plane API versions 預定於 2027-02-27 退役,data-plane API 不在這次退役範圍。Key Vault access-control default

2026-02-01 是 API version,不是某一天才開始生效的 feature flag;IaC 必須明確保存實際使用的 version 與 permission model。

在讀值以前核對 Reference 與授權

接著沿著 proposal 追蹤核准的 reference,確認實際值直到 adapter 才被解析。

Secret flow 顯示 proposal 只帶 reference,adapter 最後才解析值。

action hash 綁定 reference 與版本政策,前面的資料不包含原始 secret value。授權先對著 reference 做,真正需要使用時才讀值。這張圖沒有驗證 Azure SDK、vault resource 白名單或網路路徑。

SecretReference 是 strict、frozen 且 extra="forbid" 的 Pydantic model,只接受 vault_alias、name 與整數 version。Server 用這三個值派生 data-plane scope;path traversal、字串型 version 或額外的 value 欄位會在入口被拒絕。

接著把同一個 reference 綁過四個階段:

SecretUseRequest
→ trusted planner 建立 SecretUseProposal
→ policy-owned SecretDecisionAuthority 獨立簽發 opaque decision
→ SecretPolicyDecision 綁定 action_hash、reference 與 provenance
→ adapter 先驗 decision provenance/hash/reference
→ 全部通過後才讀取指定 secret version

D19-A02 讓 request 指向 vendor token,proposal 卻換成另一個 reference。兩份資料各自符合 schema,仍因不屬於同一筆操作而得到 SECRET_PROPOSAL_BINDING_DENIED,Receipt 為 0。另一筆測試改寫 action hash,adapter 則回 SECRET_ADAPTER_ACTION_HASH_MISMATCH。Spy store 同時確認這兩條拒絕路徑都沒有讀取 secret,檢查確實發生在取值以前。

使用者只需要表達「確認供應商連線」,SecretReference 由受信任的設定與 planner 建立。模型若需要看 reference,也只提供不含值的 alias;proposal 與 policy record 同樣不帶 secret。等到真正要呼叫舊 API 前,adapter 才解析實際值。先用本機 API 看這條流程:

from magic_panda_agent.stages.day19 import cumulative_stage_replay

evidence = cumulative_stage_replay()

assert evidence["disabled_before"]["reason_code"] == (
    "LEGACY_DISABLED_SECRET_ACCEPTED"
)
assert evidence["disabled_before"]["details"]["selected_enabled"] is False
assert evidence["disabled_after"]["reason_code"] == "SECRET_VERSION_DISABLED"
assert evidence["disabled_after"]["receipt_count"] == 0
assert evidence["confused_after"]["reason_code"] == (
    "SECRET_PROPOSAL_BINDING_DENIED"
)
assert evidence["current_after"]["reason_code"] == "SECRET_REFERENCE_RESOLVED"
assert evidence["current_after"]["details"]["selected_version"] == 2
assert evidence["current_after"]["details"]["pipeline_stages"] == [
    "request", "proposal", "policy", "adapter"
]
assert evidence["current_after"]["details"]["adapter_binding_verified"] is True
assert evidence["current_after"]["details"]["after_hits"] == 0

底層使用 LocalRotatingSecretStore,讀取權綁定 (tenant_id, subject)。因此另一個租戶即使也有 vault-agent,仍然無法沿用 bamboo-hq 的 grant。

current_after 的公開證據只留下 reference alias、version 與 redaction count,不含 secret value。實際 shape 是:

{
  "resource_id": "vendor-token:v2",
  "reason_code": "SECRET_REFERENCE_RESOLVED",
  "receipt_count": 1,
  "side_effect_count": 0,
  "details": {
    "selected_version": 2,
    "selected_enabled": true,
    "before_hits": 5,
    "after_hits": 0
  }
}

接到 Azure SDK 時,也保留相同分工。下面是 adapter 示意碼,沒有在這次 lab 中執行;注意不要把 get_secret(...).value 寫進紀錄:

from azure.identity import ManagedIdentityCredential
from azure.keyvault.secrets import SecretClient

credential = ManagedIdentityCredential()
client = SecretClient(
    vault_url="https://<vault-name>.vault.azure.net",
    credential=credential,
)

secret = client.get_secret("vendor-token", version="<approved-version>")
call_legacy_dependency(secret.value)  # value 不進 prompt、log、trace

get_secret() 不指定版本時會拿到最新版本,這正是前面說的「會移動的選擇」。示意碼因此明確帶入核准的版本;開源教材 Day 19~30 的 examples/cloud/key_vault_reader.py 也同樣要求 SYNTHETIC_SECRET_VERSION。

範例中的 placeholder 必須由部署設定提供,不能接受 request body 傳入任意 vault URL。正式 adapter 也應限制 cloud、scheme、host、port、userinfo、query 與 path, 避免將 bearer 送往看起來很像 Key Vault 的 hostile endpoint。本篇還多綁一層「核准的 vault resource」:另一個同樣長得像 *.vault.azure.net 的合法 URL 仍得到 KEY_VAULT_RESOURCE_DENIED。URL 長得像 Azure,不代表它就是這筆部署核准的 vault。

Rotation drill 要把 cache 與撤銷排進同一場演習

一輪完整的輪替,還要把新舊版本、cache 與下游撤銷一起檢查:

建立新 credential/secret version
→ 下游暫時接受新舊值(若產品支援)
→ workload 取得新版本
→ 清除 process 與 distributed cache
→ 用新值完成正向 smoke
→ 停用/撤銷舊值
→ 用舊值執行負向 smoke,要求失敗
→ 觀察錯誤率與舊版本使用量
→ 保存 rollback 與事件紀錄

如果下游只能接受一把 key,新舊版本就無法自然重疊,需要安排維護時段或協調兩端切換。Cache 也要有短 TTL、版本感知與明確 invalidation,輪替才不會依賴 process 碰巧重新啟動。

Soft delete 與 purge protection 也不是同一個開關。Microsoft 建議同時啟用 soft delete、purge protection 與自動 rotation;soft delete 提供 7–90 天的 recovery window,purge protection 才能在 retention 到期前阻止永久清除。Secure Key Vault

Foundry BYO Key Vault 是 connection 管理,不是 runtime 讀值

假設 Foundry connection 指向 customer-owned Key Vault,今天仍要分兩筆觀察:平台能否使用 connection secret,以及工程司 runtime 能否以自己的身分讀取指定 vendor secret。輪替後,前者要核對 connection 是否更新;後者要證明舊版本與舊 credential 已被拒絕。兩個成功畫面即使都出現 Key Vault 名稱,也不能互相代替。

若大魔術熊貓工程司替 Foundry resource 設定 customer-owned Key Vault 來保存 connection secrets,這條權限與 FastAPI runtime 讀單一 vendor secret 不同。官方文件目前列出幾個重要限制:Set up an Azure Key Vault connection

  • 每個 Foundry resource 限一個 Key Vault connection。
  • 建立時 resource/project 不能已有其他 connections;IaC 應先建立 Key Vault connection。
  • 不支援自動 secret migration;更換 vault 要重建 connection secrets。
  • 刪除底層 vault/connection secret 會讓相依 connection 失效。
  • Key Vault Secrets Officer role assignment 最多可能需要約 30 分鐘傳播。

官方範例為 Foundry resource MI 指派 Key Vault Secrets Officer,用途是管理 connection secrets。只讀 vendor secret 的 runtime 通常使用 Secrets User;要按實際 set、delete 或 read 工作選角色,不能直接複製 connection 管理者的權限。

Version 1 要真的失敗,Version 2 不能留下 canary

讀值之後,再掃描 response、event、outbox、error 與 trace 是否留下 canary。

五個 sink 比較顯示修補前各命中、修補後合成 canary 命中為零。

五個出口合計從 before_hits=5 降到 after_hits=0,畫面只留下 alias 與次數。這組掃描不涵蓋未知編碼、heap、crash dump 或外部 telemetry,所以仍要各自檢查其他保存位置。

再把版本選擇與三種讀取拒絕分開看,確認舊版本真的不能用了。

Secret rotation UI 顯示舊版拒絕、current 和 resolved version,以及三條 reader denial。

舊版的 old_enabled=false,讀取結果為 SECRET_VERSION_DISABLED;目前與實際解析的版本都是 2。未授權使用者、外租戶的同名 subject,以及另一個 secret name,都得到 SECRET_READER_DENIED。這次沒有呼叫 Azure Key Vault,圖中也沒有驗證 Receipt、cache 失效或雲端輪替。

最後獨立查看 endpoint 格式與核准資源,確認網址符合格式之外,還指向正確的 vault。

Key Vault endpoint policy matrix 顯示合法 alias 通過,八種 hostile shape 與 wrong resource 全部被拒絕。

合法 alias 解析成功;http、suffix、userinfo、port、path、query、fragment、whitespace 八種問題都被拒絕,非核准資源則得到 KEY_VAULT_RESOURCE_DENIED。這裡只測合成 URL 與本機白名單,沒有建立 Azure Key Vault connection,也沒有驗證 DNS、private endpoint 或 Azure RBAC。

進入 day19/ 後執行:

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

這組驗收要確認:

  • 未授權 principal、相同 subject/不同 tenant、以及未授權 secret name 都得到 SECRET_READER_DENIED。
  • decision provenance、action hash 或 reference 被竄改時,adapter 在 get_version() 前拒絕,spy store read count 維持 0。
  • rotation 後 old version 為 disabled,實際 get_version() 得到 SECRET_VERSION_DISABLED。
  • before profile 仍接受 disabled version 並產生 receipt;after profile receipt 為 0。
  • strict SecretReference 拒絕額外 value、path traversal 與型別 coercion;scope 由 server 派生。
  • request/proposal reference 置換與 policy action-hash 竄改都在 adapter 前失敗。
  • Azure-shaped 但非核准 resource 的 vault URL 得到 KEY_VAULT_RESOURCE_DENIED。
  • enabled version 2 可由已通過身分與 Key Vault data-role fixture 的 runtime 解析。
  • raw response、event、outbox、error、trace 先命中 canary,export 後每個 sink 都是 0。

Day 19 的驗收記錄為 5 passed。下面查看 disabled version 的前後比較、D19-A02 reference 置換與 endpoint/resource matrix,輸出只保留 reference 和判斷結果,不顯示 secret value。從 day19/ 執行:

PYTHONPATH=src:. uv run python scripts/render_stage_evidence.py --day 19 --evidence-id d19-disabled-secret-version-matrix

再沿著 request → proposal → policy → adapter 核對同一份 reference 與 action hash,確認選到 version 2。五個輸出位置的命中數應從 before_hits=5 降到 after_hits=0。輸出只保留合成 alias 與次數,並用 AZURE_KEY_VAULT_REQUEST_NOT_EXERCISED 標明沒有呼叫 Azure:

PYTHONPATH=src:. uv run python scripts/render_stage_evidence.py --day 19 --evidence-id d19-current-secret-redaction

Process、Cache 與併發輪替的剩餘風險

只要 workload 合法取得 secret,遭入侵的 process 仍可在記憶體中使用或外洩它。Key Vault 不知道某次 get 之後是否被送進模型,也不會自動清除應用 cache。能改用 Managed Identity、workload federation 或短效 token 的 dependency,應優先減少 secret 數量。

本機 rotate() 也沒有跨 thread/process 的 lock、ETag 或 transaction。兩個 operator 同時輪替可能產生競態;正式流程需要 single-writer/lease 或外部協調,並在 rotation 後重新讀取 active version 驗證。

ISO/IEC 27001:2022 與 27002:2022 在這裡只提供 credential 建立、保管、授權、輪替、撤銷與紀錄覆核的治理視角。這份 Lab 沒有 ISMS scope、適用性聲明或稽核,不能寫成符合 ISO。

明天會把 Key Vault 放回網路拓撲:誰能讀值之外,還要確認 DNS 解析到哪裡、PNA 是否關閉,以及 Agent 出站能不能偷偷走到未核准 endpoint。

第一方參考資料


上一篇
Day 18|Microsoft Foundry 的 AI Agent 攻防實戰:Azure Owner 不是宇宙萬能管理員,Foundry RBAC 到底管哪一層?
下一篇
Day 20|Microsoft Foundry 的 AI Agent 攻防實戰:Private Endpoint 也有風險
系列文
Microsoft Foundry 的 AI Agent 攻防實戰:建立高資安與可治理的 Agent 系統 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言