---
title: FR-077 遠端 Agent 控制鏈資安掃描
status: ✅ 五棒全部掃完並驗收收口（2026-09-17）；發現已全數處理——控制面不驗身分（CM-1595，唯一 CRITICAL）已修（CM-2052，1.21.0 出貨）、健康檢查 SSRF 於 1.21.1 補修（CM-2370）、註冊 Token 裁定不修（SaaS 前再評估）、版控 .env 與外洩密碼已清／已撤銷
notion:
  parent: {id: CM-1591, title: "FR-077 遠端 Agent 控制鏈資安掃描（3 棒，只掃不修）", url: "https://app.notion.com/p/FR-077-Agent-3-3d4346da4cd081a492c3e07b944aa873"}
  children:
    - {id: CM-1592, title: "R1 agent 身分與註冊（42 檔，jedi monorepo）", url: "https://app.notion.com/p/FR-077-1-jedi-remote-agent-agent-42-Opus-1M-effort-low-3d4346da4cd081ea8b6afa9f63fe3c84", status: 修正待驗證}
    - {id: CM-1593, title: "R2a 任務下發與狀態機＋控制面 route／守門殼（30 檔，jedi-remote-agent；2026-09-16 重寫）", url: "https://app.notion.com/p/FR-077-2-jedi-remote-agent-agent_task-jedi-detection-agent-29-Opus-1M-effort-low-3d4346da4cd081908e3ad354ea117f0a", status: 已完成（掃描完成並驗收，2026-09-17）}
    - {id: CM-1601, title: "R2b 檔案取用與 agent 連線（13 檔，jedi monorepo；2026-09-16 重寫）", url: "https://app.notion.com/p/FR-077-2b-agent-jedi-detection-agent-11-agent_task-2-13-Opus-1M-effort-low-3d5346da4cd081f2bb39fc77e049a7dc", status: 修正待驗證}
    - {id: CM-1594, title: "R3 主專案宿主接線與資料面 adapter（16 檔，BE repo；2026-09-16 重寫）", url: "https://app.notion.com/p/FR-077-3-BE-Agent-adapter-17-Opus-1M-effort-low-3d4346da4cd081d69ff3d6ad0b5ad820", status: 已完成（掃描完成並驗收，2026-09-17）}
    - {id: CM-1595, title: "修 控制面四端點驗 agent 簽章、fail closed、任務綁歸屬（F1+F2+F3+F5，CRITICAL）", url: "https://app.notion.com/p/FR-077-agent-agent-fail-closed-R1-F1-F2-F3-F5-CRITICAL-3d5346da4cd08199a619e14935f4bdf2", status: 作廢（09-21 統一派工，改照 SUMMARY 內化版處理）}
    - {id: CM-1596, title: "修 健康檢查 SSRF（F4，MEDIUM）", url: "https://app.notion.com/p/FR-077-agent-SSRF-base_url-agent-R1-F4-MEDIUM-3d5346da4cd0817b9657e73f52f041fa", status: 作廢（09-21 統一派工，改照 SUMMARY 內化版處理）}
    - {id: CM-1597, title: "修 enroll token 到期／次數＋重註冊持有證明（F6，MEDIUM，依賴 1595）", url: "https://app.notion.com/p/FR-077-enroll-token-token-uid-R1-F6-MEDIUM-3d5346da4cd081f6b1a6d229c17fb1b6", status: 作廢（09-21 統一派工，改照 SUMMARY 內化版處理）}
    - {id: CM-1598, title: "處置 jedi-common/.env 受版控（F7 越界，LOW）", url: "https://app.notion.com/p/FR-077-jedi-common-env-git-rm-cached-env-sample-R1-F7-LOW-3d5346da4cd0816498f0c2c42a271695", status: 作廢（09-21 統一派工，改照 SUMMARY 內化版處理）}
    - {id: CM-1599, title: "R1b 補掃密碼學核心（10 檔，jedi monorepo）", url: "https://app.notion.com/p/FR-077-1b-jedi-remote-agent-JWT-enroll-token-10-Opus-1M-effort-low-3d5346da4cd081f9a49fe3685dd92645", status: 已完成（掃描完成並驗收，2026-09-17）}
    - {id: CM-1600, title: "文件：彙整 R1 結果與裁決進 FR 主檔並 build HTML", url: "https://app.notion.com/p/FR-077-R1-FR-build-HTML-R2-R3-R1b-Sonnet-5-3d5346da4cd08104b463fba6457386a4", status: 修正待驗證}
