FR-187.F2 掃描報告:加密金鑰遷移與容器啟動讀設定

§1

🔴 一句話結論

找到 1 個中風險問題:主系統容器啟動時,把「發給 Agent 用的那把憑證簽發章」加進了所有對外連線的信任清單;拿得到 Agent 註冊碼的人可以替自己蓋一張「我是 Anthropic/客戶郵件伺服器」的假證,搭配網路中間人位置就能偷走 AI 金鑰與郵件密碼。換鑰匙腳本本身沒掃到問題。

§2

這一棒在檢查什麼

三把舊加密鑰匙(Drive、檢測工具、AI)併成一把 APP_ENCRYPTION_KEY:升級時由 migrate_encryption_keys.py 以超級管理員身分把全部客戶的密文換新鑰重鎖;另外看整合憑證加密怎麼讀這把鑰匙、設定載入器怎麼退回舊鑰,以及正式容器啟動腳本做了什麼。4 檔、約 753 行。

§3

掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 怎麼修 嚴重度
F1 主系統對外連線「信任誰發的證」的清單裡,混進了一把任何 Agent 都能請它蓋章的章 AI 金鑰、郵件密碼、物件儲存密鑰、送去 AI 的證據內容可被偷看與竄改 ① 一組 Agent 註冊碼(租戶管理員就能產)② 能攔截主系統主機對外網路(區網 DNS/ARP 偽冒、被控制的閘道) docker/production/entrypoint.sh:48-51 不要把 Agent CA 放進整個程式共用的 SSL_CERT_FILE;只給需要的物件儲存客戶端與 Redis 中(要同時滿足兩個前提)
§4

每條發現詳述

F1 主系統把 Agent 的簽發章當成全網通用的信任根

