大部分談 Webhook 可靠性的文章,都把遺失的事件當成資料問題。少了一列資料,你遲早會發現,然後回填。很煩,但可以復原,而且工程團隊以外的人大多看不到。
身分事件打破了這個框架,因為兩個方向並不對稱。
一個沒送到的 access.granted 事件,會造成一個進不去的使用者,而進不去的使用者幾分鐘內就會告訴你。它會自己回報。系統免費內建了一層由人組成的警示機制。
一個沒送到的 access.revoked 事件,會造成一個還進得去的使用者。沒有人會回報這件事。得到好處的人沒有理由提起,其他人則以為停權流程(offboarding)已經完成,因為你的身分驗證服務商說完成了。這個缺口會一直存在,直到稽核發現為止,而那可能是一年後。
重點摘要
- 漏掉授權,是一張客服工單。漏掉撤銷,是一扇沒裝警報器的敞開大門
- 身分驗證服務商的儀表板顯示撤銷成功,因為在他們那邊確實成功了
- Clerk 以及其他建立在 Svix 上的服務,會在大約 27.5 小時內嘗試 8 次,全部失敗持續 5 天後就停用端點
- 端點被停用本身就是一個無聲的狀態,而且是最要緊的那一個
- 把 Webhook 接收端的可用性當成存取控制,而不是基礎設施的例行保養
儀表板顯示不出來的失敗
管理員把某人移出組織。身分驗證服務商更新自己的紀錄、讓自己的 session 失效,然後把 organizationMembership.deleted、user.deleted 或 session.revoked 送到你的端點。
你的端點正在部署,回了 502。
從身分驗證服務商的主控台看,成員資格已經沒了。從管理員的角度看,停權已經完成。你的應用程式卻還留著一列資料,說這個人是那個組織的成員,而你根據最初的授權所發出的快取 session 或 API token,也都還有效。
在任何人會看的地方,都不會出現錯誤。服務商的 Webhook 紀錄裡有一筆失敗的傳遞,但除非已經起了疑心,沒有人會去讀 Webhook 紀錄。
重試替你爭取時間,然後就停了
Clerk 透過 Svix 發送 Webhook,不少其他身分驗證和基礎設施產品也是這樣。它的排程是公開的,值得記清楚,因為它精確定義了在 Svix 停止重試一個撤銷事件之前,你的端點可以壞多久。
一共八次嘗試:「立即、5 秒、5 分鐘、30 分鐘、2 小時、5 小時、10 小時、10 小時(每個間隔都接在前一次之後)」。
這大約涵蓋 27.5 小時,確實很大方,在我們的 Webhook 服務商參考資料裡,也是所有發送端中最長的之一。一段部署時間、一次事故、一個倒楣的週五:全都能從容吸收。第八次嘗試之後,Svix 會送出一個類型為 message.attempt.exhausted 的營運用 Webhook(operational webhook),但只有在你替營運用 Webhook 設定了去處時,它才有用。
接著是第二道門檻:「如果對某個端點的所有嘗試在 5 天內都失敗,該端點就會被停用,並且會有一個營運用 Webhook(EndpointDisabledEvent)送到你的帳號。」這個計時還有附帶條件:「只有在 24 小時內有多次傳遞失敗,而且第一次和最後一次失敗相隔至少 12 小時,才會開始計時。」
仔細看停用狀態。端點一旦被停用,新事件就不會再為它排入佇列。等你修好端點,它們也不會補送過來。這段期間的每一個撤銷事件都維持未送達,直到有人發現並重播;而唯一的通知,是一個送到某個帳號的營運用 Webhook,那個帳號卻沒有人設定過目的地。
這種不對稱是設計的前提
一旦接受這兩個方向的代價不同,一些平常的決定就會改變。
對移除存取權的事件,要失敗即拒絕(fail closed)。如果處理程式處理不了 session.revoked,正確的做法不是回 200 再記一筆警告,而是回 5xx,讓重試排程啟動,並確保你自己的警示會觸發。授權可以樂觀一點。撤銷不行。
不要只靠 Webhook。推送事件是降低延遲的手段,不是事實來源。定期對身分驗證服務商的 API 對帳,問「現在誰應該有存取權」,就能把永久漏掉的撤銷,變成下一輪掃描時會自我修復的問題。這和「Webhook vs 輪詢」裡的混合架構論點相同,而身分正是它不再是可選項的情況。每小時跑一次,最糟的情況就是一個小時的過時存取權,而不是永遠。
讓你自己的 session 會過期。如果你的應用程式根據身分事件自行簽發 session 或 token,短 TTL 加上會重新檢查成員資格的刷新機制,代表漏掉的撤銷會自己過期失效。長效 token 則會讓一次漏掉的 Webhook,變成無限期的存取權。
順序在這裡也很重要。一個在 member.removed 之後才重試送達的 member.added,會把你剛移除的人加回去,而且沒有任何事件被處理兩次。版本防護的做法,也就是只在更新的時間戳記比該列新時才套用,寫在「同一個 Webhook 你收到了兩次」。對存取控制來說,與其依賴時間戳記比較,不如直接拒絕不合法的狀態轉換。
要知道端點被停用時是什麼樣子。這是整個類別裡最有價值的一個警示。如果身分 Webhook 安靜的時間比平常的間隔還長,就是出事了,而「安靜」正是停用狀態的症狀。一個「上班時間 6 小時內沒有任何身分事件」的檢查就能抓到它。其他方法都抓不到。
稽核問題怎麼辦
如果你要處理 SOC 2、ISO 27001 或任何客戶的資安問卷,裡面會有一題問及時停權(deprovisioning)。如果你的撤銷路徑只有一個 Webhook、沒有對帳,誠實的答案是:停權會及時發生,除非它無聲無息地沒發生,而且事後你分辨不出來。
這很難寫進文件裡。但只要兩樣東西就能修好:一份持久的紀錄,記下每一個送達的身分事件;以及一個對帳工作,它的產出你可以拿出來佐證。紀錄回答「我們被告知了什麼、什麼時候」;掃描回答「我們有沒有照做」。為什麼可變的資料庫列對第一個問題是薄弱的答案,我們寫在「可偵測竄改的 Webhook 紀錄」。
AnyHook 的角色
結構性的問題在於,接收端的可用性已經成為存取控制的一部分,而建在應用程式基礎設施上的接收端,會繼承應用程式的可用性。
把身分驗證服務商指向 in.anyhook.net/you/identity,就能把兩者分開。AnyHook 在邊緣節點大約一秒就確認收到並持久儲存,所以你那邊的部署、冷啟動或資料庫中斷,根本不會造成傳遞失敗。接下來:
- 對你自己端點的重試排程是 5 分鐘 → 30 分鐘 → 3 小時 → 18 小時 → 之後每天一次。Scale 方案(大約 6 天)比 Svix 的 27.5 小時更久;Pro 方案的最後一次嘗試大約在 22 小時,Free 方案則在 35 分鐘,超過之後,要重播的就是已儲存的事件
- 每個撤銷事件在嘗試傳遞之前就已經儲存,而且儲存時已加密,所以「我們到底有沒有收到」這個問題,有一個不依賴應用程式 log 的答案
- 依時間範圍重播,讓一段出包的部署時間,變成一次重新傳遞,而不是人工稽核誰早該失去存取權
- 連續失敗 1、5、20 次時的失敗警示,會遠早於任何服務商的停用門檻
這些都不能取代對帳掃描,你還是應該跑一個。Webhook 中繼服務(webhook relay)會儲存它接受的每一個事件;只有掃描能抓到從來沒送出的東西。
結論
授權會自我修正,因為受影響的人會抱怨。撤銷不會,因為受影響的人不會抱怨。就是這一個不對稱,讓身分 Webhook 值得比其他整合更嚴格的可靠性標準,即使它們的 payload 看起來都一樣。
對移除採取失敗即拒絕,讓自己的 session 會過期,跑對帳掃描,並替意外的安靜設定警示。不這麼做,你得到的就是一個以沉默作為失敗模式的資安控制。