relates: [FR-075, FR-076, FR-039, FR-058, FR-066, FR-069]
---

> **這是六個資安掃描 arc 之一。** 六個 arc 合起來的結論、修正卡狀態、還沒開卡的待辦與覆蓋率地圖，在跨 arc 總表：[`security-scan-consolidated/`](../security-scan-consolidated/index.html)——**要問「總共掃出什麼、還有哪些要修」看那份，本頁只講這一支。**

用 Claude Code 的 `claude-security` plugin 掃「遠端 Agent 控制鏈」，找資安問題。
**只掃不修**——本 arc 只掃描，修正另開卡；修正現況見下方修正卡清單。

---

# ✅ 本 arc 收口（2026-09-17）

五棒全掃完，總結在 [SUMMARY.md](SUMMARY.md)。以下保留續行期的紀錄供追溯。

## ▶ 續行期（2026-09-15 解除暫緩，2026-09-16 三張卡重寫）

**現況**：R1b（CM-1599）10 檔零漂移可直接派；R2a／R2b／R3 三張卡因套件 9 月 11 日拆檔（`plugin.py`→`plugin/` 五檔、`api/` 拆出 route 表與守門殼，12 支任何一棒沒掃過）與 `fc1cadb`（控制面改兩段式身分）而重寫派工段，舊段留卡上標作廢。**CM-1595 曾於 2026-09-16 降為 HIGH、同日撤銷回到 CRITICAL**——首腦開檔比對 `fc1cadb` 前後：兩版都是拿請求裡的 agent 編號查列、不比對「你是不是那台」，那筆修改只收緊執行身分，冒充別家 agent 的路一樣走得通。建議派工順序 R1b → R2a → R2b → R3，一次一棒。

以下為 2026-09-08 暫停時的紀錄，保留供追溯：

**決策者裁定：FR-077 暫停，改從其他 jedi-* 套件開始掃。**

理由：R1 雖然找到 7 個真問題，但掃描工具的驗證面板因額度用盡全滅，**工具的正式產出是空的、覆蓋率無法宣稱**，靠人工補核才有東西——這樣的產出無法用來證明「這個套件檢查過了」。與其在同一個套件上反覆試工具極限，不如先掃風險更高、範圍天然較小的套件。

**已完成的部分（R1 的 7 條發現）依然有效**——每條都經 runner 逐條開檔核對＋首腦複核關鍵條，修正卡是照它開的，依據紮實。不可信的是「只有這七條」。

**要續行時**：其餘四棒（R1b 10 檔／R2a 18 檔／R2b 13 檔／R3 17 檔）的卡都開好了、範圍與重點都寫死在卡裡，檔數已按新尺（每棒 ≤15 檔）調過，隨時可派。四張修正卡（CM-1595～1598）已隨修正線處理（見下方修正卡清單）。

交接與全套件排名見 [`security-scan-STATE.md`](../FR-075-2609-jedi-package-security-audit/handoff/security-scan-STATE.md)。

---

# 🔴 一頁看完：R1 掃到什麼

第一階段（R1，42 個檔）掃完了，**找到 7 個問題**。其餘四棒（R1b／R2a／R2b／R3）已開卡但暫停。

## 最重要的一件事

**我們發給客戶、裝在他們機房的 agent，跟雲端之間的四條通訊管道，完全不檢查對方是誰。**

程式碼裡寫著「不用檢查，因為外層 nginx 會驗客戶端憑證」——**但出貨的 nginx 設定根本沒有那一行**（三個設定檔全部找不到）。所以實際情況是：**你在請求裡自己寫「我是哪台 agent」，系統就信你**。

最嚴重的後果：打一個 HTTP 請求過去，系統會回你那台 agent 的待辦工作，**裡面包含解密後的明文帳號密碼**。而且那不是我們產品的密碼，是**客戶自己機器的密碼**（客戶的 SonarQube token、客戶伺服器的 SSH／WinRM 帳密）。

## 七個問題一覽

