FR-187.C 掃描報告:金鑰登記簿、Agent 授權照開通、註冊碼管理

§1

一句話結論

找到 2 個低風險問題,都在「Agent 授權照線上開通」那個不需登入的入口:一是任何拿著一把註冊碼的人,可以讓整台主系統所有客戶的線上開通暫時全部被 License Center 鎖住;二是開通碼會以明文寫進 api 日誌。金鑰登記簿本身(換鑰匙、撤鑰匙、私鑰保管)這次沒找到問題。

§2

這棒檢查什麼

主系統 9 個檔、約 822 行:

  • 金鑰登記簿(主系統拿來簽 Agent 通行證的鑰匙清單,可以新增、切換現行、撤銷):core/plugins/_agent_keys.py、app/remote_agent/service/trust_anchor_service.py、api/remote_agent/routes/trust_anchor_route.py、domain/remote_agent/trust_anchor_domain_service.py、infra/remote_agent/trust_anchor_repo_impl.py、core/trust_anchor_bootstrap.py
  • Agent 授權照線上開通(Agent 帶註冊碼來,主系統代它去 License Center 開通):app/remote_agent/service/agent_license_activation_service.py、api/remote_agent/routes/agent_license_route.py
  • 註冊碼的產生與撤銷:api/remote_agent/routes/agent_enroll_token_route.py
§3

總覽表

編號 問題(白話) 嚴重度 急不急 在哪裡
C-1 有註冊碼的人亂打開通,會讓全部客戶的線上開通被鎖 低 不急,出貨前排進修正 agent_license_activation_service.py:95
C-2 開通碼明文寫進 api 日誌與 app.log 低 不急,改遮罩規則一行 common/middleware/app_mw.py:257
§4

每條詳述

C-1 有註冊碼的人亂打開通,會讓全部客戶的線上開通被鎖

  • 白話:POST /agents/license/activate 不需要登入,只要帶一把有效的註冊碼(產給安裝人員裝 Agent 用的那串碼)就能打。主系統收到後直接幫忙轉給 License Center(LC,原廠的授權簽發站),自己不做任何頻率限制。LC 是用「來源 IP」計算失敗次數、15 分鐘錯 5 次就鎖。所有客戶的開通請求都經同一台主系統出去,在 LC 眼裡都是同一個 IP。
  • 出事會怎樣:有人每 15 分鐘丟 5 個亂碼,這台主系統所有租戶(客戶)的 Agent 線上開通、以及主系統自己的線上授權開通,都會一直回「已鎖定」。只影響「能不能開通」,不會洩資料,也沒辦法靠這條暴力猜出開通碼(因為一樣被鎖)。
  • 要先有什麼才打得到:任一把沒撤銷、沒過期的註冊碼。註冊碼預設沒有到期日,而且這裡刻意不看剩餘次數,所以「已經用完的舊碼」也算數。拿得到的人包括有建碼權限的子公司管理員、裝機人員、或能讀 Agent 主機上設定的人。
  • 在哪裡:app/remote_agent/service/agent_license_activation_service.py:71-90(驗碼不看次數)、:95(直接轉打 LC);入口 api/remote_agent/routes/agent_license_route.py:31-38。nginx 前門的通用限流是每 IP 每分鐘 30 次,擋不住 5 次。
  • 怎麼修:主系統這端先擋失敗次數(以註冊碼雜湊+來源 IP 為單位,存 Redis,可沿用 common/util/redis_rate_limiter.py);另請 LC 改成依主系統轉帶的租戶或註冊碼識別鎖定,而不是依 IP;可考慮要求註冊碼還有剩餘次數或已綁定一台註冊過的 Agent 才轉打。
  • 面板:三位檢查員 3 票全數成立,嚴重度維持「低」。

C-2 開通碼明文寫進 api 日誌與 app.log

  • 白話:主系統有一層全域攔截,每個請求的內容都會先「遮掉密碼類欄位」再寫進 api 日誌(api_logs 表,管理頁可查,保留 90 天)和 log/app.log。遮罩規則只認 password、secret、token 等字眼,所以 enroll_token 會被遮,但 activation_code(開通碼,買授權拿到的那串一次性序號)原樣寫進去。
  • 出事會怎樣:如果開通沒成功(LC 連不上、回 503、被鎖),開通碼還沒被用掉,卻已經躺在日誌裡。有權看 api 日誌的人可以把它抄走,拿去開通自己的機器,搶走原本要給別台的授權。
  • 要先有什麼才打得到:①那次開通沒成功、碼沒被用掉;②能讀 api 日誌(platform.api-log.read 能力點)或主機上的 app.log。
  • 在哪裡:common/middleware/app_mw.py:243-244(寫 app.log)、:257(寫 api_logs.request);遮罩規則在套件 jedi_common/utils/common_utils.py:139-142。這個檔在本棒範圍外,是因為範圍內的開通端點會經過它才列入。
  • 怎麼修:在共用遮罩規則(jedi-masking/jedi_common)加 activation_code,最好加一條通用的「*_code 類憑證欄位」規則,api 日誌、app.log、錯誤追蹤三邊一起生效。主系統自己的 /license/activate 也有同樣的缺口,可以一起修。
  • 面板:三位檢查員 3 票全數成立,嚴重度維持「低」。
