FR-187 G 掃描報告:容器非 root 與 compose 設定

§1

🔴 一句話結論

沒有急件。 找到 3 個問題,都是低:

  1. 登入頁不用登入就能讀到升級進度檔,裡面有管理員帳號名稱;
  2. Redis 健康檢查把全權帳號的密碼寫在指令列上;
  3. Agent 金鑰目錄本來要掛成唯讀,但因為同一個目錄又被掛了一次,三顆後端容器其實都改得動。

三條都經三個獨立檢查員投票,結果都是 3 比 0 成立,我也自己開檔核對過。九顆容器的非 root、拿掉特權(cap_drop)、對外埠這些主要防線,人工逐項查過都到位。

§2

這一棒在檢查什麼

出貨給客戶的 compose 檔(docker/production/docker-compose.yml,就是「客戶機器上每顆容器用什麼身分跑、掛哪些目錄、開哪些埠、拿到哪些密碼」的清單),加上一次性建庫容器的 docker/init/Dockerfile 和 docker/init/entrypoint.sh。範圍共 3 檔、約 1046 行。為了查證,有翻範圍外的前端 nginx.onprem.conf 與 scripts/installer/install.sh,這兩個檔只為查證而讀。

§3

掃到什麼:總覽表

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡 怎麼修 嚴重度
F1 網站上有一個升級進度檔,任何人不用登入就能讀,升級跑完後也不會刪 外人知道「哪個管理員帳號在做升級」和目前的確切版本,可以拿去猜密碼、挑已知漏洞 ①這台跑過至少一次升級或升級前檢查 ②連得到網站 443 埠 docker/production/docker-compose.yml:570(對外的那段在 FE repo nginx.onprem.conf:228-237) 升級結束就清掉這個檔,或只在升級進行中才提供;公開版本不要放帳號名稱和錯誤訊息 低
F2 Redis 健康檢查每 5 秒跑一次 redis-cli --pass <密碼>,密碼出現在行程的指令列 主機上任何一個普通帳號都看得到這組全權密碼,可以讀或刪 Redis 裡所有資料,包括簡訊/信箱一次性驗證碼 ①在客戶的 Docker 主機上有一個普通(非 root)帳號 ②主機沒開「不准看別人行程」的設定(hidepid) docker/production/docker-compose.yml:421 改用環境變數 REDISCLI_AUTH 傳密碼,或另開一個只能 PING 的健康檢查帳號 低
F3 Agent 的 CA(簽憑證用的根憑證)和 JWT 簽章金鑰,一處掛唯讀,同一個主機目錄卻另一處掛成可寫 只要攻破任何一顆後端容器(含處理客戶上傳檔的 worker),就能換掉 CA 和金鑰;換掉的東西升級後還在,主機下次升級時還會拿它簽內部憑證 ①已經在 api/socketio/worker 其中一顆容器裡拿到執行程式的能力 ②照預設方式安裝 docker/production/docker-compose.yml:221(與 :243 的唯讀掛載指向同一目錄;scripts/installer/install.sh:1410 設定該目錄) 把防竄改標記與 setup-token 移到獨立子目錄,只把它掛成可寫;pki/agent 改歸 root、唯讀,而且只掛進 api 低

為什麼都是低: F1 外洩的只有帳號名稱和版本,沒有密碼。F2 要先有主機帳號。F3 要先攻破容器。三條都需要先過一道關卡,過關後能多拿到的東西也有限。

§4

每條發現的詳述

F1 — 升級進度檔公開可讀、不會清掉

白話: 升級時,主機會把進度寫進 /srv/guidant-ai/upgrade/upgrade-progress.json。為了讓後端停機時維護頁也能顯示進度,前門 nginx 把這個檔直接公開在 /upgrade-progress.json,不檢查登入。檔裡有 triggered_by.user(按下升級的管理員登入帳號)、升級前後的版本號,以及失敗時的錯誤訊息。升級結束只會改狀態欄,檔案本身一直留著。nginx 註解寫「沒在升級時回 404」,但沒有任何程式真的做到這點。

在哪裡: docker/production/docker-compose.yml:570 把 upgrade 目錄掛進 guidant-fe;compliance-manager-fe/nginx.onprem.conf:228-237 的 location = /upgrade-progress.json 沒有任何登入檢查。

首腦核對註記(小弟已開檔): 兩處都確認存在。nginx 那段只加了安全標頭,沒有 auth_request、allow/deny。

F2 — Redis 密碼出現在指令列

白話: Linux 上每個行程的指令列(ps 看得到的那一串),預設所有本機使用者都看得到。健康檢查把全權帳號 GUIDANT_REDIS_USER 的密碼直接當參數傳給 redis-cli。這個帳號有 ~* &* +@all,也就是對所有資料、所有頻道都有全部權限,等於繞過三個後端行程各自的權限切分(SR-06.6)。另外 Redis 沒有要求連線端出示憑證(--tls-auth-clients no),所以拿到密碼就能從主機直接連 Redis 容器的內部網路位址。

在哪裡: docker/production/docker-compose.yml:421;帳號權限定義在 :398。

怎麼修: 指令改成 REDISCLI_AUTH="$$REDIS_PASSWORD" redis-cli --tls --cacert /tls/ca.crt --user "$$REDIS_USER" ping。