| # | 嚴重度 | 白話：這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 修正卡 |
|---|---|---|---|---|---|
| **F1** | 🔴 **最嚴重** | 「報到」這條管道不檢查身分，你說你是哪台，系統就信 | **拿走客戶自己機器的帳號密碼**（SonarQube token、SSH／WinRM 帳密） | 只要連得到 API，**不用帳號密碼**；再知道任一台 agent 的編號（一般員工在畫面上就看得到） | CM-1595 |
| **F3** | 🟠 嚴重 | 同一條管道，連「這台 agent 是不是我們家的」都沒查 | **A 公司能拿走 B 公司的帳密**，多租戶隔離在這條路上等於不存在 | 同 F1，再知道別家一台 agent 的編號 | CM-1595 |
| **F2** | 🟠 嚴重 | 「回報工作做完了」這條管道，不檢查這份工作是不是你的 | **可以偽造或抹掉稽核證據**——把沒做過的掃描標成「通過」，或把真的發現問題的結果蓋掉。客戶拿這些紀錄去應付外部稽核 | 只要連得到 API，**不用帳號密碼**；再知道一個任務編號 | CM-1595 |
| **F5** | 🟡 中等 | 同上，只是換個角度看的同一個洞 | 同 F2 | 同 F2 | CM-1595（併） |
| **F4** | 🟡 中等 | 管理頁「測試連線」按鈕，填什麼網址就打什麼 | 可以拿我們的伺服器**當跳板去掃客戶內網**，回傳結果還會告訴他哪台通、哪台不通 | **需要管理員帳號**（所以比上面低一級） | CM-1596 |
| **F6** | 🟡 中等 | 安裝用的通行碼永久有效、整間公司共用一組；拿著它就能宣稱「我是那台機器」把別台搶過來 | 把別台 agent 的連線位址改成攻擊者的機器，之後雲端要資料就送到他那裡 | 拿到該公司的安裝通行碼（裝在他們每一台機器上）＋知道另一台的編號 | CM-1597 |
| **F7** | ⚪ 低 | 有個設定檔（`.env`）不小心進了版控 | 目前裡面只有 localhost 預設值、沒有真實密碼。風險是**以後**有人填了真的密碼會靜默外洩 | 不適用（現在還沒東西可洩） | CM-1598 |

> **越界說明**：F7 的檔案不在這一棒的掃描範圍內（掃描工具為了追脈絡讀到隔壁套件）。事實成立，但要記得它不是本棒範圍的產出。

## 決策者已經裁了什麼（2026-09-08）

- **F1 升到「最嚴重」**（原本 runner 判「嚴重」）。理由：不用帳號密碼就打得到 ＋ 能跨公司 ＋ 洩漏的是客戶自己的密碼，三個條件全中。
- **F1／F2／F3／F5 併成一張卡**（CM-1595）修——它們是同一個毛病，修法也是同一套。
- **nginx 那條設定併進同一張卡**，不另開。因為**光補 nginx 沒用**：nginx 只驗「這是不是我們發的憑證」，驗不出「你是不是你說的那台」。
- **要補掃一小棒**（CM-1599）：這次八個重點只碰到一個，其中「簽憑證」「簽 token」兩塊是加密核心，掃出來零問題——但複審沒跑，分不出是「看過沒問題」還是「根本沒看」。
- **R2／R3 現在就派**。

## 這份結果可信嗎？

**可以拿來當修正依據，但不能拿來說「其他地方都乾淨」。**

掃描工具的「三人複審」這次**全部因為額度用完沒跑成**（105 個複審 agent 全滅）。少掉的是「幫忙抓誤報」這一環。

但這七條的複審**由人做了兩遍**：runner 逐條開檔案核對行號，首腦（本 session）再親自核 F1／F2／F3 的行號、自己 grep 三個 repo 確認 nginx 真的沒有那行設定、追明文密碼的流向。**每一條都是「打開檔案，那幾行就是那樣寫的」**——這是事實陳述不是推論，不需要投票來確認它存在。

**不可信的是「只有這七條」**：複審沒跑，加上研究員只有兩個，範圍內漏掉的機率比正常一棒高。所以才要補掃 R1b。

---

## 為什麼要做

