本卡屬 FR-077(母卡 CM-PARENT),第 1 棒:agent 身分與註冊(jedi monorepo 的
jedi-remote-agent)。只掃不修。
我們發一支程式(agent)給客戶裝在他自己的機房裡。這支程式開機後要先跟雲端「報到」——證明「我是合法的 agent、我屬於哪個客戶」,之後才能定期撥回來問有沒有工作。這一棒掃的就是報到與身分證明這一段:拿什麼東西報到、雲端怎麼驗、驗過之後發什麼給它、它下次怎麼證明還是同一台。
用 claude-security plugin 掃 42 個檔並產出報告。只找問題、不修問題。
身分是另外兩棒的前提——不先確定「誰能證明自己是 agent」,談「agent 能拿到什麼」沒有意義。所以這棒先跑,它的發現會影響 R2/R3 的判讀。
範圍取法是按業務模組垂直切:從 route 一路到 repository 的完整路徑都在同一棒(api/ 的路由表 → app/service/ 三支管理服務 → domain/remote_agent/ → infra/remote_agent/),加上 common/agent_auth/ 的五支密碼學原語與 plugin.py 的注入契約。不按 DDD 分層水平切——權限漏洞活在層與層的接縫上,只給 route 或只給 service 兩邊都看不出來。
出問題會怎樣:能假冒 agent 註冊,就能進到某個租戶的 agent 池;能接手別台的身分,就能收走本該給它的派工(含派工 payload 內的解密參數與憑證)。
這些是首腦讀過全部程式碼後認為最容易出事的地方。是思考起點不是檢查清單——工具不會照這個走,寫在這裡是給你(runner)掃完後自己追的方向。FR-076 的 L2 那條 HIGH 就是 runner 照卡片重點人工追出來的,工具當時只找到 LOW。
app/service/agent_enroll_token_service.py)。generate() 產 secrets.token_urlsafe(32),per-tenant 一組共用、DB 存 sha256、明文只回一次。沒有 expires_at、沒有使用次數上限、沒有綁定機器。這一組 token 會進 installer、發給客戶、裝在該租戶每一台 agent 上。要問:拿到它的人能做什麼?revoke() 停用後已註冊的 agent 會怎樣?get_current() 回傳的摘要有沒有洩漏可用來猜測的資訊?app/service/agent_enrollment_service.py 的 register())。它跑在無 user context 的 @transaction(session_scope → is_super_admin='t',繞 RLS),tenant 完全由 token 決定:get_enabled_by_hash(token_hash) → token_entity.tenant_id。要追:token hash 比對是不是常數時間?get_enabled_by_hash 在 super admin scope 下查的是全庫還是有別的限制?token_entity.tenant_id is None 那個守門為什麼需要(什麼情況會是 None)?_resolve_agent() + _guard_fingerprint_collision())。這裡的 docstring 極長且處處自我辯護,要特別不信任註解。關鍵設計:agent 帶 agent_uid 且屬本 tenant → 視為同一台;沒帶就用 (tenant, device_fingerprint) 找。撞號守門對已離線的持有列刻意放行(理由:客戶重灌後 uid 丟失、指紋不變,硬擋會永遠註冊不上)。離線門檻是心跳間隔 ×3(預設 900 秒)。要問:拿到 enroll token + 知道目標指紋 + 讓目標離線(或等它離線),是不是就能接手它那一列?接手後能拿到什麼?註解說 2026-08-21 曾實測抓到一個判定寫錯(只比 resolved.uid 而非 matched_by_uid)——現在這版對不對,自己驗,不要因為它說修好了就跳過。heartbeat())。先用 agent_uid 查,查不到就 get_one_remote_agent(device_fingerprint=device_uuid) ——這一查沒有 tenant_id,而它跑在 super admin scope(繞 RLS)。對照同檔 register() 的同款查詢是有帶 tenant_id 的。指紋在 FR-066 D11 之後只剩兩源(product_uuid|machine-id),VM 模板複製整批機器會撞號。跨租戶撞號時會發生什麼?另外 heartbeat() 也接受 agent_uid 直接查(get_one_remote_agent(uid=agent_uid))——這一查同樣沒有 tenant 檢查,而 register 那邊是有的(found.tenant_id == tenant_id)。common/agent_auth/ca.py 的 sign_csr())。它驗 csr.is_signature_valid 之後,直接沿用 CSR 自己帶的 subject 與 public key 簽出憑證,SAN 從 agent 自報的 base_url 推導。憑證同時帶 SERVER_AUTH 與 CLIENT_AUTH 兩個用途、有效期預設 3650 天。要問:CSR 的 subject 完全由 agent 決定會怎樣(能不能簽出一張 subject 看起來像雲端自己的憑證)?san_from_base_url() 拿 agent 自報的 base_url 推 SAN,能不能簽出綁定任意主機名的憑證?CLIENT_AUTH 用途讓這張憑證能拿去對誰做 mTLS?沒有 CRL、沒有 OCSP,撤銷只靠 DB 的 status=revoked ——那對已簽出的憑證有沒有效?common/agent_auth/jwt_util.py)。mint() 簽 RS256,claim 有 iss/aud/exp/op/tenant_id/bound_fp,TTL 預設 60 秒。這支只簽不驗——驗在 agent 端。要問:op 這個 claim(例如 "GET /blob/xxx/sha256")是誰帶進來的、有沒有注入面?aud=agent_uid 由 caller 傳,有沒有路徑能讓攻擊者影響它?公鑰在 register 時發給 agent(load_jwt_public_key_pem),私鑰路徑來自 settings——路徑可控會怎樣?common/agent_auth/settings.py + plugin.py)。AgentAuthSettings.mode 預設 "none"(資料面不加認證),全部路徑欄位預設空字串。plugin.py 的 register() 若 consumer 沒給 settings_provider 就凍結 config.auth(=全預設=mode none)。register() 裡「有 CSR 但沒設 CA」才報錯,沒 CSR 就靜默回註冊成功且憑證欄位是空字串。要追:什麼樣的接線疏漏會讓整條鏈靜默跑在 none 模式?plugin.py 的 _guard() 用動態子類套 decorator,只套 get/post/put/delete/patch 五個 method——有沒有其他 HTTP method 能繞過?ROUTE_TABLE 第三欄是唯一的認證意圖宣告,比對一次每條路徑的第三欄對不對。app/service/remote_agent_service.py)。get_agent(uid) / update_agent / revoke_agent / delete_agent 都只用 uid 查、沒有任何顯式 tenant 檢查,隔離完全靠 RLS。這些跑在有 user context 的 @transaction(RLS 生效),理論上沒問題——但要驗:verify_remote_agent_is_exist() 走的 get_by_uid() 真的受 RLS 管嗎?rebind_agent() 讓管理員手動覆寫任意指紋,加上④的指紋 fallback,組合起來能做什麼?check_health() + reconcile()/_agent_sha256_get())。base_url 是 agent 自報的,雲端拿它去發 HTTP 請求——典型 SSRF 面。check_health 在 mode=none 時是裸 httpx.get(f"{url}/health"),url 來自 request body(管理端點,但仍是使用者輸入)。要問:能不能拿它探測內網?reconcile 的 save_file_name 直接串進路徑 /blob/{save_file_name}/sha256,有沒有路徑穿越?