本卡屬 FR-077(母卡 CM-1591),修 R1(CM-1592)的 F6,並承接卡片「重點看什麼」①(enroll token 生命週期)。面板未跑成,runner 開檔核對+首腦複核屬實。MEDIUM(需持有租戶 enroll token)。與 CM-A 卡動同一支
agent_enrollment_service.py——序列做,A 先。
客戶裝 agent 時,安裝程式裡帶一組「報到通行碼」(enroll token)。它是整個租戶共用一組、永不過期、無次數上限、不綁機器——業界(Elastic enrollment token、Kubernetes bootstrap token)的通行碼都是短效或一次性的,我們的等於永久通行證。
更糟的是註冊時可以宣告「我是編號 X 那台」:程式只檢查「X 屬於這個租戶」就允許覆寫 X 那一列——包括 base_url。於是拿到通行碼+知道另一台的編號(管理頁看得到),就能把那台的網址改成攻擊者主機;之後雲端向那台取資料(reconcile、取檢測報告)會打到攻擊者那裡,簽給那台的短效 JWT 也送過去。
Runner 核對到一個細節值得記:_guard_fingerprint_collision() 的 docstring 記載 2026-08-21 修過一個判定錯誤,那次修正是對的,但它修的是「VM 複製品憑指紋冒充」;本條走的是被豁免的那條路徑(matched_by_uid=True),豁免條件只有「說得出 uid」+「同租戶」,沒有持有證明。註解沒錯,只是回答的不是這個問題。
首腦核對:
app/service/agent_enrollment_service.py:94-99 _resolve_agent():found.tenant_id == tenant_id 即回 (found, True),無持有證明。:167-172 resolved_uid = ... if matched_by_uid else None,holder.uid == resolved_uid → continue。:216-228 接著以請求值覆寫 base_url/device_fingerprint/status/enabled。app/service/agent_enroll_token_service.py:generate():secrets.token_urlsafe(32),DB 只存 sha256;remote_agent_enroll_tokens 表無 expires_at/use_count/max_uses 欄(migrations/001 grep 過)。~/Projects/Jedicogy/module/jedi-python-package/jedi-remote-agent/jedi_remote_agent/app/service/agent_enrollment_service.py:94-99 _resolve_agent():憑 uid+同租戶即 matched_by_uid=True
~/Projects/Jedicogy/module/jedi-python-package/jedi-remote-agent/jedi_remote_agent/app/service/agent_enrollment_service.py:186-272 register():驗 token → resolve → 覆寫
~/Projects/Jedicogy/module/jedi-python-package/jedi-remote-agent/jedi_remote_agent/app/service/agent_enroll_token_service.py generate()/revoke()/get_current()
~/Projects/Jedicogy/module/jedi-python-package/jedi-remote-agent/jedi_remote_agent/domain/remote_agent/service/remote_agent_enroll_token_domain_service.py get_enabled_by_hash()
~/Projects/Jedicogy/module/jedi-python-package/jedi-remote-agent/jedi_remote_agent/infra/remote_agent/model/remote_agent_enroll_token.py 要加欄位
~/Projects/Jedicogy/module/jedi-python-package/jedi-remote-agent/jedi_remote_agent/migrations/ 隨包 migration 加一支
~/Projects/Billows/Audit-Manager/compliance-manager-be/scripts/sql/ 主專案對應 migration(只套 DEV)
remote_agent_enroll_tokens 加 expires_at(nullable)、max_uses(nullable)、use_count(default 0)。generate(curr_user, label, ttl_hours=None, max_uses=None)——先回寫本卡問決策者定預設值:業界 24 小時~7 天,首腦建議預設 7 天、無次數上限(客戶批次裝機要時間),管理頁可調。get_enabled_by_hash() 改成同時檢查 enabled AND (expires_at IS NULL OR expires_at > now()) AND (max_uses IS NULL OR use_count < max_uses);register() 成功後 use_count += 1。先查再寫:時間源用 common/agent_auth/heartbeat.py 的 utc_now()(CM-1317 定的單一時間源,last_seen_at 同款 UTC naive),不要 datetime.now()。register() 若 payload 帶 agent_uid,必須同時帶 X-Agent-Signature(CM-A 卡做的簽章機制,用該 uid 對應的 client_public_key_pem 驗);驗過才走 matched_by_uid=True 的更新路徑;驗不過或沒帶 → 當作沒帶 uid,走指紋 fallback + 撞號守門的新建流程(讓既有守門正常判定)。這樣「客戶重灌後 uid 丟失、指紋不變」的合法場景仍走原本設計的「舊列已離線 → 放行接手」路徑,不受影響。get_current() 摘要:目前回 label/created_at,加回 expires_at/use_count,讓管理員看得到通行碼狀態。不回明文(維持)。use_count == max_uses → 401;有效 → 200 且 use_count +1。