客戶端機房裡跑著我們發出去的 agent，它會**主動撥回雲端**要工作、下載檔案、回報結果。
這條鏈從來沒有被系統性掃過：security-scan-2607 只掃主專案 BE 與 FE、FR-075 掃
jedi-common 與 jedi-iam、FR-076 掃 License 鏈。**Agent 控制鏈是完全空白。**

這條鏈的風險形狀跟前兩個 arc 都不同：

- **控制面四個端點刻意不掛任何 user 認證**（`/agents/register`、`/agents/heartbeat`、
  `/agents/tasks/<uid>/ack`、`/agents/tasks/<uid>/result`）。這是設計，不是疏漏——
  agent 不帶 user JWT，身分靠 mTLS 在上游 nginx 終結。但**BE 自己拿不到 client 憑證**，
  所以「是哪一台 agent」全靠 payload 或 header 自報。自報的東西能不能被冒充，是本次的核心問題。
- **agent 是裝在客戶機房的東西**。它拿得到 enroll token、拿得到雲端簽的憑證、拿得到
  JWT 公鑰。攻擊者若能拿到一台 agent（或它的 token），能做到什麼程度，要盤清楚。
- **這條鏈會下載檔案**。`GET /agents/files/<uid>` 回的是源碼壓縮包 binary 串流，
  授權判定在 jedi-detection 的 resolver 清單裡。

## 產品時程脈絡

第一個對外版本是落地版（Host），SaaS 版機制預留。裁決修正優先序時用
`docs/claude/memory/project_first_release_is_host_saas_reserved.md` 的判準——
**落地版打得到的優先**。Agent 鏈本身就是落地版的核心組件（客戶機房裝 agent），
故本 arc 的發現原則上都算「落地版打得到」。

## 怎麼拆（三棒）

這條鏈被 repo 邊界切成三段，掃描的 `scanRoot` 只能指一個目錄，故跨 repo 無法同一棒。

| 棒 | 範圍 | scanRoot | 檔數 |
|----|------|----------|------|
| **R1 agent 身分與註冊** | `jedi-remote-agent` 的 agent_auth ＋ remote_agent 全鏈 ＋ 三支管理 app service ＋ route ＋ plugin.py | jedi monorepo | 42 |
| **R2a 任務下發與狀態機＋控制面 route／守門殼** | `jedi-remote-agent` 的 agent_task 全鏈（狀態機、派工單、payload provider port）＋ `api/`、`plugin/` 拆檔 12 支（2026-09-16 併入） | jedi-remote-agent | 30 |
| **R2b 檔案取用與 agent 連線** | `jedi-detection` 的 agent 相關全部（含檔案下載授權 resolver）＋重疊帶入 agent_task 兩支 | jedi monorepo | 13 |
| **R3 主專案宿主接線** | 主專案 BE 的 `core/plugins/remote_agent.py`、`api/remote_agent/`、`infra/remote_agent/`、DI container、`agent_auth_settings`、資料面 mTLS adapter（接線已收進 core/plugins，舊 `app/remote_agent/` 不存在） | compliance-manager-be | 16 |

**R3 檔少但密度最高**——`admin_required` 實際疊了什麼、檔案下載端點的宿主側、
`RemoteAgentAdapter` 的 mTLS 實作、CM-1559 fail-open 的落點，全在這 17 檔裡。
規模與 FR-075 S1（11 檔）、FR-076 L1（10 檔）同級。

**建議順序**：R1 先跑（身分是其他棒的前提，**已完成**），R2a／R2b／R3／R1b 可平行（不同檔）。

**R2 為何拆成兩棒（2026-09-08）**：R1（42 檔）的驗證面板因 session 額度用盡全滅（105 個 verifier 一個沒活、21 票 0 投），證明該量級撐不住；手上唯一一次面板跑完是 FR-076 L1（10 檔）。故把原 R2（29 檔）拆成 18＋13。**R2b 額外重疊帶入 R2a 的 `agent_task_domain_service.py` 與 `agent_task_repo_impl.py` 兩支**——因為檔案下載授權判的是「這個檔案是不是該 agent 名下未完成任務引用的」，問題前半在 R2b、後半在 R2a，直接對半切會讓這個接縫沒人看（違反本 arc「按業務模組垂直切、不水平切」的原則）。重複報的部分由首腦驗收時挑掉。

