Files
Arcrun/docs/incidents/2026-05-29-encryption-key-drift.md
T
uncle6me-web 922a57fe34 arcrun — AI workflow execution engine (clean history)
Self-hosted 開源:WASM 零件 + recipe + cypher-executor,跑在你自己的 Cloudflare。

此為重建的乾淨歷史起點(移除曾誤 commit 的 GCP SA 金鑰,舊歷史保留在
richblack/arcrun 與本地 backup 分支)。含:
- acr init --self-hosted installer(建 KV/R2 + codeload 拉預編譯 wasm + wrangler deploy + seed recipe)
- recipe push 把關(資料外流提醒 + 打通檢查)
- 19 個正當零件預編譯 wasm(claude_api/km_writer/kbdb_upsert_block 排除:違反 DECISIONS §1)
- CLI / cypher-executor / registry / 完整 SDD

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 15:52:38 +08:00

4.7 KiB
Raw Blame History

2026-05-29 credential 解密失敗(兩個 Worker 的 ENCRYPTION_KEY 漂移)

症狀acr recipe test kbdbcredential 注入)回 HTTP 500auth_static_keycredential kbdb_api_key 解密失敗 根因(主)arcrun-auth-static-key Worker 的 ENCRYPTION_KEY secret 跟正本(cypher-executor / CLI 用的那把)值不同、格式也不同(44-char base64 vs 64-char hex)。AES-GCM 用錯 key 必然解密失敗。 根因(附)component-loader.tsres.json().catch(() => res.text()) 讀 response body → body 被讀兩次 → Body has already been used修法(1) wrangler secret put ENCRYPTION_KEY 把 auth-static-key 對齊正本 64-hex(2) 新增 readBodyOnce() 先取 text 再 parse JSON。 影響BACKLOG 步驟 2credential 注入鏈路)阻擋;Phase 3 降級假零件成 recipe 的前置。


症狀

acr recipe test kbdb 端到端打不到 2xx。直接 probe auth_static_key

POST https://auth-static-key.arcrun.dev/  {action:"authenticate", api_key:"ak_…", service:"kbdb"}
→ {"success":false, "error":"credential kbdb_api_key 解密失敗", ...}

前置都綠(排除誤判方向):

  • auth_recipe:kbdb 存在、primitive=static_keykv_get 命中 410 bytes
  • kbdb_api_key credential 存在 KVkv_get 命中 108 bytes 的 {encrypted, iv}
  • 失敗精準落在「解密」這一步

定位(key-fingerprint 診斷,只印 SHA-256 前綴,不印 key/明文)

aesGcmDecryptwasi-shim.ts)暫加:

console.error(`[decrypt] ENCRYPTION_KEY sha256_prefix=${fpHex} keyLen=${len}`)

deploy auth-static-key + wrangler tail 抓到:

來源 keyLen sha256 前綴 格式
加密端(CLI ~/.arcrun/config.yamlencryption_key 64 fa84f2ce9027 hex(→32 bytes)✓
解密端(arcrun-auth-static-keyENCRYPTION_KEY secret 44 ff219b123c89 base64 ✗

兩個 mismatch 同時存在:值不同 + 格式不同。hexToUint8Array 套在 44-char base64 上會解成垃圾 bytesAES-GCM 必失敗。

漂移源頭:arcrun/.env 裡的 ENCRYPTION_KEY 就是那把錯的 base64ff219b123c89),有人拿它去 wrangler secret put 設進 auth-static-key。

為什麼正本是 64-hex

/registerregister.ts:42)把 encryption_key: c.env.ENCRYPTION_KEY 原樣回給用戶 —— 即 cypher-executor 的 ENCRYPTION_KEY。用戶 config 是 64-hexfa84f2ce9027),所以正本 = cypher-executor 那把 64-hex。CLI 加密 credential 也用這把。auth-static-key 必須跟它一致才能解開。

診斷用完即移除(wasi-shim.ts 還原,git diff 為空)。

附帶 bugBody has already been used

修對 key 後,/execute 端到端從 500 變成「Node n1 failed: Body has already been used」。

component-loader.tsmakeRecipeRunner / makeAuthRecipeRunner

const data = await res.json().catch(() => res.text());  // ✗ res.json() 失敗時 body 已消費

KBDB /health 回非 JSON(純文字)→ res.json() throw → .catch(() => res.text()) 第二次讀 body → throw。

修法 — 讀一次:

async function readBodyOnce(res: Response): Promise<unknown> {
  const text = await res.text();
  try { return JSON.parse(text); } catch { return text; }
}

修法步驟

  1. cd .component-builds/auth_static_key && wrangler secret put ENCRYPTION_KEY,貼正本 64-hex= ~/.arcrun/config.yamlencryption_key)。richblack 手動rule 05runtime secret 不進 CI、CC 不碰)。
  2. component-loader.tsreadBodyOnce(),兩處 res.json().catch(...) 換掉。tsc --noEmit 綠,deploy cypher-executor。
  3. 修正源頭文件 arcrun/.envENCRYPTION_KEY 改成 64-hex(避免下次再設錯)。

驗證證據

  • 直接 probe auth-static-keyHTTP 200, success:true, 產出 Authorization: Bearer …
  • 端到端 /executeHTTP 200, trace 乾淨
  • auth 確證:直接 curl KBDB /blocks 不帶 token → 401 {"error":"Missing token"};經 cypher-executor(注入 token)→ 過 auth,進 KBDB handler 回 ZodError(缺 content)。無 401 = token 被接受

教訓

  • 同一把 key 出現在 ≥2 個 Worker 的 secret = 漂移風險。auth-static-key / auth_service_account / cypher-executor 都讀 ENCRYPTION_KEY,靠人各設一次必漂。長期應有單一發放來源或部署時自動同步。
  • debug 加密問題,先比 key 指紋(SHA-256 前綴),不要碰 key 明文。一個 fingerprint log 就分辨出「值錯」vs「格式錯」vs「資料壞」。
  • res.json().catch(() => res.text()) 是反模式 —— body 只能讀一次。永遠先 res.text()JSON.parse