FR-077 總結報告 — 遠端 Agent 控制鏈資安檢查

FR-077 總結報告:遠端 Agent 控制鏈資安檢查

檢查期間 2026-09-07~17|分五次檢查完成|首腦已驗收


🔴 結論:整條控制鏈只有一件大事,但那一件很大

分五次檢查了 99 個檔案(套件本體 72 個、主專案接線 16 個、借用的檢測套件 11 個),五次一共通過 24 條發現。去除重複之後:

幾條 說明
併進既有的修正卡 22 反覆指向同一件事:客戶機房的代理程式跟雲端之間的通訊,雲端不檢查對方是誰
全新的問題 2 都是檢查範圍以外順手撈到的密碼外洩,跟遠端代理程式本身無關

這個套件真正的資安問題只有一個,就是 CM-1595。 它是六個資安檢查案裡唯一一件被評為「緊急」的。五次檢查有四次各自獨立地撞到它,每一次都從不同角度多補一塊拼圖——現在它的樣貌已經完整到可以直接動工修了(現況:已修,CM-2052)。

修正現況:當時(09-17)決策者決定修正卡先不派、等全部掃完分類後統一開工,避免同型問題散在不同卡片各修一半;09-21 起改照內化版問題總表(docs/security-report/SUMMARY.md)統一派工。結果:CM-1595(控制面不驗身分)已修(CM-2052,1.21.0 出貨);CM-1596 健康檢查 SSRF 於 1.21.1 補修(CM-2370);CM-1597 註冊 Token 裁定不修(落地版僅內網可達,SaaS 前再評估到期或一機一碼);CM-1598 版控 .env 已撤出(jedi 套件庫 f6459efd)。早期四張卡已作廢。


§1

一、五次檢查各看了什麼、找到什麼

次別 檢查什麼(白話) 檔數 通過幾條 範圍內新問題
R1 代理程式的身分與註冊——它怎麼證明「我是誰」 42 7 6(開出 CM-1595~1598 四張修正卡)
R1b 密碼學核心——簽憑證、簽通行證、連線加密 10 2 0(另撈到範圍外的 GitLab 權杖 → 總表第 83 項)
R2a 任務下發與狀態機+對外介面與守門殼 30 5 0
R2b 代理程式來拿檔案、雲端連回代理程式 13 4 0(另撈到範圍外的 GitLab/GitHub 權杖)
R3 主專案這端怎麼把套件接上來 16 6 0(另撈到公開文件站洩密 → 總表第 84 項)

怎麼讀這張表:R1 那次是第一次掃,所以問題都是新的;後面四次「範圍內新問題」全是 0,意思不是白跑——意思是那四次把同一條攻擊路徑翻來覆去驗證了四遍,都指向同一個結論。這反而是好消息:問題不是散的,是集中的。

§2

二、24 條通過發現怎麼歸併的

歸到哪裡 幾條 哪幾次撞到
CM-1595(緊急:控制面端點不檢查身分;✅ 已修 CM-2052) 13 R1 四條、R2a 四條、R2b 三條、R3 兩條
CM-1596(測試連線可當內網跳板;✅ 已修 CM-2370) 2 R1、R2a
CM-1597(註冊通行碼永不過期;🚫 裁定不修) 2 R1、R1b
CM-1598(套件的 .env 進了版控;✅ 已修 jedi f6459efd) 1 R1
CM-1629(資料庫密碼散在 249 個檔;✅ 已清 CM-2048) 1 R3 的 F1
總表第 3 項(原廠管理員密碼每套安裝都一樣) 1 R3 的 F6
GitLab/GitHub 權杖(併 CM-1573 那條線) 2 R1b、R2b
🆕 總表第 83 項(jedi-issue 歷史檔裡的第二把 GitLab 權杖) 1 R1b,越界
🆕 總表第 84 項(公開文件站洩露資料庫超管密碼) 1 R3,越界
合計 24

整個 arc 的淨新增只有兩條,而且兩條都是「越界」撈到的密碼外洩——工具因為設了攻擊面重點,會順手跑一輪密碼掃描,這兩條就是那輪撈到的,跟遠端代理程式本身沒有關係。

第 84 項值得單獨看一眼。 它的密碼跟 CM-1629 是同一把,本來不該算新的。但它新在位置:那份寫著密碼的需求文件在 docs/features/ 底下,而那個目錄每次推上主線就會自動部署到公開文件網站。首腦 2026-09-17 親自打開那個網址看過——頁面在線上、密碼字串在上面、四個帳號名可見。CM-1629 原本的前提是「拿得到程式碼倉庫的人才看得到」,現在變成「任何人,不用帳號、不用內網,開瀏覽器就看得到」。這是性質改變,所以另立一項。