## 共同設定（沿用 FR-075／076 教訓）

| 項目 | 值 | 為什麼 |
|---|---|---|
| runner 主 session 模型 | **Opus 5 (1M context)** | Sonnet 5（200K）跑 40 檔以上必被 180 秒 stall 門檻砍掉，S4～S7 首輪四棒共燒 29.6 小時零產出 |
| effort | `low` | medium 展開成矩陣派 40+ agent，同檔被 8 個研究員各讀一遍 |
| focus | `attack-surface` | 跳過 tests／dist／docs |

啟動前必驗 scope 檔數（`git ls-files` 對上卡片數字才啟動）。

## 進度總表

| 棒 | 卡 | 狀態 | 報告 | 發現數（嚴重度分佈） | 首腦驗收 |
|----|----|------|------|------|------|
| R1 agent 身分與註冊（42 檔） | CM-1592 | ✅ 掃完（**面板未跑成，額度全滅**，runner 逐條開檔核對＋首腦複核） | [scan-R1-agent-identity.md](scan-R1-agent-identity.md) | 7 條（3 HIGH／3 MEDIUM／1 LOW 越界），F1 經決策者升 CRITICAL → 1C 2H 3M 1L | ✅ 2026-09-08 |
| R1b 補掃密碼學核心（10 檔，`common/agent_auth/`） | CM-1599 | ✅ 掃完（2026-09-16；stamp `unverified` 但 `findings-refused`＝面板完整跑完，15 票全投；**runner 四件未交、首腦補寫**） | [scan-R1b-crypto-core.md](scan-R1b-crypto-core.md) | 通過 2（皆 MEDIUM）：F1 重複 CM-1597（前提由引用變實證）／F2 越界新發現＝jedi-issue 歷史第二把 GitLab token 未撤銷（總表第 83 項）；駁回 3（ca.py 簽憑證三條，事實屬實但被 enroll token 那道門擋住） | ✅ 已驗（2026-09-16）。**回答了 R1 留下的問題：簽憑證／簽 token 零發現是「讀過」不是「沒讀」**；⚠️ 駁回三條與 F1 互相倚靠，CM-1597 修好前只是暫時 |
| R2a 任務下發與狀態機＋控制面 route／守門殼（**30 檔**） | CM-1593 | ✅ 掃完（2026-09-16；stamp `verified`、2 研究員全回、15 票全投全 3:0——**remote-agent 第一次真正跑完面板**；**runner 四件未交、首腦補寫**） | [scan-R2a-task-dispatch.md](scan-R2a-task-dispatch.md) | 通過 5（3H 1M 1L），**淨新增 0**：F1 ack/result 無認證＝R1 F2/F5、F2 心跳回明文憑證＝R1 F1、**F3 任務租戶由呼叫者給的 uid 決定（跨租戶，3:0 坐實）**＝R1 F3、F4 無歸屬＝R1 F5、F5 SSRF 越界＝R1 F4 | ✅ 已驗（2026-09-17）。全部併 CM-1595／1596；**補完 CM-1595 攻擊鏈四點**（唯讀授權豁免 `/agents/`、宿主主動 fetch 攻擊者 blob、可選租戶的鍵三種、管理面回傳指紋）；七重點工具碰三個，首腦補四個 |
| R2b 檔案取用與 agent 連線（13 檔） | CM-1601 | ✅ 掃完（2026-09-16；stamp `unverified` 但 `findings-refused`＝面板完整跑完，15 票全投；四件交付齊） | [scan-R2b-file-access.md](scan-R2b-file-access.md) | 通過 4（皆 MEDIUM）：**F2＋F3 檔案下載端點零認證、租戶由攻擊者指定的編號決定＝新發現（第五個裸奔控制面端點，洩客戶源碼包）**／F4 重複 R1 F2/F5（CM-1595，但增量證實 domain service 這層也不查歸屬）／F1 越界＝jedi-issue 歷史 `.env` 的 GitLab＋GitHub 權杖（併總表第 83 項）；駁回 1（probe 明文退路，被 installer 預設 `full` 擋住） | ✅ 已驗（2026-09-16）。**回答卡片重點①且範圍更寬：`fc1cadb` 沒堵住跨租戶**——查租戶用的就是攻擊者給的編號且刻意提權；⚠️ 順帶更正：總表第 47 項「`upload_files` 零隔離」已過期（DEV 實查 RLS 四條 policy 齊全，疑 `a48b41cc` 補上） ✅ 已驗（2026-09-17）：四條屬實；F2／F3 併 CM-1595（首腦裁）；第 47 項更正屬實 |
| R3 主專案宿主接線（**16 檔**） | CM-1594 | ✅ 掃完（2026-09-17；stamp `verified`、2 研究員全回、21 票全投；**runner 四件未交、首腦補寫**） | [scan-R3-host-wiring.md](scan-R3-host-wiring.md) | 通過 6（3H 3M），**範圍內淨新增 0**：F2 心跳放解密憑證＝CM-1595 宿主側終點、F4／F5 檔案下載 header 自報＝CM-1595 第五端點；越界 F1＝CM-1629 同把、**F3 公開文件站洩 `cmmgr`＋兩組登入帳密（總表第 84 項，高，首腦親自 fetch 確認）**、F6＝第 3 項 | ✅ 已驗（2026-09-17）。八重點工具碰兩個，首腦補六項全乾淨。**FR-077 五棒收口，[SUMMARY](SUMMARY.md)** |