§5

被面板否決的一條

  • 登記簿每把鑰匙可設 not_after(到期日),但簽票、驗票都只看狀態、不看到期日。三位檢查員一致判定不成立(0/3):到期日只是管理參考欄位,鑰匙要不要用由管理員切換狀態決定,沒有造成可利用的後果。
§6

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

  1. guarded=True 那四支有沒有真的套到守門:成立(守門有效)。core/plugins/_agent_keys.py:192-195 四支標 True;core/plugins/remote_agent.py:292 把 key_routes(guarded=True) 併進 _host_admin_routes(),:422-423 逐支以 _admin_guarded() 包起來(逐 method 套 admin_required,能力點來自 KEY_ADMIN_CAPABILITIES)。guarded=False 四支在 :424-425 原樣掛,而它們在 trust_anchor_route.py 每支都有 @jwt_required()+@require_platform_admin_route。八支都有守門。
  2. 能不能撤掉最後一把在用的鑰匙/private_key_ref 擋不擋 ../:不成立。trust_anchor_service.py:114-115 狀態是 active 的鑰匙直接拒絕撤銷,要先切換到別把才能撤,所以不會出現某用途「沒有現行鑰匙」。讀私鑰走 private_key_path()(:143-150),先 resolve() 再檢查是否仍在保管目錄底下,../ 與符號連結跳出都會被拒。
  3. _write_private_key 的保管目錄會不會流出去:不成立。目錄是 AGENT_JWT_PRIVATE_KEY 所在的資料夾,安裝版在 ${GUIDANT_DATA_DIR}/pki/agent/trust-anchors/(scripts/installer/install.sh:1716,1756),檔案 0600、目錄 0700、O_EXCL 不覆蓋。guidantai diag 診斷包只收容器狀態、設定檔(金鑰類欄位遮成 ***)與 log、升級紀錄,不收 pki/ 目錄。備份是否包含 pki/ 屬客戶主機的備份策略,不是程式缺口。
  4. 開機補登用哪把公鑰、會不會覆蓋:不成立。公鑰讀 AGENT_JWT_PUBLIC_KEY 指的檔(trust_anchor_service.py:131),三個用途用同一把,靠 kid 前綴區分。只在登記簿整張表是空的時候才補(:125),每列走 add_if_absent,不會覆蓋既有的列。
  5. 開通端點有沒有頻率限制、會不會一人亂打全部客戶被鎖:成立,就是 C-1。主系統這端沒有限流(redis_rate_limiter 沒套到這支),只有 nginx 前門每 IP 每分鐘 30 次的通用限流。
  6. LC 回的 license 原樣回、log 有沒有記開通碼與指紋全文:服務本身不成立。_call_lc 的 log 只記指紋雜湊前 12 碼、開通碼雜湊前 8 碼與狀態碼(:93,101,106),不記回應內容。但全域攔截會把請求內容(含開通碼明文)寫進日誌,這就是 C-2。回應那一面:成功時的授權照會經 after_request 寫進 api_logs.response(app_mw.py:307),授權照綁機器指紋、Agent 端會驗章,流出風險低,這次沒有列為問題。
  7. 產生註冊碼的能力點、明文碼有沒有進 log:不成立。建碼要 detection.remote-agent.create、撤碼要 detection.remote-agent.delete、列表要 detection.remote-agent.read(_agent_keys.py:156-162),經 _admin_guarded 套上。明文只出現在建碼的回應裡,回應的鍵名是 token,api_logs.response 寫入前會被遮罩規則遮掉;route 自己的 log 只記前門憑證路徑,不記碼。
§7

可信度(兩層)

  • 這兩條存在嗎:存在。三位檢查員各自從「打不打得到」「後果多大」「有沒有別的防線」三個角度投票,兩條都是 3/3。我也開檔逐行核對過上面列的行號。全部是讀程式碼推論,沒有實際打過端點。
  • 只有這兩條嗎:不能這樣保證。effort low 只有一位研究員讀全部範圍,另有一輪掃寫死在程式裡的密碼/金鑰,是一次快速掃描,不是逐檔窮舉。C-1 涉及 LC 的鎖定邏輯在另一個 repo(license_center),研究員只讀了程式碼,沒有實測。
§8

執行概況(照 stamp 抄)

  • run ID:wf_0d7a8ea3-eb7;掃描版本 eee99445e72a(branch ws-CM-3711-CM-3716,工作目錄乾淨);effort low,只看正式程式(attack-surface)
  • verification.status:verified;reason_kind:null
  • candidates:3(去重後 3);panel_votes:9(三位檢查員對 3 條各投一票,全部投出)
  • researchers_dispatched:2;researchers_returned:2
  • 結果:2 條成立(皆低),1 條否決
  • 歸檔:docs/features/security-scan-consolidated/runs/be/CLAUDE-SECURITY-20261108-180426/