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

## 🔴 一句話結論

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

## 這一棒在檢查什麼

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

## 掃到什麼：總覽表

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

## 每條發現詳述

### 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 幫未核准者簽證」，本條補上「主系統對外全面信任這張證」），建議併卡處理。

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

| 卡片要我看的 | 人工結果 |
|---|---|
| 舊鑰從命令列參數還是環境變數帶入；`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）。 |

## 這份結果可信到什麼程度

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

## 執行概況

| 項目 | 值 |
|---|---|
| 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 分鐘 |