## R1 發現摘要（技術版，含程式碼位置）

**根因一句話**：四個控制面端點（`/agents/register`、`/agents/heartbeat`、`/agents/tasks/<uid>/ack`、
`/agents/tasks/<uid>/result`）完全不掛任何認證，程式碼註解說身分靠 mTLS 在上游 nginx 終結，
但出貨的 `nginx.onprem.conf`／`nginx.conf`／`nginx.e2e.conf` **三檔全部 grep 不到一行 `ssl_verify_client`**。
於是 agent 身分只剩「請求自己說它是誰」，任何能連到 API 的人都能冒充任一台 agent，
拿走該租戶掃描工具的明文帳密（SonarQube token、WinRM／SSH 帳密等，通往客戶自己的基礎設施）。
加上控制面無 user context、`session_scope` 走 super admin 繞 RLS（機制重複 CM-1559，
但在這條鏈上的具體後果是新發現），偽造與外洩都能跨租戶。

白話版見上方「一頁看完」；本節是給工程師的技術對照，完整核對過程與每條的建議修法見
[scan-R1-agent-identity.md](scan-R1-agent-identity.md)，此處只列摘要，**不重抄 602 行報告**。

| ID | 嚴重度 | 一句話 | 觸發前提 | 併進哪張修正卡 |
|---|---|---|---|---|
| F1 | 🔴 CRITICAL（決策者升） | 心跳無認證，自報 `agent_uid` 即取走該租戶明文工具憑證 | 能連到心跳端點（不需憑證）＋知道一個 `agent_uid`（低權限使用者即可從 UI 取得） | CM-1595 |
| F3 | HIGH | 心跳身分解析可跨租戶（與 F1 同根因，`heartbeat()` 查詢無 `tenant_id`，`register()` 有） | 同 F1＋知道另一租戶某台 agent 的 uid | CM-1595 |
| F2 | HIGH | ack／result 無認證且不查任務歸屬，可偽造或抹除稽核證據 | 能連到 ack/result 端點（不需憑證）＋知道一個任務 uid | CM-1595 |
| F5 | MEDIUM | ack／result 缺歸屬檢查（F2 的授權面，同檔同根因） | 同 F2 | CM-1595（併 F2） |
| F4 | MEDIUM | 健康檢查 `base_url` 未過濾直進 `httpx.get`，可當內網探測代理（SSRF） | 需租戶管理員權限（比 F1/F2 低一級的原因） | CM-1596 |
| F6 | MEDIUM | 持 enroll token＋知道另一台 agent 的 uid，即可接管該 agent 註冊列、改 `base_url` | 持有該租戶 enroll token（整租戶共用一組，無到期無次數上限）＋知道同租戶內另一台 agent 的 uid | CM-1597 |
| F7 | LOW，⚠️ 越界 | `jedi-common/.env` 受版控，`.gitignore` 對已追蹤檔無效 | 不適用（結構性風險，非當下可利用） | CM-1598 |

## 業界對照（首腦 2026-09-08 給決策者的分析）

### 同類產品怎麼做「agent 證明自己是誰」