白話:電腦連 HTTPS 網站時,會檢查對方出示的「身分證」是不是可信的發證單位蓋的章。我們的容器啟動時,為了讓主系統認得內部的 Redis 與檔案儲存(SeaweedFS),把內部憑證章(CA,就是「發證單位」)加進信任清單。問題是這把章就是 Agent CA——Agent 註冊時,它會依 Agent 自己報的網址替它簽一張伺服器身分證,而且還沒被核准的 Agent 也簽(這是已知的 #249)。兩件事加起來:攻擊者自稱網址是 https://api.anthropic.com,就拿到一張主系統會相信的「我是 Anthropic」身分證。

出事會怎樣:攻擊者站在主系統對外連線的中間(網路中間人),出示這張假證,主系統的 AI 呼叫(litellm)、更新站與授權站呼叫(httpx)、外部 S3(minio)、郵件寄送(SMTP STARTTLS)全部照單全收。AI 金鑰(Authorization 標頭)、郵件密碼、物件儲存密鑰、送去 AI 的證據內容都會外流。

要先有什麼才打得到:

  • 安裝時預設啟用 Agent CA(預設就是)
  • 任一租戶的 Agent 註冊碼(租戶管理員可自產,或裝 Agent 時發出去的碼外流)
  • 能攔截或導向主系統容器的對外連線——這是最大門檻,所以是「中」不是「高」

在哪裡:

  • docker/production/entrypoint.sh:44-51(合併信任庫、export SSL_CERT_FILE)
  • scripts/installer/internal_tls.sh:15(把 pki/agent/ca.crt 複製成內部 CA——首腦核對:開檔確認,確實是同一把)
  • 簽發端在 jedi-remote-agent 的 issue_credentials/san_from_base_url(已知 #249)

怎麼修:

  1. entrypoint.sh 不再 export SSL_CERT_FILE;改成只把內部 CA 交給需要的客戶端:內建 SeaweedFS 的 minio 客戶端自帶 http_client(urllib3 PoolManager(ca_certs=/etc/guidant/internal-ca.crt)),Redis 用既有的 REDIS_SSL_CA_CERTS。
  2. 或者 Redis/SeaweedFS 改用一把只簽內部服務、從不簽 Agent CSR的獨立 CA;或替 CA 加名稱限制(nameConstraints,限定只能簽內部服務名)。
  3. 根源一併收:#249 修好(未核准 Agent 不簽 serverAuth、SAN 不吃 Agent 自報網址),這條的前提②就打不通。

首腦核對註記:中風險,非 HIGH,依卡片規則不強制逐條核;仍已開 internal_tls.sh:15 與 entrypoint.sh:48-51 確認信任鏈成立。與 §3.1 #249 是同一根源的新後果(#249 原本只講「CA 幫未核准者簽證」,本條補上「主系統對外全面信任這張證」),建議併卡處理。

§5

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

卡片要我看的 人工結果
舊鑰從命令列參數還是環境變數帶入;docker/init/entrypoint.sh:134 怎麼呼叫 不成立(沒問題)。腳本支援 --old-*-key 參數(ps 看得到),但 init 容器呼叫時只帶 --dry-run(docker/init/entrypoint.sh:134),鑰匙全走環境變數(installer 以 -e 變數名 不帶值帶入)。命令列參數只剩手動跑時的選項,腳本檔頭已註明建議走環境變數。
_scrub(:200)遮 psql 錯誤回顯是否完整 大致成立、殘留低。只遮 gAAAAA… 形狀的密文;錯誤回顯裡的明文欄位值不會出現(UPDATE 寫的都是密文),舊值也是密文。另截斷 500 字。新鑰匙本身從不進 SQL。沒發現外洩路徑。
build_apply_sql:隨機 tag dollar quote、SET LOCAL app.is_super_admin='t' 不成立(沒問題)。tag 每次隨機且會檢查不在內容裡;row_id 經 int();表名欄名來自寫死的 TARGETS。SET LOCAL 只在 --single-transaction 這一個交易有效(:212);讀取那段用的是 SET(:225)但每次 psql 是獨立連線,結束即消失。腳本只在 init 容器(cmmgr 身分)或手動執行,主系統服務不會呼叫。
rekey_token:新鑰解得開當「已是新的」;can_apply 遇 bad 整批不套還是跳過 不成立(行為正確)。只要有一列三把舊鑰都解不開,can_apply=False,整批一列都不寫、回 exit 2(:301-305)——預設擋下來,不會半套。另外讀與寫之間有人改資料時,WHERE 欄=舊值 對不上會整批 RAISE 回滾(:194)。
APP_ENCRYPTION_KEY 沒設時「警告後明文落庫」,正式安裝會不會走到而沒人發現 成立但屬設計取捨、低風險。config_secret_crypto.py:65-69 沒鑰就原樣存明文,core/app_factory.py:397 只記一行 warning。正式安裝 install.sh:1305 一律產生這把鑰並寫進 env 檔,升級也會沿用(:1229、:2444),正常路徑走不到。只有有人手動刪掉 env 檔那行才會發生,且只有 log 一行警告、畫面無提示。建議(非本棒範圍):非 DEV 環境缺鑰時拒絕啟動。
config_loader.py 新增 36 行:讀 /run/guidant/guidant.env,不存在時退回哪裡 卡片前提與現況不符。本版 config/config_loader.py 沒有讀 /run/guidant/guidant.env 的程式碼(全檔 159 行,新增的是 resolve_encryption_key:新鑰優先、沒設才退回各用途自己的舊鑰)。所以不存在「退回磁碟舊明碼檔」這條路。舊鑰退回只回鑰匙字串,不讀任何檔。
docker/production/entrypoint.sh:非 root 後還做什麼;合併 CA 的來源能不能被可寫掛載塞假 CA 成立,即 F1。非 root 後只做 chdir、合併 CA、exec binary,沒有 chown/降權。CA 來源 /etc/guidant/internal-ca.crt 是 installer 從 Agent CA 複製來的唯讀掛載,一般使用者塞不進假 CA;真正的問題是這把 CA 本身會替 Agent 簽任意網址的證(見 F1)。
§6

這份結果可信到什麼程度

  • 「這些存在嗎」:F1 經三個獨立檢查員(分別從「打不打得到」「影響多大」「有沒有防線擋」三個角度)投票,3 票全數成立;首腦另開檔確認信任鏈。可信。
  • 「只有這些嗎」:effort low=單一研究員讀完 4 檔(另一名專掃寫死的密碼,無發現)。兩名研究員都有回報、無人中途失敗。但 low 是快篩,不是窮舉;人工逐條看過卡片列的七個重點,未再發現其他問題。不宣稱「只有這一條」。
  • 所有結論都來自讀程式,沒有實際執行或打攻擊。
§7

執行概況

項目 值
run ID wf_e3e13c9e-e7c
報告目錄 CLAUDE-SECURITY-20261108-191411(已歸檔至 docs/features/security-scan-consolidated/runs/be/)
revision 1c9be8d84fb96afcaf2af91af2c4380f9cb6f8ec(工作樹乾淨)
範圍 4 檔,effort low,focus attack-surface
verification.status verified
reason_kind 無
candidates 1
panel_votes 3(三個檢查員對 1 條各投一票,3 票全投出)
researchers_dispatched / returned 2 / 2
耗時 約 67 分鐘