首腦核對註記: 第 421 行原文確認。--no-auth-warning 只是不印警告,不會把密碼藏起來。

F3 — Agent 金鑰目錄被重複掛載成可寫

白話: 共用設定段把主機的 pki 目錄整個可寫掛進 /opt/guidant/pki(第 221 行,原本用途是放防竄改標記)。安裝程式又把 AGENT_CERT_DIR 設成同一個 pki 目錄(install.sh:1410),第 243 行再把它唯讀掛一次。同一份檔案從可寫的那條路徑照樣改得動,所以唯讀等於沒設。金鑰歸 uid 1000,三顆後端容器也都用 uid 1000 跑。socketio 和 worker 根本用不到這些金鑰,卻也拿得到。

在哪裡: docker/production/docker-compose.yml:221、:243;scripts/installer/install.sh:1410、:1364。

首腦核對註記: install.sh:1410 AGENT_CERT_DIR=${GUIDANT_DATA_DIR}/pki 確認。前提是攻擊者已經在容器裡,所以這條是「被攻破之後能藏得更久」的問題,不是入口。

§5

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

卡上要看的點 結果 說明
每個服務的 user: 不是 0、cap_drop: [ALL]、no-new-privileges 大致成立 db 用 999:999、redis 用 999:1000、seaweedfs 用 1000:1000,都有明寫。api/socketio/worker 靠 image 的 USER 1000(:197 註解)。fe 靠 image 的 uid 101,init 靠 Dockerfile 的 USER postgres。九顆都有 cap_drop 和 no-new-privileges。有一處不確定:bugsink(:711)和 sandbox(:675)沒寫 user:,執行身分完全看第三方/分類器 image 的預設。這一棒沒有開 image 驗證,建議修正卡順手補 user: 寫死。
有沒有掛 docker.sock、主機 /proc、/sys 不成立(沒掛) 全檔 grep 沒有這三種掛載。/proc/1/cmdline 只是 worker 健康檢查讀自己容器內的檔。
/run/guidant 掛進哪幾顆、env_file 給了誰 安全 /run/guidant/guidant.env 是主機路徑,由 compose 讀取後只透過 env_file 交給 api/socketio/worker 三顆(:159-160),沒有被當成目錄掛進任何容器。bugsink、fe、sandbox、db、redis 都沒有 env_file;sandbox 在 :685 明寫不給。
ports: 只有 fe 對外 成立 唯一的 ports: 在 fe(:540-547,80→8080、443→8443)。db、redis、api 都只有 expose(內部網路可見)。:450 的臨時開 8000 只是註解,沒有生效。
tmpfs 的 uid/gid 與權限 不成立(沒問題) /run:uid=1000,gid=1000,mode=755(:208)。tmpfs 是每顆容器各自一份記憶體檔案系統,別顆容器看不到。sandbox 的 /home/classifier:mode=1777 也只在自己容器內。
init Dockerfile 有沒有把私鑰、.env、測試金鑰帶進 image 不成立(沒帶) COPY 只有 scripts/init/、scripts/sql/、migrate_encryption_keys.py 和 entrypoint 四項。docker/init/Dockerfile.dockerignore 用白名單排掉其他目錄。版控裡 scripts/init、scripts/sql 沒有金鑰類檔案(唯一命中 drop-users-topt-secret.sql 是刪欄位的 migration)。殘餘風險:build 機工作目錄裡如果有沒進版控的檔,也會被 COPY;build 機走 git pull,風險低。
entrypoint 各 MODE、MIGRATE_KEYS_DRY_RUN 能不能拿來跳過檢查 不成立 MIGRATE_KEYS_DRY_RUN 有值時只會加上 --dry-run(只看不寫),是往安全的方向偏。MIGRATE_APPLY 預設 0(不寫入)。MODE 無效時 exit 4。每個模式開頭都檢查必要密碼。seed-ai-keys 失敗時仍回 0,這是刻意設計(AI 金鑰可事後補),不算跳過檢查。
§6

這份結果可信到什麼程度

  • 「這些問題存在嗎?」可信。 三條都經三個獨立檢查員(分別從「打不打得到」「影響多大」「有沒有防線擋住」三個角度)各投一票,9 票全數投出,全部 3 比 0 成立;小弟也逐條開檔對過行號。
  • 「只有這些嗎?」不能保證。 effort low 只派一個研究員讀完 3 檔,沒有多人交叉。人工逐條查證補了卡上列的重點,但 bugsink/sandbox 的 image 預設身分沒有開 image 驗證。掃描每次跑的結果不完全一樣,定期重掃能慢慢補齊。
§7

執行概況(照 stamp 抄)

項目 值
run ID wf_0ce346ae-ca0
掃描版本 058a84c02(branch ws-CM-3711-CM-3724)
範圍/effort 3 檔/low
verification.status verified
reason_kind 無
candidates(候選發現數) 3
panel_votes(檢查員投票數) 9(3 位 × 3 條,沒有漏投)
researchers_dispatched/returned 1/1
耗時 約 43 分鐘,10 個 agent,0 個失敗
原始產物 docs/features/security-scan-consolidated/runs/be/CLAUDE-SECURITY-20261108-203944/