我們的產品形態——雲端管理台＋裝在客戶機器上的 agent 定期撥回——在業界很成熟，
代表性產品有 Elastic Agent（Fleet）、Wazuh、Datadog Agent、CrowdStrike Falcon、
Tenable Nessus Agent、AWS SSM Agent，以及 Kubernetes 的 kubelet 加入叢集流程。
它們解法細節不同，但共同骨架幾乎一致：

| 環節 | 業界共同做法 | 我們現在 |
|---|---|---|
| 註冊用的通行碼 | 通行碼只用來換身分，用完即失效或短效（Kubernetes bootstrap token 預設 24 小時；Elastic 的 enrollment token 可撤銷、可設一次性；Wazuh 建議註冊密碼配合 IP 白名單） | 整個租戶共用一組、永不過期、無次數上限 |
| 註冊後的身分 | 每台 agent 拿到自己獨有的金鑰／憑證，之後每次通訊都用它證明身分（Elastic 每台一組 API key；Falcon／Datadog 每台獨立 agent ID＋金鑰；Kubernetes 每個節點一張自己的憑證） | 有簽憑證給 agent，但雲端從來不驗它——心跳只看請求裡自報的字串 |
| 誰負責驗 | 應用層自己驗是底線；TLS 層的客戶端憑證驗證是加分項，不是替代品 | 應用層不驗，全押在 nginx，而 nginx 也沒設 |
| 一台 agent 能碰到什麼 | 只能碰自己名下的東西（Elastic Fleet 的 policy 綁 agent；SSM 的 instance 只拿自己的 command） | 知道別台的編號就能拿別台的任務、替別台回報結果、跨租戶也行 |
| 給 agent 的秘密 | 盡量不給明文；給的話綁定「這台、這次、短效」（Vault agent 的 response wrapping、SSM 走 Secrets Manager 的權限） | 心跳回應直接夾解密後的客戶帳密 |

一句話總結差距：業界的通行碼是「換身分證的臨時號碼牌」，我們的通行碼是「永久通行證」；
業界每台 agent 有自己的身分證且每次查驗，我們發了身分證但門口從不看。

### 對到業界的嚴重度標準

- **F1／F3 心跳不驗身分＋跨租戶**：CWE-306（關鍵功能缺認證）＋ CWE-862（缺授權）。
  在 SaaS／多租戶產品的漏洞賞金計畫裡，「無需認證即可跨租戶讀取他人機密」一律是最高級（Critical）。
  而我們洩漏的還不是自己的機密，是客戶第三方系統的帳密——業界叫「credential exposure leading
  to lateral movement」，出事時是客戶被打穿，責任回到我們。
- **F2 偽造任務結果**：CWE-306＋資料完整性。對合規產品特別致命——客戶拿這些紀錄去應付外部稽核，
  等於核心產出物可被任意人塗改。業界一般判 High，對「稽核證據」類產品會上調。
- **F6 拿安裝碼搶 agent**：業界叫 enrollment token abuse，Elastic／Kubernetes 都靠「token 短效＋
  撤銷」壓低它。我們的 token 永久有效，所以這條實際比一般情況重。
- **F4 SSRF**：標準 Medium，需管理員權限。
- **F7 `.env` 進版控**：Low，結構性風險。

### 業界標準修法（五層）

我們其實已經有 80% 的零件（註冊時會簽憑證給 agent、會發 JWT 公鑰、agent 端也真的用
`cert=` 出示憑證），缺的是「雲端反過來驗 agent」這一段。

1. **應用層自己驗（治本，業界底線）**：agent 每次心跳／ack／result 用私鑰簽請求，
   雲端用存的公鑰驗。驗不過就拒絕——fail closed。這是 Kubernetes kubelet、Elastic Agent 的做法。
   `agent_uid` 只能當交叉比對，不能當身分來源。
2. **每個動作綁「你自己的東西」**：ack／result 檢查 `agent_tasks.agent_id` 與 `tenant_id`；
   心跳的指紋 fallback 加 `tenant_id`。
3. **通行碼降級成「臨時號碼牌」**：enroll token 加到期（業界 24 小時～7 天）＋次數上限；
   重註冊宣告既有 uid 須出示原憑證。
