FR-077 · 需求索引 · 本頁由 build 掃資料夾生成
用 Claude Code 的 claude-security plugin 掃「遠端 Agent 控制鏈」,找資安問題。 只掃不修——產出是問題清單,修正另外開卡。
決策者裁定:FR-077 暫停,改從其他 jedi- 套件開始掃。*
理由:R1 雖然找到 7 個真問題,但掃描工具的驗證面板因額度用盡全滅,工具的正式產出是空的、覆蓋率無法宣稱,靠人工補核才有東西——這樣的產出無法用來證明「這個套件檢查過了」。與其在同一個套件上反覆試工具極限,不如先掃風險更高、範圍天然較小的套件。
已完成的部分(R1 的 7 條發現)依然有效——每條都經 runner 逐條開檔核對+首腦複核關鍵條,修正卡是照它開的,依據紮實。不可信的是「只有這七條」。
要續行時:其餘四棒(R1b 10 檔/R2a 18 檔/R2b 13 檔/R3 17 檔)的卡都開好了、範圍與重點都寫死在卡裡,檔數已按新尺(每棒 ≤15 檔)調過,隨時可派。四張修正卡(CM-1595~1598)同樣待派——其中 CM-1595 是 CRITICAL,首腦建議不該跟著 arc 一起停。
交接與全套件排名見 security-scan-STATE.md。
第一階段(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 的檔案不在這一棒的掃描範圍內(掃描工具為了追脈絡讀到隔壁套件)。事實成立,但要記得它不是本棒範圍的產出。
可以拿來當修正依據,但不能拿來說「其他地方都乾淨」。
掃描工具的「三人複審」這次全部因為額度用完沒跑成(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 都不同:
/agents/register、/agents/heartbeat、 /agents/tasks/<uid>/ack、/agents/tasks/<uid>/result)。這是設計,不是疏漏—— agent 不帶 user JWT,身分靠 mTLS 在上游 nginx 終結。但BE 自己拿不到 client 憑證, 所以「是哪一台 agent」全靠 payload 或 header 自報。自報的東西能不能被冒充,是本次的核心問題。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 任務下發與狀態機 | jedi-remote-agent 的 agent_task 全鏈(狀態機、派工單、payload provider port) |
jedi monorepo | 18 |
| R2b 檔案取用與 agent 連線 | jedi-detection 的 agent 相關全部(含檔案下載授權 resolver)+重疊帶入 agent_task 兩支 |
jedi monorepo | 13 |
| R3 主專案宿主接線 | 主專案 BE 的 api/remote_agent/、app/remote_agent/adapter/、infra/remote_agent/、DI container、agent_auth_settings、資料面 mTLS adapter |
compliance-manager-be | 17 |
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「按業務模組垂直切、不水平切」的原則)。重複報的部分由首腦驗收時挑掉。
| 項目 | 值 | 為什麼 |
|---|---|---|
| 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 | 7 條(3 HIGH/3 MEDIUM/1 LOW 越界),F1 經決策者升 CRITICAL → 1C 2H 3M 1L | ✅ 2026-09-08 |
R1b 補掃密碼學核心(10 檔,common/agent_auth/) |
CM-1599 | 待派 | — | — | — |
| R2a 任務下發與狀態機(18 檔) | CM-1593 | 待派(原 29 檔已收窄) | — | — | — |
| R2b 檔案取用與 agent 連線(13 檔) | CM-1601 | 待派(由原 R2 拆出) | — | — | — |
| R3 主專案宿主接線(17 檔) | CM-1594 | 待派 | — | — | — |
根因一句話:四個控制面端點(/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,此處只列摘要,不重抄 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 |
我們的產品形態——雲端管理台+裝在客戶機器上的 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 有自己的身分證且每次查驗,我們發了身分證但門口從不看。
.env 進版控:Low,結構性風險。我們其實已經有 80% 的零件(註冊時會簽憑證給 agent、會發 JWT 公鑰、agent 端也真的用 cert= 出示憑證),缺的是「雲端反過來驗 agent」這一段。
agent_uid 只能當交叉比對,不能當身分來源。agent_tasks.agent_id 與 tenant_id; 心跳的指紋 fallback 加 tenant_id。ssl_verify_client。但業界共識是這不能當唯一防線—— 它只證明「持有我們發的某一張憑證」,證明不了「是你宣稱的那台」。Zero-trust 的原則就是不信網路層。| 事項 | 裁決 | 理由 |
|---|---|---|
| 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-1597 | enroll token 到期/次數+重註冊持有證明(F6) | MEDIUM | 2 | 依賴 CM-1595;動同一支檔,序列做 | 待派 | — |
| CM-1596 | 健康檢查 SSRF(F4) | MEDIUM | 3 | 可與 CM-1595 平行 | 待派 | — |
| CM-1598 | jedi-common/.env 受版控(F7 越界) |
LOW | 3 | 獨立 | 待派 | — |
| CM-1599 | 補掃 R1b 密碼學核心 10 檔 | 掃描 | 與 R2/R3 平行 | — | 待派 | — |
修完 CM-1595 這條鏈才算達到業界底線;CM-1597 做完才算追上主流。
common/agent_auth/ca.py,留給 CM-1599 補掃判斷。docs/claude/memory/followup_agent_cert_expiry_no_renewal.md:agent mTLS 憑證無 renew/rotate 邏輯,過期即無聲斷線,非本 arc 新發現。.claude/skills/security-scan-lead/SKILL.mddocs/features/FR-075-2609-jedi-package-security-audit/handoff/security-scan-STATE.mddocs/features/FR-075-2609-jedi-package-security-audit/scan-methodology.mddocs/claude/notion-card-templates.md以下全部由 build 掃資料夾產生,新增檔案重 build 即自動出現。標題連結指向渲染後的 HTML,md 連向源檔。
| 文件 | 類型 | 標題 | 最後更新 |
|---|---|---|---|
| scan-R1-agent-identity / md | 文件 | R1 掃描結果:Agent 身分與註冊(jedi-remote-agent) | 2026-09-08 |
卡片內容(決策紀錄、驗收條件)以 Notion 為準,本頁只記座標。
| 關係 | 卡號 | 標題 | 狀態 |
|---|---|---|---|
| 母案 | CM-1591 | FR-077 遠端 Agent 控制鏈資安掃描(3 棒,只掃不修) | — |
| 子卡 | CM-1592 | R1 agent 身分與註冊(42 檔,jedi monorepo) | 修正待驗證 |
| 子卡 | CM-1593 | R2a 任務下發與狀態機(18 檔,jedi monorepo;原 29 檔已收窄) | 待派 |
| 子卡 | CM-1601 | R2b 檔案取用與 agent 連線(13 檔,jedi monorepo) | 待派 |
| 子卡 | CM-1594 | R3 主專案宿主接線與資料面 adapter(17 檔,BE repo) | 待派 |
| 子卡 | CM-1595 | 修 控制面四端點驗 agent 簽章、fail closed、任務綁歸屬(F1+F2+F3+F5,CRITICAL) | 待派 |
| 子卡 | CM-1596 | 修 健康檢查 SSRF(F4,MEDIUM) | 待派 |
| 子卡 | CM-1597 | 修 enroll token 到期/次數+重註冊持有證明(F6,MEDIUM,依賴 1595) | 待派 |
| 子卡 | CM-1598 | 處置 jedi-common/.env 受版控(F7 越界,LOW) | 待派 |
| 子卡 | CM-1599 | R1b 補掃密碼學核心(10 檔,jedi monorepo) | 待派 |
| 子卡 | CM-1600 | 文件:彙整 R1 結果與裁決進 FR 主檔並 build HTML | 修正待驗證 |