---
title: C20 agent → 主系統前門
eyebrow: Guidant AI 資安檢視總報告 · 連線清單
h1: C20 代理程式 → 主系統前門（控制面，HTTPS :443）
lede: 裝在客戶機房的代理程式主動撥出、連回我們雲端的那條線——報到、心跳領工作、確認收到、回報結果、下載檔案全走它。這條線的另一端在我們控制之外，而且**不帶任何使用者登入**：對方是機器不是人。這一頁回答：這條線**怎麼連**、**哪些功能走它**、它帶來**什麼威脅、駭客怎麼打**、我們**要怎麼防、目前做到哪**。
chips:
  - { text: "裝了 agent 才有", kind: plain }
  - { text: "不帶使用者登入", kind: warn }
  - { text: "威脅 4 種", kind: accent }
  - { text: "掃描命中 4 條", kind: plain }
---

> **這一頁怎麼來的**：[DFD Level 0](../DFD/dfd-level0.html#c20) 把產品運作時的連線編成 C01～C25；[STRIDE 六頁](../STRIDE/S-spoofing.html)每一條問題都標了發生在哪幾條線。這裡以**連線**為單位，把這條線的特性、掃描在這條線上抓到的攻擊手法、以及由此該有的防線整理成一頁。個別問題修了沒不在這頁講，請點條目連回 STRIDE 頁。

## 一、連線圖

```{.mermaid cap="C20 — 代理程式到前門。agent 從客戶機房撥出，經前門 :443 進 api 的「控制面」五支端點。前門不驗身分，api 入口也不驗使用者 JWT——這五支靠「控制面通行證」認機器。紅色是只憑一組註冊 token 就能打的入口。"}
%%{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](C21-api-to-agent-dataplane.html) 資料面）。[DFD](../DFD/dfd-level0.html#c20) 寫「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](../STRIDE/E-elevation-of-privilege.html#m01-1)、[M03-13](../STRIDE/E-elevation-of-privilege.html#m03-13)、[M03-14](../STRIDE/E-elevation-of-privilege.html#m03-14)、[M03-9](../STRIDE/I-information-disclosure.html#m03-9)），歸納起來是**四種攻擊手法**。前提都一樣：這條線上**沒有人在登入**，認的是機器——認錯機器，後面全部失守。

| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | 🔴 最嚴重 | **冒充代理程式——後端認不出對方是哪台機器** | ① 站在任何連得到雲端 :443 的位置，不需要帳號；② 照代理程式的格式打「心跳」說「我是客戶 X 的機器 Y」；③ 後端若只讀請求裡自報的編號就相信，就把派給 Y 的工作連同**解密後的主機帳密**交出來；④ 再打「回報結果」替 Y 回「掃描全部正常」，或把真實結果蓋掉；⑤ 管理員按「撤銷」後若兩條通道不查撤銷，被撤銷的機器還能繼續送假資料 | 冒充任一家客戶的機器、拿走客戶主機明文帳密、偽造或抹除弱點掃描的稽核證據 | [S 冒充身分](../STRIDE/S-spoofing.html)、[I 資料外洩](../STRIDE/I-information-disclosure.html)、[T 竄改資料](../STRIDE/T-tampering.html) | [M01-1 代理程式五條通訊管道全部不檢查對方是誰](../STRIDE/E-elevation-of-privilege.html#m01-1) 🔴 |
| T2 | 🟠 高 | **回報的內容被當成可信的——檔名、格式照單全收** | ① 控制得了代理程式回應的人（被入侵的 agent、或 T1 冒充成功的人）回報一份掃描報告，檔名自己取（含路徑片段、或 `.html` 裡藏程式）；② 後端照外面給的檔名存成證據，不去掉路徑、不查副檔名；③ 稽核人員在證據清單點「預覽」，報告裡的程式在他的瀏覽器、以他的身分跑起來 | 稽核人員的登入身分（通往 [C01](C01-browser-to-frontdoor.html) T4） | [T 竄改資料](../STRIDE/T-tampering.html)、[E 權限提升](../STRIDE/E-elevation-of-privilege.html) | [M03-13 掃描報告「檔名叫什麼就存什麼」，走同一條預覽路徑](../STRIDE/E-elevation-of-privilege.html#m03-13) 🟠 |
| T3 | 🟡 中 | **從畫面那頭發動——不該動手的人派出帶帳密的掃描** | ① 以唯讀角色登入畫面（這一步在 [C02](C02-frontdoor-to-api.html)）；② 按「開始掃描」，端點只確認他是專案成員就放行；③ 派工單寫進資料庫，**代理程式下次心跳經這條線領走**，帶著客戶主機帳密連進正式機器；④ 反覆取消再重派干擾檢測，或刪掉執行紀錄 | 以不該有的身分發動帶帳密的掃描、改掉掃描執行紀錄 | [E 權限提升](../STRIDE/E-elevation-of-privilege.html)、[R 事後無法追查](../STRIDE/R-repudiation.html) | [M03-14 唯讀角色可以對客戶機器發動掃描、刪掉執行紀錄](../STRIDE/E-elevation-of-privilege.html#m03-14) 🟡 |
| T4 | 🟡 中 | **領工作時要用的祕密，在資料庫裡多留了一份明文** | ① 派工時系統把客戶主機帳密解密，準備讓代理程式心跳時領走；② 解密後的明文順手寫進派工單的參數欄位、落到資料庫；③ 拿到資料庫備份或任一唯讀帳號的人查派工表——每次掃描都留一份客戶正式機房的密碼，且不會清理 | 客戶正式機房的可用主機密碼與掃描器管理員權杖 | [I 資料外洩](../STRIDE/I-information-disclosure.html) | [M03-9 開始掃描時把解密後的主機帳密多存一份明文](../STRIDE/I-information-disclosure.html#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](C01-browser-to-frontdoor.html) D4 收口。⚠️ 回報 payload 裡其他欄位（摘要統計、訊息文字）怎麼進畫面沒逐欄盤過 |
| D4 | **鑰匙只在領工作那一刻解密、只留記憶體**：派工單落庫時不含明文帳密；代理程式領單時另一條路當場解密 | T4 | ✅ `_dispatch_one()` 寫明文那一行已刪，心跳時走 `detection_task_payload_provider._resolve_tool_credentials()` 當場解密；DEV 殘留查為 0 筆。⚠️ 心跳回應本身仍含明文帳密（這是設計：代理程式要拿它去連主機）——所以 D1 認錯機器的後果就是帳密外流，兩條防線是連體的 |
| D5 ◇ | **誰能派工，在畫面那頭就要守住**。發動掃描、取消、刪紀錄只放行任務被指派人與專案管理者 | T3 | ✅ 八支寫入端點已換成 `assert_job_operator`（只放行被指派人與 manager），詳見 [C02](C02-frontdoor-to-api.html) 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 小時）與一次性（用過即廢），是整條線上成本最低、擋得最前面的一刀。

## 六、依這些防線，掃描還沒看過的地方

- **註冊 token 的生命週期**（D2）：沒有過期、沒有次數上限、沒有「哪台機器用了它」的紀錄。掃描六頁沒有任何一條提到它。
- **前門層 mTLS 的實況**（D7）：DFD 與多份文件寫「控制面 mTLS」，實際 nginx 不驗。掃描是讀程式碼不是讀部署設定，所以沒抓到；這是文件與現況的落差，不是程式 bug。
- **回報 payload 的其他欄位**（D3）：檔名修了，摘要統計與錯誤訊息怎麼進畫面、有沒有跳脫，沒逐欄看。
- **心跳頻率與速率限制**：一台被入侵的 agent 用通行證每秒打心跳，後端每次都查一次 DB——有沒有上限？沒看。

---

*依據：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。*