4. **nginx 補客戶端憑證驗證（加分項）**：補 `ssl_verify_client`。但業界共識是這不能當唯一防線——
   它只證明「持有我們發的某一張憑證」，證明不了「是你宣稱的那台」。Zero-trust 的原則就是不信網路層。
5. **減少給 agent 的明文秘密（中期 follow-up）**：業界做法是短效、綁單次任務的 wrapped secret。

## 決策紀錄（決策者 2026-09-08）

| 事項 | 裁決 | 理由 |
|---|---|---|
| F1 嚴重度 | 升 **CRITICAL** | 無認證＋跨租戶＋洩漏第三方憑證三條件全中；UUID 會出現在 UI／日誌／封包，業界從不把它當秘密 |
| A／B 併卡 | 併一張（CM-1595） | 修法是同一套 agent 認證機制，三端點掛同一驗證、共用測試；分兩張會讓兩個 runner 各寫一套 |
| nginx | 併進 CM-1595 當「連帶」，不另開卡 | 加分項非主修法；獨立開卡會讓人誤以為「補了 nginx 就算修好」。驗收條件明寫應用層 fail closed 為必要 |
| 補掃 | 開 R1b（CM-1599） | 密碼學核心（CA／JWT）零發現在面板未跑下不可信；`sign_csr()` 沿用 agent 自帶 subject、SAN 從自報 base_url 推導、帶 CLIENT_AUTH、無 CRL 都可疑 |
| R2／R3 | 現在就派 | 掃不同檔，與本批決策無關；R3 掃的正是 nginx 之外的另一半宿主設定，結果回饋到 CM-1595 |

## 修正卡清單

| 卡號 | 修什麼 | 嚴重度 | 優先序 | 依賴 | 狀態 | commit |
|------|--------|--------|--------|------|------|--------|
| CM-1595 | 控制面四端點驗 agent 簽章、fail closed、任務綁歸屬（F1+F2+F3+F5）＋ nginx 連帶 | 🔴 CRITICAL | 1 | — | ✅ 已修（CM-2052，1.21.0 出貨；原卡 09-21 作廢） | — |
| CM-1597 | enroll token 到期／次數＋重註冊持有證明（F6） | MEDIUM | 2 | **依賴 CM-1595；動同一支檔，序列做** | 🚫 裁定不修（決策者 10-01，總表 M01-3；SaaS 前再評估；原卡作廢） | — |
| CM-1596 | 健康檢查 SSRF（F4） | MEDIUM | 3 | 可與 CM-1595 平行 | ✅ 已修（1.21.1 CM-2370，總表 M01-2；原卡作廢） | — |
| CM-1598 | `jedi-common/.env` 受版控（F7 越界） | LOW | 3 | 獨立 | ✅ 已修（jedi 套件庫 `f6459efd`，總表 M01-4；原卡作廢） | — |
| CM-1599 | 補掃 R1b 密碼學核心 10 檔 | 掃描 | 與 R2／R3 平行 | — | ✅ 已完成（掃描完成並驗收） | — |

修完 CM-1595 這條鏈才算達到業界底線；CM-1597 做完才算追上主流。

## follow-up（不在本 arc 修）

- **心跳回應夾明文客戶憑證**——業界做法是短效、綁單次任務的 wrapped secret，非本 arc 治本範圍（見上「業界標準修法」第五層）。
- **憑證無 CRL／OCSP**——已 revoked 的 agent 憑證對 nginx 端仍視為有效，R1 未觸及 `common/agent_auth/ca.py`，留給 CM-1599 補掃判斷。
- **憑證到期無告警**——既有 memory [`docs/claude/memory/followup_agent_cert_expiry_no_renewal.md`](../../claude/memory/followup_agent_cert_expiry_no_renewal.md)：agent mTLS 憑證無 renew／rotate 邏輯，過期即無聲斷線，非本 arc 新發現。

## 相關

- 首腦手冊：`.claude/skills/security-scan-lead/SKILL.md`
- 現況（三個 arc 共用）：`docs/features/FR-075-2609-jedi-package-security-audit/handoff/security-scan-STATE.md`
- 方法論與失敗紀錄：`docs/features/FR-075-2609-jedi-package-security-audit/scan-methodology.md`
- 卡片骨架：`docs/claude/notion-card-templates.md`
