FR-177.A2 資安掃描:資料面 8443 與主系統下達的指令(CM-3509)

⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。

§1

一句話結論

找到 1 個中風險問題:內網任何人只要對 8443 開一條連線、什麼都不送,就能讓 Agent 的資料面整個卡住(主系統派工、重啟、升級、取檔全部停擺),直到那條連線斷掉或服務重啟。它不會洩漏資料,也不會讓人冒用主系統下指令。「重點看什麼」七條逐條人工查過,其中守門殼、JWT 驗證、各指令端點、信任錨更新都沒有發現能繞過的路。

§2

這棒檢查什麼

主系統會主動連到 Agent 的 8443 埠(資料面)下指令,包括派任務、通知設定變更、換證、重啟、升級。這一棒要回答三個問題:Agent 怎麼確認「這真的是主系統」、哪些路徑不必驗證、Agent 信任哪一台主系統。

範圍共 11 個檔:

  • guidant_agent/sync/api/ 底下的 8 個檔(守門、伺服器、5 支 cmd_*.py、__init__.py)
  • guidant_agent/sync/domain/agent_auth.py
  • guidant_agent/sync/app/cloud_trust.py
  • guidant_agent/sync/app/task_queue.py
§3

總覽表

編號 嚴重度 白話 要先有什麼 急不急
F1 中 開一條空連線就能讓 8443 停止服務 能連到 8443 這個埠(客戶內網通常都能) 建議排進下一輪修;只能讓服務停擺,不能拿資料
§4

每條詳述

F1:開一條 TCP 連線不完成 TLS 交握,就能凍住整個 8443

  • 白話:TLS(加密連線)在正式傳資料前,雙方要先「握手」交換憑證。現在 8443 用的是 werkzeug(Flask 內建的小型伺服器),它在唯一一條負責接新連線的執行緒裡做這次握手,而且沒有設逾時。只要有人連上來卻不握手,這條執行緒就一直等,後面所有連線都排在門外進不來。
  • 出事會怎樣:主系統送來的請求一律收不到,包括取證據檔、派工(/tasks)、重啟(/restart)、升級(/update)、設定通知(/config/notify)、換證(/credentials/renew)、檢測端點。這段期間主系統會以為 Agent 失聯。資料不會外洩,也改不了任何東西。
  • 要先有什麼才打得到:
    • Agent 已註冊、AGENT_AUTH_MODE=full(正式出貨的預設),也就是 8443 有在聽。
    • 攻擊者能對 8443 開 TCP 連線。這個埠綁在 0.0.0.0,客戶內網任何一台機器通常都連得到。不需要憑證,也不需要帳密。
    • mTLS(雙向憑證驗證,主系統也要出示憑證)在這裡擋不住,因為驗對方憑證的動作本身就發生在那次被卡住的握手裡。
  • 在哪裡:guidant_agent/sync/api/data_plane_server.py:129-131,start_data_plane_tls 呼叫 make_server(host="0.0.0.0", ..., threaded=True, ssl_context=context)。threaded=True 只代表「握手完成之後」才分給新的執行緒,握手本身仍在接連線的那一條。
  • 人工核對:開檔確認過。整支程式沒有呼叫 settimeout/setdefaulttimeout,build_server_ssl_context(同檔 70-90 行)也沒有設任何逾時。三位檢查員都看了 werkzeug 與 Python ssl 的原始碼,得到同一個結論。這項結論是讀程式碼判斷的,沒有實際打過。
  • 怎麼修(另開卡):把握手移出「接連線」那條執行緒。做法一:接進來的 socket 用 do_handshake_on_connect=False 包,改到各連線自己的執行緒裡、在逾時之內做握手。做法二:換成每條連線各自握手、並設有逾時的正式伺服器(例如 cheroot/waitress)。另外再加每條連線的讀取逾時,免得握手後慢慢送請求一樣能霸佔執行緒。
§5

