⚠️ 歷史紀錄:本報告描述的問題已於 2026-11-07 全部修正並合回 Agent 主線(修正卡 CM-3559~3564、3573~3576、3584,驗收紀錄見母卡 CM-3507)。文中的檔名與行號是掃描當下(commit
bab2aa3)的版本,修正後已變動,請勿拿來定位現行程式。
工具沒找到問題;我自己照「重點看什麼」逐條核對,找到 1 個值得開修正卡的小問題(嚴重度:低~中):私鑰檔建立的那一瞬間權限是「大家都能讀」,之後才改成只有本人能讀,而且改權限失敗時會默默放過。其餘六點人工查過都不成立。
Agent(裝在客戶機器上的程式)第一次裝好時怎麼向主系統報到、拿到自己的憑證與通行證;憑證快到期時怎麼自己換新;這些金鑰檔怎麼存在客戶機器上。這段是 Agent 身分的根:私鑰或通行證被同機其他程式拿走,就能冒充這台 Agent 跟主系統講話。
範圍 6 個檔、1336 行:guidant_agent/sync/app/enroll.py、guidant_agent/sync/app/cert_renew.py、guidant_agent/sync/domain/registration.py、guidant_agent/sync/domain/agent_auth.py、guidant_agent/sync/adapters/cert_files.py、guidant_agent/platform/agent_identity.py。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 | 來源 |
|---|---|---|---|---|---|---|
| M1 | 私鑰與通行證檔是「先建檔、寫完、再改權限」,建檔當下用系統預設權限(通常大家可讀);改權限失敗時不報錯 | 同台機器上的其他帳號可能在那一瞬間(或改權限失敗時永久)讀到 Agent 私鑰與通行證,拿去冒充這台 Agent | 攻擊者已經在客戶機器上有一個普通帳號,且能讀到憑證目錄(目錄本身權限也沒鎖) | guidant_agent/sync/adapters/cert_files.py:106-115(write_secret);呼叫端 guidant_agent/sync/app/enroll.py:131,138;目錄建立 enroll.py:90 |
見下方詳述 | 人工核對(工具未報,未經三人投票確認) |
急不急:不急,排一般修正卡即可。打得到的前提是「客戶機器已經被別人登入」;正式安裝形態下憑證目錄多半在只有服務帳號能進的資料目錄裡,會再擋一層(本棒範圍外,未核對)。
agent.key+agent.crt 的人可以自己簽「持有證明」(DPoP,就是每個請求附上「我真的有這把私鑰」的簽章),再配上 agent.json 裡的通行證,就能以這台 Agent 的身分打主系統;拿到私鑰還能在換證時冒充「舊的那台」。enroll.py:90 用 os.makedirs(cfg.cert_dir, exist_ok=True) 建目錄,沒指定權限,跟著系統預設(通常 0755);guidant_agent/sync/adapters/cert_files.py:110:open(path, "wb") 建檔,沒指定權限guidant_agent/sync/adapters/cert_files.py:112-115:寫完才 os.chmod,OSError 直接 passguidant_agent/sync/app/enroll.py:131(私鑰)、:138(含通行證的 meta):呼叫上面那支guidant_agent/sync/app/enroll.py:90:憑證目錄建立沒指定權限enroll.py:93 → write_registration 直接寫正式目錄)不是「先寫暫存再改名」,中途斷電可能留下寫一半的檔。後果只是下次開機判定不完整而重新註冊(is_enrolled 要四個檔都在),或某檔是半截而啟動失敗;換證那條(cert_renew.py:185-195)已經走暫存目錄+swap,沒這個問題。cert_files.write_secret 裡改成「建檔當下就給定權限」:fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode) 寫進暫存檔、os.fsync、再 os.replace(tmp, path);mode=0o600 時若 os.chmod/os.open 權限設定失敗就 raise(或至少 logger.warning),不要默默 pass。enroll.py:90 改成 os.makedirs(cfg.cert_dir, mode=0o700, exist_ok=True),並對既有目錄補一次 os.chmod(cfg.cert_dir, 0o700)。install.sh 有沒有事先把目錄設成 0700,不在本棒範圍,沒查;如果安裝腳本已經鎖住目錄,實際風險降為「只剩改權限失敗那條」。| 卡上的疑點 | 人工結果 | 為什麼 |
|---|---|---|
enroll.py:110/:131-134 私鑰權限、目錄權限、是否先寫暫存再改名;write_secret 預設 mode |
成立(部分)→ M1 | write_secret 預設 mode=0o600(cert_files.py:106),私鑰與 meta 用預設、其他三個公開檔用 0644,最終權限是對的;但「建檔當下權限太寬」「改權限失敗不報」「目錄沒設權限」「首次註冊不是暫存再改名」四點成立,見 M1 |
registration.py:3 SAN 來自外部 san_host,能不能改成別台主機名 |
不成立(Agent 這側) | san_host 取自 cfg.base_url(enroll.py:109),是 Agent 自己的安裝設定,不是網路上別人給的值;能改它的人已經能改 Agent 設定檔。CSR 的名字(CN)固定是本機指紋(enroll.py:110 common_name=device_uuid)。主系統驗不驗 SAN 在主系統 repo,本棒沒查 |
cert_renew.py:182-186 換證帶什麼證明自己是舊的那台;失敗降級會不會讓舊憑證永遠有效;:244 狀態檔 0644 |
不成立 | 換證帶 agent_uid 加上「舊私鑰對新 CSR 的簽章」(cert_files.py:118-132 prior_key_proof);主系統回的 uid 不同就不換(:183-184)。失敗只是留用舊證,舊證到期日不變、照樣會過期,不會變永久有效。狀態檔只有「上次嘗試時間、成功/失敗、錯誤訊息前 200 字」(:242),錯誤訊息來自例外文字,程式裡的 RenewRejected 訊息都不含路徑與金鑰;唯一可能夾帶的是主系統回的錯誤訊息,無機密 |
agent_auth.py:206 不驗簽解 control_token 拿 tenant_id,拿去做什麼 |
不成立 | 呼叫端只有兩處:guidant_agent/console/api/__init__.py:135、guidant_agent/console/api/local_auth_routes.py:98,放進本機主控台 session 的 tenant_id 欄。通行證來自本機 0600 的 agent.json(不是網路輸入),Agent 是單租戶,系統身分固定 AGENT_TENANT_ID = 1(agent_identity.py:21),寫 DB 的 tenant_id 走的是那個常數、不是這裡的值。能改 agent.json 的人早已能拿走通行證本身 |
agent_auth.py:141-148 _decode:沒 kid 時用整組試?JWT_ALGS 能否降級成 none/HS |
不成立 | 卡上的前提與程式不同:拿到信任錨清單後,kid 沒有或查不到一律拒絕(:144-147,key_for(None) 回 None),不會整組試。演算法:註冊鑰那條只收 ["RS256"](:37、:143);清單那條演算法由公鑰型別決定(_alg_for,:72-78,只有 RS256/EdDSA),不看票頭寫什麼,所以 none 與「拿公鑰當 HMAC 密碼」那招都進不來 |
agent_identity.py 的 device_uuid 怎麼算、會不會漂移、能否被環境變數覆寫 |
**本棒查不到(範圍外) | agent_identity.py 只處理「單租戶系統身分」(每請求、每背景執行緒裝 tenant_id=1),不算 device_uuid**。device_uuid 在 guidant_agent/platform/host_identity.py+fingerprint.py(見 .claude/rules/security/sync-and-identity.md),不在本棒六檔內,建議下一棒補掃。agent_identity.py 本身人工看過:身分只在行程內組,不讀任何標頭或 token,無問題 |
enroll.py:214 cloud_tls_verify(cfg) 哪些設定會回 False |
不成立 | guidant_agent/sync/app/cloud_trust.py:49-66:只會回 True(系統信任的憑證機構)或自訂信任檔的路徑;檔案設了但不存在就丟錯擋下,沒有任何回 False(不檢查對方是不是真的主系統)的路徑,而且每次都先過 ensure_secure_endpoint 要求 https |
| 項目 | 值 |
|---|---|
| Workflow run ID | wf_8d84cad0-16c |
| 掃描版本 | bab2aa31f00d3bf9b2e34c2a6dab88d1885b227f |
| effort | low(一個研究員掃全範圍+三人投票) |
| 範圍檔數 | 6(1336 行) |
| 研究員 派出/回來 | 1/1 |
| 候選發現(candidates) | 0 |
| 投票數(panel_votes) | 0(沒有候選,所以沒有票可投) |
| verification.status | verified |
| reason_kind | null |
| 研究員耗時 | 約 15 分鐘(907 秒)、約 9.6 萬 token |
| 歸檔 | docs/security-scan/FR-177/runs/CLAUDE-SECURITY-20261105-152137/ |