存取模式
每個 V2 部署都把五種存取模式全部編譯進去 — 切換模式不需要重新部署、也沒有 feature flag。部門 Worker 的第一次部署起始為 teamsync_internal(Durable Object 的播種列,revision 0):在部門管理者選擇其他模式之前,公開通道一律回 403。播種用 INSERT OR IGNORE,已存有政策的 Durable Object 在後續部署中保留原政策。以 builder 政策 department-worker-project-v2.5 或更新版本建置的 bundle 都播種 teamsync_internal,目前的 department-worker-project-v2.10 也一樣;以 v2.4 或更早政策建置的 bundle 播種的是 public,只有當這種修訂版剛好是部門的第一次部署時才有影響。簽章的 TeamSync 通道在任何模式下都可用。
下表是 Worker 層級政策。以 builder 政策 v2.8 或更新版本建置的 release 上,管理者還能為每個 manifest 操作個別設定五種模式之一,每個操作也能擁有自己的 API 金鑰 — 見逐操作存取。無論模式來自 Worker 層級政策還是某個操作,拒絕狀態碼都相同。
| 模式 | 公開 dispatcher 准入 | 拒絕狀態碼 |
|---|---|---|
public | 任何滿足有界 HTTP 契約的請求 | — |
api_key | 需要與儲存摘要吻合的 X-TeamSync-Worker-Key | 401 access_denied |
ip_allowlist | 用戶端 IP(cf-connecting-ip,由 Cloudflare 設定,地端則由 edge host 設定)必須落在管理者擁有的 IPv4/IPv6 CIDR | 403 access_denied |
api_key_and_ip | 邏輯 AND — 兩個檢查都要過;絕非 fallback/OR | 403 access_denied(是的 — 加了允許清單後,壞金鑰的狀態碼從 401 變成 403) |
teamsync_internal | 公開通道整條關閉;只剩簽章的後端通道 | 403 access_denied |
在哪裡強制執行
不在 dispatcher。公開通道上 dispatcher 只轉發證據 — 呼叫者的金鑰(若符合 ^tsw_[A-Za-z0-9_-]{43,124}$)轉成 x-teamsync-public-api-key,cf-connecting-ip 轉成 x-teamsync-client-ip。生成的 runtime 讀取自己 Durable Object 的呼叫狀態後決定:
- 以政策 v2.8 或更新版本建置的修訂版(後端 v5.13.0 起建置的每個修訂版):退役鎖定的 Worker 先回 403
worker_retired;接著 runtime 比對路由,再以比對到之操作的有效政策准入 — 除非操作設定覆寫,否則就是 Worker 層級政策。比對不到的路徑與不符的方法,會在回 404/405 之前先以 Worker 層級政策准入。正在服務之 release 的操作設定為pending時,公開通道改回 403operation_settings_pending。 - 較舊的修訂版保留建置時的 runtime:只有一份 Worker 層級政策,在選路之前准入。
- 金鑰檢查:以常數時間比較(
crypto.subtle.timingSafeEqual)拿提交金鑰的 SHA-256 對適用的儲存摘要(操作自己的金鑰或 Worker 層級金鑰)— 先做比較,摘要未設定時用全零的 32 位元組替身,所以未設定的金鑰永遠不會誤配,時間也洩漏不了任何資訊。 - IP 檢查:嚴格的手寫 IPv4/IPv6/CIDR 解析(不接受 zone id、不接受 IPv6 內嵌點分 IPv4),逐位元組的前綴包含判定。
模式↔允許清單的配對在每一層都驗證:ip_allowlist/api_key_and_ip 需要 ≥1 條 CIDR(最多 64;逐操作覆寫每個也最多 64、合計最多 256)、其他模式必須恰好 0 條 — 後端 schema 驗一次、Durable Object 再驗一次,退化的組合根本存不進去。
API 金鑰機制
- 輪替(
POST /worker/access-policy/api-key)生成tsw_+secrets.token_urlsafe(32)(47 字元)。明文只回傳一次;只儲存 SHA-256 摘要與後四碼,送到 Durable Object 的也只有摘要+後四碼。明文金鑰在結構上就進不了控制 outbox。 - Worker 層級輪替僅限舊版:上線 release 以政策 v2.8 或更新版本建置時,它回 409
{"code":"worker_wide_key_legacy_only","message":"Rotate an operation key instead"}。請改為輪替操作金鑰 — 格式相同、header 相同,明文同樣只顯示一次(管理逐操作存取)。 - 撤銷是冪等的:對未設定金鑰做撤銷回
revoked: false且不推進 revision(稽核為access_policy.api_key.revoke_noop)。撤銷 Worker 層級金鑰在每個 release 上都可用。 - 金鑰自己的前綴
tsw_就在提交憑證篩查清單裡 — 你甚至無法把長得像金鑰的字串放進 manifest example。
CAS 管理
所有政策讀寫都是 /worker/access-policy 上的部門管理者操作,且要求 Worker 已部署且 active(否則 409)。寫入帶 base_revision;Durable Object 做 compare-and-swap(revision == base),過期的 base 回 409 Access policy revision is stale。政策物件整體替換 — 永不合併。操作設定則依 release generation 各有獨立的 CAS revision(逐操作存取)。
信任但驗證:每次變更後,後端斷言 provider 回傳的 revision 恰為 base_revision + 1(撤銷 no-op:不變),且 payload 鍵集合恰為 {api_key_configured, api_key_last_four, ip_allowlist, mode, revision, updated_at} — 任何漂移都是 503,不是 409。呼叫期間 release 被換掉時,回帶代碼的 503 {"code":"worker_release_unavailable","message":"Worker release changed; retry this request"}。變更走耐久的控制面稽核 outbox,所以每次改動最終都成為回執背書的稽核事實,回應帶 Cache-Control: no-store。
其他地方的效果
- 生成的 OpenAPI 即時跟隨模式(每次請求
openapi.json都重新讀取):金鑰模式 → 安全機制TeamSyncWorkerApiKey(headerX-TeamSync-Worker-Key);teamsync_internal→TeamSyncBearer;public/ip_allowlist→ 無。servers[0].url在公開 dispatcher URL({origin}/public/{script_name})與後端 invoke base(teamsync_internal時)之間切換。根層級的x-teamsync-access-policy擴充欄位只露出mode加兩個布林 — 永遠不露 CIDR。操作設定為reconciled時,各操作還會帶自己的security、servers與x-teamsync-access-policy(外加source),components.securitySchemes也可能同時含兩種機制(逐操作存取)。 - 公開通道的 CORS 無條件是
access-control-allow-origin: *,且access-control-allow-credentials會被主動剝除 — 公開 Worker 永遠做不了帶憑證的 CORS。 - 公開通道不帶 release generation,所以公開呼叫者永遠打到當下上線的修訂版,也永遠不會看到
release_generation_mismatch。