「重點看什麼」逐條人工查證

  1. data_plane_auth.py:46-47 _skips_jwt、loopback 判定:不成立(目前)。不必驗證的路徑只有 /health、/agent-info 兩條,而且必須完全相同(/health/x 仍要驗)。/local-ops/* 要來源是 loopback(本機 127.0.0.1)才跳過驗證。我搜過全 repo,沒有 ProxyFix,也沒有讀 X-Forwarded-For,所以 remote_addr 是 TCP 真實來源,外部從 8443 連進來不會是 127.0.0.1。風險提醒:容器部署時,如果 8000 埠被 docker 轉發,或前面放了在本機跑的反向代理,來源會變成 loopback 或 docker 閘道位址。依檔頭說明,/local-ops 另有「只有 root 讀得到的權杖」這第二道門;這部分在範圍外,沒有細看。
  2. data_plane_auth.py:79-92 before_request 掛載、mode 不是 full:守門成立;mode=none 是刻意設計。守門用 app.before_request 掛在整個 app 上,所以之後才註冊的 blueprint(Flask 的路由群組)一樣會經過,沒有前綴清單會漏掉。5 支指令 blueprint 由 register_command_blueprints 掛在同一個 app 上。mode 不是 full 時完全不驗(第 90-91 行),也就是 demo 模式下 8443 等於全開。不過 8443 只有在憑證齊全、已註冊時才會起來;正式出貨要確保不會用 none 跑。
  3. agent_auth.py:221-238 verify_data_plane_jwt:成立,沒有漏洞。JWT(主系統簽發的短效通行證)會驗 RS256 簽章、iss、aud(必須是本機 agent_uid)、bound_fp(必須等於本機指紋),並且用 require 強制 exp、iss、aud、op、jti 都要存在,沒有 exp 的 token 不會通過。重放(同一張票打兩次)由 TicketReplayGuard.consume 擋(data_plane_auth.py:71-75)。op 必須等於「方法 路徑」,簽給某支端點的票不能拿去打另一支。重放紀錄只存在記憶體,重啟後就清空;但 token 只活 60 秒,可以接受。
  4. 五支 cmd_*.py:不成立。
    • cmd_credentials.py:body 刻意是空的,只是叫 Agent 自己在背景換憑證(私鑰在本機產生),不收工具帳密,所以沒有「改完存哪」的問題。
    • cmd_config.py:body 只寫「哪幾類設定變了」,Agent 收到後再自己去主系統拉設定(config_pull),通知本身改不了任何 key。能不能透過拉回來的設定改到 allow_cidr 這類信任邊界,屬於拉取端,不在本棒範圍。
    • cmd_restart.py:每次都要一張新的、綁 POST /restart 的 JWT,而且重複請求與開機沒多久就請求都會被拒(too_soon 回 409)。只有主系統能觸發,不構成 DoS(阻斷服務攻擊)。
    • cmd_tasks.py、cmd_update.py:見第 6 條;/update 的版號、sha、大小形狀由 update_sync.trigger 驗。
  5. cloud_trust.py:不成立(從 8443 改不掉信任錨)。對主系統的 https 信任錨來自安裝時的 CLOUD_CA_FILE(TOFU,首次使用時確認憑證,概念同 SSH 第一次連線),只讀 env,8443 沒有任何端點能改它。另外非 https 的主系統位址一律拒絕。JWT 驗章公鑰的更新(apply_trust_anchors)只會經由心跳設定通道套用,而且先驗形狀再落檔;8443 的 /config/notify 只能讓 Agent 去拉,拉的對象仍是那台經 TLS 釘住的主系統,所以做不到「自己換自己的老闆」。前提是主系統本身沒被打穿。
  6. task_queue.py:部分成立(低風險,未列入發現)。cmd_tasks._parse 只驗 task_uid(1~64 字)和「payload 是物件」,payload 裡的內容(目標主機、帳密)進佇列前沒有逐欄驗 schema,原封交給 scan 的 dispatch_tasks。但送得進來的一定是持有主系統簽章 JWT 的人,而目標還會在 scan 端再比對授權清冊(allowed_targets,範圍外)。佇列只存在記憶體(queue.Queue),沒有落地檔,所以沒有檔案權限的問題。帳密不會進 log(只記 uid)。
  7. data_plane_server.py TLS 設定:成立,設定正確。build_server_ssl_context(70-90 行)的最低版本是 TLS 1.2,verify_mode = CERT_REQUIRED(要求主系統出示內部 CA 簽的客戶端憑證,也就是 mTLS),憑證三件套來自註冊流程。唯一的問題就是 F1 的握手逾時。
§6

可信度兩層

  • 「這些存在嗎」:F1 由三位獨立檢查員(分別看「打不打得到」「影響多大」「有沒有防線」)一致確認(3/3),我也自己開檔核對過,可信度高。全程是讀程式碼判斷,沒有執行程式、也沒有實際打過。
  • 「只有這些嗎」:本輪是 low effort,由一位研究員讀完整個範圍,另外再跑一輪找寫死在程式裡的密碼/金鑰,兩位都有回報,加上上面七條人工查證。範圍很小(11 檔),覆蓋算完整,但掃描每次結果會有些不同,不能保證沒有其他問題。範圍外的 config_pull、allowed_targets、local_ops、update_sync 沒有看。
§7

執行概況(照 stamp 抄)

  • Run ID:wf_67cc4f3e-bd2;原 run 目錄 CLAUDE-SECURITY-20261105-155939,已歸檔到 docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-155939/
  • 掃描版本:3813abd05232(工作目錄乾淨),effort low,focus attack-surface
  • verification.status: verified;reason_kind: 無
  • candidates: 1;panel_votes: 3(三位檢查員對 1 條各投 1 票,3 票全投出、全數成立)
  • researchers_dispatched: 2;researchers_returned: 2
  • 面板完整跑完;耗時約 21 分鐘