§3

三、CM-1595 現在長什麼樣子

五次檢查把這件事拼完整了。它不是一個洞,是一整片沒上鎖的牆。

一句話

我們發給客戶、裝在他們機房的代理程式,跟雲端之間的五條通訊管道,全部不檢查對方是誰。 程式碼註解裡寫著「不用檢查,因為外層的 nginx 會驗客戶端憑證」——但出貨的 nginx 設定檔裡,那一行根本不存在(三個設定檔全部找不到)。

五個不檢查身分的端點

端點 做什麼的 打進去能拿到什麼
註冊(register) 代理程式第一次安裝時報到 取得一張身分憑證
心跳(heartbeat) 每隔一段時間報到、領工作 🔴 客戶自己機器的明文帳號密碼(SonarQube 權杖、SSH/WinRM 帳密)
收到工作(ack) 回報「我收到這份工作了」 搶走別台的工作
回報結果(result) 回報「我做完了,結果在這」 🔴 偽造或抹掉稽核證據
下載檔案(files) 來拿要掃描的源碼 🔴 客戶上傳來掃描的源碼壓縮包

前四條是套件那側的,第五條是主專案自己手動掛上去的裸端點。

「你是哪一家客戶」有三種鍵可以選,全都由攻擊者填

這是這件事最麻煩的部分。伺服器決定「這次動作算在哪一家客戶頭上」的做法是:拿請求自己送上來的那個編號,在一個刻意繞過客戶隔離的查詢裡去找,找到之後把整個執行環境切換成那一家。

可以用來選的鍵有三種:

  1. 代理程式編號——任何已登入的使用者在管理頁上就看得到
  2. 機器編號——同一份 VM 樣板複製出來的機器,這個值一模一樣
  3. 任務編號——沒有身分檢查的心跳端點,回應裡就會把它送給你

三種都要堵,只堵一種等於沒堵。

四個放大器

五次檢查陸續補上的細節,每一個都讓這件事更嚴重:

  1. 授權過期進入唯讀模式也照樣打得到——唯讀中介層的白名單明文豁免了整個 /agents/ 前綴。授權過期的環境不會因此變安全。
  2. 雲端會主動去抓攻擊者指定的檔案——回報結果時可以附一個檔案編號,雲端的收尾程式會照那個編號去把檔案抓回來,存成這次檢測的稽核證據。攻擊者從「能寫一筆假紀錄」升級成「能把自己準備的任意檔案塞進客戶的稽核案卷」。
  3. 管理頁把機器指紋回傳出去——等於送出冒充用的第二把鑰匙。
  4. 註冊用的通行碼四層都沒有到期機制——整家客戶共用同一組、永遠有效、撿到就能用(這一條是 CM-1597,R1b 把它從「引用別處說法」變成「逐檔實證」)。

修法主軸

四句話:

  1. 呼叫者的身分要由簽章證明,不是相信請求裡自報的編號。
  2. 歸屬比對要做到 domain 層——不能只在最外層掛一個守門裝飾器,因為只要之後多一條呼叫路徑(批次補登、維運工具、資料修復腳本)就會再繞過一次。
  3. 五個端點要一起修——只修其中幾個,剩下的還是門戶洞開。
  4. nginx 補上客戶端憑證驗證只是連帶——它只證明「你是一台登記過的代理程式」,不證明「這張工單是你的」,所以應用層的比對不能省。

§4

四、預計怎麼修復

以下是掃描當時(2026-09-17)規劃的修法;原四張卡已作廢,實際處理現況見上方「修正現況」。

🔴 CM-1595:五個端點不檢查身分(唯一的「緊急」,優先做)

現況:已修(CM-2052,1.21.0 出貨;原卡作廢)。

分四步,順序不能顛倒。

第一步:先決定用哪一種身分證明 ⚠️ 這步要決策者拍板

有兩條路,選一條做主、另一條最多當輔助:

方案 怎麼運作 優點 代價
短效通行證(建議) 平台本來就會簽這種通行證,讓代理程式每次呼叫都帶上 程式本身就有,不必動基礎設施;應用層自己驗得了,客戶怎麼架都不受影響 要處理通行證的發放與更新
客戶端憑證 由 nginx 在最外層驗,驗過的身分往後傳 連線層就擋掉,應用層負擔輕 要改客戶機房的 nginx 設定,落地版客戶各自裝機,改動成本高;且它只證明「你是一台登記過的機器」

