Guidant AI 資安檢視總報告 · 連線清單
裝在客戶機房的代理程式主動撥出、連回我們雲端的那條線——報到、心跳領工作、確認收到、回報結果、下載檔案全走它。這條線的另一端在我們控制之外,而且不帶任何使用者登入:對方是機器不是人。這一頁回答:這條線怎麼連、哪些功能走它、它帶來什麼威脅、駭客怎麼打、我們要怎麼防、目前做到哪。
這一頁怎麼來的:DFD Level 0 把產品運作時的連線編成 C01~C25;STRIDE 六頁每一條問題都標了發生在哪幾條線。這裡以連線為單位,把這條線的特性、掃描在這條線上抓到的攻擊手法、以及由此該有的防線整理成一頁。個別問題修了沒不在這頁講,請點條目連回 STRIDE 頁。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB'}}}%%
flowchart LR
subgraph AGENTBOX["🏢 客戶機房(我們控制不到)"]
AG["evidence-agent<br/>持有:通行證、憑證、<br/>註冊 token"]
end
ATK(["🕵 攻擊者<br/>任何連得到 :443 的位置"])
subgraph MAIN["🏠 主系統 stack"]
direction TB
FE["guidant-fe<br/>nginx 前門 :443<br/>TLS 終止・不驗身分"]
subgraph API["guidant-api · 控制面端點"]
direction TB
REG["/agents/register<br/>報到:驗註冊 token"]
CP["/agents/heartbeat 領工作<br/>/agents/tasks/…/ack 確認<br/>/agents/tasks/…/result 回報<br/>/agents/files/… 下載檔案<br/>驗控制面通行證+查撤銷"]
end
DB[("guidant-db<br/>remote_agents<br/>agent_tasks")]
FE --> REG
FE --> CP
CP --> DB
REG --> DB
end
AG == "C20 · HTTPS :443<br/>Bearer 通行證" ==> FE
ATK -. "C20 · 同一條線<br/>不需帳號" .-> FE
classDef ext fill:#FBFCFC,stroke:#9AA8AA,stroke-dasharray:3 2
classDef open fill:#FDECEC,stroke:#C0392B
class ATK ext
class REG open
| 項目 | 現況 |
|---|---|
| 誰 → 誰 | 客戶機房的 evidence-agent 容器 → 我們的 guidant-fe 前門(nginx :443)→ 轉給 guidant-api。方向是客戶撥出,所以客戶防火牆只要放行出站 443,不必開任何入站埠 |
| 協定/埠 | HTTPS :443,路徑都在 /api/1.0/agents/… 下。agent 端 CLOUD_ENDPOINT 指定雲端位址 |
| 傳什麼 | 五件事:① 報到(送憑證申請、拿回簽好的憑證與通行證);② 心跳領工作(agent 定期來問「有沒有派給我的單」,回應裡帶任務參數——含當場解密的客戶主機帳密與工具權杖);③ 確認收到;④ 回報結果(掃描成功/失敗、報告檔編號);⑤ 下載檔案(規則包、使用者上傳的源碼包) |
| 怎麼驗身分 | 不是使用者登入。報到那一支靠註冊 token(管理員在畫面產生、每家客戶同時只有一組有效,DB 只存雜湊);其餘四支靠控制面通行證——報到時由後端用簽章私鑰簽發的長效 JWT(效期同憑證,預設 10 年),每個請求帶 Authorization: Bearer,後端驗簽後只認證裡的機器編號(請求裡自報的一律不採信),並每個請求查一次資料庫確認該台沒被撤銷或停用——撤銷靠查表即時生效,不靠票過期 |
| 雙向憑證(mTLS)的實況 | agent 端每次呼叫都會出示自己的客戶端憑證(cert=),但前門 nginx 沒有設 ssl_verify_client,從頭到尾不看它。所以這條線上實際生效的身分驗證只有通行證那一層;客戶端憑證在這條線是白帶的(它真正發揮作用的地方是 C21 資料面)。DFD 寫「mTLS 客戶端憑證+控制面簽章通行證」,前半句不符合現況,已回寫母卡 |
| agent 怎麼信雲端 | 落地版客戶的雲端幾乎都是自簽憑證。安裝時 install.sh 抓雲端憑證、把指紋印給安裝人員確認、存成 cloud-ca.pem;之後 agent 只接受這一張(「首次信任、之後釘住」,同 SSH 的做法)。程式刻意不提供任何「關閉驗證」開關——雲端換憑證會當場連不上,要重跑安裝確認 |
| 何時存在 | 客戶有裝代理程式才有。心跳間隔由雲端報到時回傳(預設 300 秒) |
依據:jedi_remote_agent/api/routing.py(路由表三級認證宣告)、jedi_remote_agent/api/guards.py 與 app/service/agent_control_auth_service.py(通行證守門)、jedi_remote_agent/common/agent_auth/jwt_util.py(通行證效期=cert_valid_days,預設 3650 天)、agent repo core/task_executor.py/core/cloud_trust.py/core/agent_auth.py、FE nginx.onprem.conf(無 ssl_verify_client)、主專案 api/remote_agent/routes/agent_file_route.py。
代理程式對雲端說的每句話都走這裡。對威脅分析來說分三群,因為三群的守門方式與後果都不同:
| 群 | 功能 | 守門靠什麼 | 對威脅的意義 |
|---|---|---|---|
| ① 報到 | 第一次安裝、換雲端重新註冊、憑證遺失重註冊 | 註冊 token(一家客戶一組)+憑證申請檔的名稱必須等於本機指紋+帶舊編號要附舊私鑰簽的持有證明 | 這是整條線身分的起點——token 流出去,攻擊者就能登記一台「假代理程式」進客戶名下,從此拿到真通行證 |
| ② 領工作與回報 | 心跳領單、確認收到、回報結果 | 通行證+每請求查撤銷+「這張單是不是派給這台」 | 領單的回應裡有客戶主機帳密的明文(當場解密);回報結果決定稽核證據長什麼樣——被冒充就是帳密外流+證據造假 |
| ③ 下載檔案 | 規則包、使用者上傳的源碼包 | 通行證+「這個檔是不是這台名下未完成任務的」 | 下載的東西會在客戶內網被當成程式執行(規則包就是掃描邏輯),檔案來源要是我們、內容要是當初上傳的那份 |
掃描在這條線上抓到 4 條問題(M01-1、M03-13、M03-14、M03-9),歸納起來是四種攻擊手法。前提都一樣:這條線上沒有人在登入,認的是機器——認錯機器,後面全部失守。
| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | 🔴 最嚴重 | 冒充代理程式——後端認不出對方是哪台機器 | ① 站在任何連得到雲端 :443 的位置,不需要帳號;② 照代理程式的格式打「心跳」說「我是客戶 X 的機器 Y」;③ 後端若只讀請求裡自報的編號就相信,就把派給 Y 的工作連同解密後的主機帳密交出來;④ 再打「回報結果」替 Y 回「掃描全部正常」,或把真實結果蓋掉;⑤ 管理員按「撤銷」後若兩條通道不查撤銷,被撤銷的機器還能繼續送假資料 | 冒充任一家客戶的機器、拿走客戶主機明文帳密、偽造或抹除弱點掃描的稽核證據 | S 冒充身分、I 資料外洩、T 竄改資料 | M01-1 代理程式五條通訊管道全部不檢查對方是誰 🔴 |
| T2 | 🟠 高 | 回報的內容被當成可信的——檔名、格式照單全收 | ① 控制得了代理程式回應的人(被入侵的 agent、或 T1 冒充成功的人)回報一份掃描報告,檔名自己取(含路徑片段、或 .html 裡藏程式);② 後端照外面給的檔名存成證據,不去掉路徑、不查副檔名;③ 稽核人員在證據清單點「預覽」,報告裡的程式在他的瀏覽器、以他的身分跑起來 |
稽核人員的登入身分(通往 C01 T4) | T 竄改資料、E 權限提升 | M03-13 掃描報告「檔名叫什麼就存什麼」,走同一條預覽路徑 🟠 |
| T3 | 🟡 中 | 從畫面那頭發動——不該動手的人派出帶帳密的掃描 | ① 以唯讀角色登入畫面(這一步在 C02);② 按「開始掃描」,端點只確認他是專案成員就放行;③ 派工單寫進資料庫,代理程式下次心跳經這條線領走,帶著客戶主機帳密連進正式機器;④ 反覆取消再重派干擾檢測,或刪掉執行紀錄 | 以不該有的身分發動帶帳密的掃描、改掉掃描執行紀錄 | E 權限提升、R 事後無法追查 | M03-14 唯讀角色可以對客戶機器發動掃描、刪掉執行紀錄 🟡 |
| T4 | 🟡 中 | 領工作時要用的祕密,在資料庫裡多留了一份明文 | ① 派工時系統把客戶主機帳密解密,準備讓代理程式心跳時領走;② 解密後的明文順手寫進派工單的參數欄位、落到資料庫;③ 拿到資料庫備份或任一唯讀帳號的人查派工表——每次掃描都留一份客戶正式機房的密碼,且不會清理 | 客戶正式機房的可用主機密碼與掃描器管理員權杖 | I 資料外洩 | M03-9 開始掃描時把解密後的主機帳密多存一份明文 🟡 |
四種手法的共同點:這條線上流的是客戶正式機房的鑰匙(主機帳密)與稽核證據的原料(掃描結果),而對方是一台我們管不到的機器。T1 是「認錯機器」;T2 是「認對了機器、但信了它說的每一個字」;T3、T4 是「鑰匙在送到機器之前,就已經在我們這邊被不該碰的人碰到」。前兩個是這條線的本質風險——機器身分一旦認錯,後面沒有第二道門。
對應四種威脅與這條線的本質,防線分七條。D1~D4 直接對應掃描抓到的四種手法;標 ◇ 的(D5~D7)是依這條線的特性補的標準防線,掃描範圍沒涵蓋、沒有實例。「目前」欄寫程式與部署裡實際有的機制;⚠️ 表示目前只靠慣例或只守到局部,沒有機制保證。
| # | 防線 | 擋哪種威脅 | 目前做到哪 |
|---|---|---|---|
| D1 | 每個請求都要有可驗證的機器身分,身分只認簽章、不認自報。報到之外的每一支端點都要帶後端簽發的通行證;後端驗簽後只用證裡的機器編號;每個請求查一次撤銷/停用狀態;「回報結果」要比對「這張單是不是派給這台」 | T1 | ✅ 路由表第三欄強制宣告每支端點的認證等級(管理員/代理程式/報到三級,認不得的在啟動時就報錯,不會有「忘了想」的公開端點);✅ agent_pass_required 守門+AgentControlAuthService 驗簽、只認 sub、每請求查 remote_agents;✅ 確認收到與回報結果在交易內再驗撤銷、比對派工對象,不符回「找不到」;✅ 主專案的檔案下載端點用同一支守門,不另寫一套 |
| D2 | 報到是身分的起點,要比其他端點更嚴:憑證申請檔的名稱必須等於本機指紋(擋「用自己的機器登記、名字填受害機」);帶舊編號重新登記要附舊私鑰簽的持有證明;被撤銷的機器一律拒收、不再洗回正常 | T1 | ✅ 三件都在 agent_enrollment_service.py。⚠️ 註冊 token 一家客戶一組、長期有效、沒有過期時間與使用次數上限(enroll_token 實體只有 enabled 旗標)——token 一旦流出,在管理員手動「撤銷並重產」之前,任何人都能登記新機器進該客戶名下。這是報到入口的最大缺口 |
| D3 | 代理程式回報的一切都是外部輸入:檔名由後端決定或消毒(切掉路徑、查副檔名白名單,不合格退回系統自產名);回報內容只更新該台名下的單 | T2 | ✅ 兩個檔名來源一律過 sanitize_report_filename(),不合格退回 detection_report_<任務編號>.html;✅ 預覽端另有 C01 D4 收口。⚠️ 回報 payload 裡其他欄位(摘要統計、訊息文字)怎麼進畫面沒逐欄盤過 |
| D4 | 鑰匙只在領工作那一刻解密、只留記憶體:派工單落庫時不含明文帳密;代理程式領單時另一條路當場解密 | T4 | ✅ _dispatch_one() 寫明文那一行已刪,心跳時走 detection_task_payload_provider._resolve_tool_credentials() 當場解密;DEV 殘留查為 0 筆。⚠️ 心跳回應本身仍含明文帳密(這是設計:代理程式要拿它去連主機)——所以 D1 認錯機器的後果就是帳密外流,兩條防線是連體的 |
| D5 ◇ | 誰能派工,在畫面那頭就要守住。發動掃描、取消、刪紀錄只放行任務被指派人與專案管理者 | T3 | ✅ 八支寫入端點已換成 assert_job_operator(只放行被指派人與 manager),詳見 C02 D1。這條線本身不驗人,守門在前一條線 |
| D6 ◇ | 通行證效期與撤銷機制要配套:長效票必須搭配每請求查撤銷,否則撤銷形同虛設;撤銷後所有通道(含下載)要同時失效 | T1 的延伸 | ✅ 通行證預設效期 10 年(與憑證同),但每個請求都查表,撤銷即時生效;DEV 實測撤銷後心跳/確認/回報/下載全部 403。⚠️ 票本身不會過期輪替——若後端簽章私鑰外流,10 年內簽出的任何票都有效,只能靠換私鑰(讓所有 agent 重新註冊)補救 |
| D7 ◇ | 前門層的雙向憑證要嘛真驗、要嘛拿掉。目前 agent 出示憑證、nginx 不看——多一層「看起來有在驗」的假象,日後有人以為 mTLS 在守而放鬆通行證那層,就出事 | T1 的延伸 | ⚠️ 前門 nginx 沒有 ssl_verify_client,agent 端白帶憑證。要嘛在前門對 /api/1.0/agents/ 路徑開 ssl_verify_client optional 並把結果傳給 api 做第二道確認,要嘛在文件與 DFD 明寫「控制面只靠通行證」——二選一,不要維持現在的半套 |
最該補的一步:D2 的註冊 token。這條線所有身分都從報到那一步長出來,而報到只憑一組「一家一組、永不過期、不限次數」的 token。給它加過期時間(例如 24 小時)與一次性(用過即廢),是整條線上成本最低、擋得最前面的一刀。
依據:STRIDE 六頁信任邊界連線標記(CM-2403/2404 驗收後版本)、jedi_remote_agent/api/routing.py/guards.py/app/service/agent_control_auth_service.py/agent_enrollment_service.py/agent_enroll_token_service.py、agent repo core/task_executor.py/core/cloud_trust.py/core/agent_auth.py/deploy/docker-compose.yml、FE nginx.onprem.conf、主專案 api/remote_agent/routes/agent_file_route.py、DFD Level 0。