建議走短效通行證當主軸——理由是不用碰客戶端的基礎設施。客戶端憑證可以之後再補,但不能拿它當唯一手段。

第二步:五個端點一起掛上驗證

  • 從驗過的通行證推出「你是哪一台代理程式」,不再讀請求裡自報的編號
  • 同時比對通行證綁定的裝置指紋與資料庫裡存的那個
  • 當認證設定是「完整」模式時,沒有驗過身分就直接拒絕(現在的預設值是「不驗」)

第三步:歸屬比對要做進 domain 層

不是在路由上加一個裝飾器就好。要讓「這張工單是不是你的」變成狀態轉換函式的必填條件——任何呼叫端都不可能繞過。具體是兩個斷言:工單記的代理程式要等於呼叫者、工單的客戶要等於呼叫者的客戶。

還有一件事必做:那個繞過客戶隔離的查詢要收窄,只能用來「查呼叫者自己是誰」,絕對不能用來查一個由呼叫者指名的東西。這是整條攻擊路徑成立的關鍵。

第四步:把機器編號這條路作廢

心跳端點不得再接受「機器編號」當作身分的替代依據。這條一作廢,管理頁回傳裝置指紋就不再是問題(放大器③自動解除)。

做完怎麼驗:

  • 手動測試:不帶任何憑證直接打這五個端點,全部要被擋(401/403);帶 A 客戶的合法憑證去回報 B 客戶的任務編號,要被擋。
  • 要寫測試(權限與身分驗證屬專案測試政策的例外,必須寫):把歸屬比對那行拿掉,測試要變紅。
  • 回歸測試:真實的代理程式要還能正常註冊、心跳、領工作、回報——這條不能漏,改壞了客戶機房的掃描會整條停擺。

CM-1596:測試連線可以當成內網跳板

現況:已修(CM-2370,1.21.1;原卡作廢)。

改動小,但要決定「限制到多嚴」。

管理頁上的「測試連線」按鈕,使用者填什麼網址就去打什麼,沒有任何限制。攻擊者可以拿我們的伺服器當跳板探測內網。

兩層做法,建議都做:

  1. 送出前驗網址:只准 http(s);把主機名稱解析出來後拒絕本機位址、link-local 與私有網段——解析之後要再檢查一次,避免解析結果中途被換掉。
  2. 回傳改成只說「通或不通」,不要把對方的狀態碼與原始錯誤訊息照原樣吐回去(攻擊者就是靠這個分辨「服務在但拒絕」與「根本沒東西」)。

更嚴的選項(要決策者裁):乾脆只准打「已經登記在該客戶名下的代理程式位址」。好處是徹底堵死,代價是管理員沒辦法用這個功能測試一台還沒登記的機器——這是產品體驗的取捨。

做完怎麼驗:手動填幾個內網位址(資料庫、Redis、授權服務)進去測,應該全部被擋;填一台合法的代理程式位址,應該正常回報通或不通。


CM-1597:註冊用的通行碼永遠不會過期

現況:🚫 裁定不修(落地版僅內網可達,SaaS 前再評估;原卡作廢)。

⚠️ 這張卡依賴 CM-1595,要等它先做完。

代理程式第一次安裝時要拿一組通行碼來註冊。現在這組碼整家客戶共用同一組、四層都沒有到期機制、用幾次都可以。撿到的人可以隨時註冊一台自己的代理程式進去。

三件事:

  1. 加上到期時間(建議一次性或短期有效)
  2. 加上使用次數上限(用過就作廢最乾淨)
  3. 重新註冊時要求持有證明——證明你就是原本那台,不是撿到碼的人

做完怎麼驗:手動測試——拿一組用過的碼再註冊一次,要被擋;拿一組過期的碼註冊,要被擋;正常的首次安裝流程要還能走完。


CM-1598:套件的 .env 檔進了版控

現況:已修(jedi 套件庫 f6459efd;原卡作廢)。

最單純的一張。 jedi-common 的 .env 被提交進版控了,裡面有設定值。

做法:從版控裡移除(保留本機檔案)、改放一份不含真實值的範本、.gitignore 補上。

⚠️ 如果那個檔裡有真的密碼,要換——移除檔案不會讓版控歷史裡的值消失。動手前先確認裡面到底有什麼。

做完怎麼驗:確認版控裡查不到那個檔,且開發環境照範本設定後還能正常啟動。


§5

五、這次檢查可信到什麼程度

要分兩件事回答。

第一層:「這些問題是真的嗎」→ ✅ 可信

次別 檔數 候選→通過 投票數 漏投 中斷 驗證章
R1 42 7→7 0 🔴 21 🔴 全滅 unverified
R1b 10 5→2 15 0 0 渲染退件
R2a 30 7→5 15 0 0 ✅ verified
R2b 13 — →4 12 0 0 ✅ verified(工作區 dirty)
R3 16 8→6 21 0 0 ✅ verified(工作區 dirty)

怎麼讀這張表:工具會派三個獨立檢查員對每條發現各投一票。「投票數 21」就是「7 條候選 × 3 個檢查員,21 票全投出」。漏投 0、中斷 0,代表每一條都被完整審過。

R1 那次投票全滅(額度用盡),所以它的七條是靠人工逐條開檔核對出來的。但後面四次把同一條路徑又驗了四遍、每次都 3:0 通過——R1 的結論等於被重新背書了四次,這比它自己一次就投票成功還可靠。

而且首腦沒有只採信工具:CM-1595 的核心(「切換成哪一家客戶是由攻擊者填的值決定的」)是首腦 2026-09-16 自己開檔比對出來的,當時甚至推翻了自己前一天把它降級的決定;R3 那條公開網站洩密,是首腦親自打開網址看到頁面才登記的,不是讀程式碼推的。

第二層:「是不是只有這些」→ ⚠️ 中等偏高,但仍不能說「掃乾淨了」

好的一面:

  • 整條鏈五棒全掃完了——套件本體、借用的檢測套件、主專案接線,沒有留空白段
  • 後四次都面板完整跑完、驗證章通過
  • 卡片點名要查的重點,工具沒碰到的部分首腦全部人工補查過,而且結論都是乾淨的(R3 八個重點裡六個是人工補的,六個全乾淨)

不能宣稱完整的一面:

  1. 五次全是低強度快篩——沒有元件盤點、沒有威脅建模、沒有廣度掃蕩
  2. 沒有逐檔閱讀紀錄——工具的閱讀帳本是空的(coverage.research 是 null),無法證明每個檔都被讀到結論
  3. R2b 與 R3 掃描時工作區不乾淨——有別條線的未提交改動,那些改動不在檢查範圍內、不影響結論,但嚴格說掃的不是一棵乾淨的樹

結論:這些問題都是真的;「這條控制鏈只有這些問題」這句話不成立。


§6

六、這次換到的兩條經驗

① 一次 42 個檔會全滅,拆小四棒全部跑完

R1 那次一口氣掃 42 個檔,結果105 個檢查員全部因額度用盡掛掉、21 張票一張都沒投出來——工具的正式產出是空的,七條發現全靠人工補核才留下來。當時決策者因此把整個 arc 暫停,理由很直接:「這樣的產出無法用來證明這個套件檢查過了」。

續行時改了尺,拆成四小棒(10/30/13/16 檔),四棒的面板全部完整跑完。

差別不只在檔數——R2a 那棒也有 30 個檔,一樣跑完了。真正的差別是有沒有跟別的工作搶額度。所以規矩不是「檔數要少」,是「一次只跑一棒,不併行」。

② 同一件事被四棒從四個角度撞到,才拼出完整形狀

如果只看 R1,CM-1595 是「四個端點沒認證」。五棒跑完之後它變成「五個端點、三種可選的身分鍵、四個放大器」——而這些不是靠更用力掃同一個地方掃出來的,是換角度看出來的:

  • R2a 從任務服務那一側看 → 發現任務編號也能拿來選客戶
  • R2b 從狀態機那一側看 → 發現只在路由掛守門不夠,要做到 domain 層
  • R3 從主專案接線那一側看 → 發現明文帳密是在哪一行生出來的、還多一個裸掛的端點

「範圍內新問題 0」的四棒,價值全在這裡。 換角度比換力氣有用。


§7

七、五份詳細報告

報告 檢查什麼 範圍內新問題
R1 代理程式的身分與註冊 42 檔 6(開出四張修正卡,含唯一的「緊急」)
R1b 密碼學核心 10 檔 0(越界撈到總表第 83 項)
R2a 任務下發與狀態機 30 檔 0(補完 CM-1595 的四點細節)
R2b 檔案取用與 agent 連線 13 檔 0(找到第五個裸奔端點)
R3 主專案宿主接線 16 檔 0(越界撈到總表第 84 項,公開站洩密)

§8

八、交付狀況

🔴 五棒之中,三棒的 runner 四件全未交(沒寫報告、沒 commit、沒回寫 Notion、沒回報):R1b、R2a、R3。這三份報告都是首腦到掃描目錄自己撈產物、自己逐條開檔核對、自己補寫的。R2b 那棒交付齊全。

三次分別是全計畫的第九、第十、第十一次 runner 交付掛零。