iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
Vibe Coding

轉生到全端工程師沒多久就要負責公司的大平台??系列 第 10

第十天|容錯不對稱:fail-open vs fail-closed

  • 分享至 

  • xImage
  •  

昨天我們看著一個請求進門:先貼上 correlation id a1b2c3d4,再驗 cookie、還原出「他是誰」,把身分放進請求上下文一路帶下去。今天要追問的是那一步的反面——萬一驗身分這件事當下做不到呢?身分中心連不上、回應逾時,或回了一個你看不懂的東西,這一刻 Portal 該放他進來,還是把他擋在門外?這個問題沒有「都對」的答案,只有「依場景選一邊」。而 Portal 在不同的門口,刻意選了相反的兩邊。

本篇結構:

  • 故障當下,沒有「安全的拖延」這個選項
  • 同一座閘道,兩扇門選相反的方向
  • 兩個容易做錯的細節:別吞太多、別比太久
  • 取捨:這不是「哪個對」,是「在這扇門前你怕什麼」

故障當下,沒有「安全的拖延」這個選項

先把問題的形狀講清楚。驗身分不是每一步都是純本地運算——驗章本身是純運算(Day 8 講過,重算一次 HMAC 跟簽章比對就好,不需要查表),這一步不會「故障」;真正會出事的,是它周邊那些環節:跟身分中心換取資訊、查某個會逾時的服務,或者 cookie 解出來的內容對不上預期格式(雖非連外,但同樣讓「確認身分」當場失敗)。這些都可能在某個瞬間失敗。

關鍵在於,這不是一個可以「等一下再說」的問題。請求就在眼前,使用者正盯著畫面等回覆,你必須現在就決定:要嘛當他通過、放他往下走,要嘛當他沒通過、回絕他。沒有第三條路。系統設計裡把這個岔路命名得很直白:

方向 出錯時往哪倒 行為 代價
fail-open 往「開」 放行 可能放進一個其實沒驗證過的人
fail-closed 往「關」 拒絕 可能擋掉一個其實合法的人

兩個都有代價,差別只在「你寧可承受哪一種代價」。而這個答案,跟「這道門背後是什麼操作」綁死。

同一座閘道,兩扇門選相反的方向

Portal 的做法是:不替整個系統選一邊,而是逐門選。一座閘道上有不只一道關卡,每一道按它守的東西和影響的量級,各自決定倒向哪邊。最典型的就是這兩扇門。

第一扇門是行員提問這條主線。小林輸入:

我的員編是 123456,AD 帳號被鎖了,順便想查內湖分行的營業時間。

這是一個查詢,最壞的後果是查到一筆本來就對全體行員公開的分行營業時間。這條路徑採 fail-open:身分若驗不出來,不擋,而是放行、把他標記成匿名往下傳。理由很實際——身分中心是會偶爾抖一下的外部依賴,你不該讓「身分中心打了個噴嚏」就拖垮整個 AI 助理,讓全公司問不了問題。可用性在這裡的權重,高於「絕對確認他是誰」。

第二扇門是後台的危險操作,例如熱重載供應商設定、停用某一家 LLM(這套熱控制 Day 15 會專門講)。這類操作會改變系統行為、影響的是所有人。它採 fail-closed:沒有正確的管理員憑證,一律拒絕,連「可能是合法管理員只是驗證服務剛好掛了」這種情況也照樣擋。理由同樣實際——這種操作做錯一次的代價,遠大於「合法管理員多等十分鐘等服務恢復」。安全在這裡的權重,反過來壓過可用性。

把這兩扇門並排攤開,不對稱就一目了然:

維度 行員提問主線 後台危險操作
典型動作 查詢、提問 熱重載供應商設定、停用某家 LLM
影響面 單次查詢,最壞查到公開資訊 改變系統行為,影響所有人
容錯方向 fail-open:放行並標匿名 fail-closed:未持有效憑證一律拒絕
權重取捨 可用性 > 絕對確認身分 安全 > 可用性
故障時回應 HTTP 200 + 匿名旗標,對話繼續 HTTP 503,明確告知「這扇門不開」

用設定把這個不對稱攤開來看,會更直觀:

portal:
  identity:
    # 行員提問主線:身分中心故障時放行、標匿名
    on-failure: fail-open      # fail-open | fail-closed

  admin:
    # 後台危險操作:未持有效管理員權杖一律拒絕
    on-failure: fail-closed
    # 連權杖都沒設定時,預設直接回 503,而不是「沒設=不檢查=放行」
    secret: ""                 # 留空 → 啟動後該端點一律 503

注意 admin.secret 留空那一段的潛台詞:fail-closed 不只管「驗證失敗」,連「根本沒設定好驗證」都算失敗,照樣關門。這是 fail-closed 心態的精髓——任何模稜兩可,都往安全那邊倒。換成 fail-open 的設定哲學,「沒設=不檢查」很容易被寫成「那就放行吧」,這在後台這種門是災難。一寬一嚴不是隨興,是跟著「這扇門背後操作的風險量級」走的。

故障時的回應也呼應這個差異:主線提問就算遇到狀況,傾向回 HTTP 200 加一個匿名旗標,讓對話繼續;後台危險操作驗不過,直接回 HTTP 503,明確告訴你「這扇門現在不開」。

兩個容易做錯的細節:別吞太多、別比太久

光是「選對方向」還不夠。fail-open 真正危險的地方,不在「放行」這個決定本身,而在實作時手一滑,把不該放的也一起放了。這裡有兩個必須講清楚的細節。

細節一:錯誤處理只能包住「驗證」那一步

fail-open 的寫法直覺上是「try 驗證、catch 到就當匿名往下走」,但這個 try 的範圍是會悄悄擴張的——如果你貪圖省事,把驗證、業務邏輯、回應組裝全包進同一個 try-catch,那麼後面任何一個真正的業務錯誤(模型呼叫炸了、檢索鏈出問題),都會被這個「fail-open 當匿名」的 catch 給接住、默默吞掉,使用者拿到一個看起來正常、其實內部早就出錯的回應。這是把「對身分故障寬容」誤用成「對所有故障都寬容」,性質完全不同。

正確的寫法是把那層保護收到極窄——只裹著驗章那一個動作,catch 到就降級成匿名身分,然後立刻跳出這層保護,後面的業務流程該炸就讓它炸、該回錯就回錯:

// ✗ try 太大:把業務例外也一起吞了,錯誤被偽裝成「匿名成功」
try {
    identity = verifyIdentity(cookie)
    answer   = runChatPipeline(identity)   // ← 這裡的失敗不該被當成「身分故障」
    return ok(answer)
} catch (e) {
    return ok(runChatPipeline(ANONYMOUS))  // 全部錯誤都被當匿名重跑,真 bug 被藏住
}

// ✓ try 只包驗證這一步:身分故障 → 降級匿名;業務例外 → 照常往上拋
identity = try {
    verifyIdentity(cookie)
} catch (e) {
    log.warn("identity verify failed, fallback anonymous req=a1b2c3d4")
    ANONYMOUS
}
answer = runChatPipeline(identity)   // 這行若失敗,是真錯誤,讓它正常浮上來
return ok(answer)

差別就在那個 try 的邊界畫在哪。包得剛好,fail-open 是一個受控的降級;包得太寬,它就變成一台把所有故障都消音的機器——這比沒做容錯還糟,因為它連讓你發現問題的機會都偷走了。

細節二:管理員權杖的比對要用常數時間

第二個細節在 fail-closed 那扇門:管理員權杖的比對要用常數時間(constant-time),這招 Day 8 驗章時提過。一般字串比對是「逐字元比,遇到第一個不一樣就馬上回 false」——這意味著比對花的時間,會洩漏「你猜對了前幾個字元」。攻擊者拿這個時間差,可以一個字元一個字元地把權杖猜出來,這類手法叫計時攻擊(timing attack)。常數時間比對的做法是不管對不對,每次都把整段比完才回結果,讓回應快慢跟「猜中幾個字」徹底脫鉤。後台這扇門守的是改變系統行為的權限,值得連這種側通道(side channel)都堵上。

取捨:這不是「哪個對」,是「在這扇門前你怕什麼」

把這篇收束成可帶走的結論:fail-openfail-closed 沒有優劣,只有適配。判準永遠是同一個問題——這扇門背後的操作,做錯一次的代價有多大?

  • 代價小、且高度依賴外部服務的可用性(行員提問),就 fail-open,別讓上游打噴嚏拖垮全場。
  • 代價大、影響面廣(後台改設定),就 fail-closed,寧可錯擋也不錯放。

同一座 Portal 上兩扇門選相反方向,看似矛盾,其實是同一個原則的兩種應用。

也要誠實講邊界。fail-open 標匿名放行,意味著下游每一站都得有「如果這人是匿名,該怎麼辦」的明確行為——比如匿名就只給公開資訊、不碰任何跟個人相關的功能。它把「擋」的責任從門口推遲到了下游,這條鏈得整條都接得住才行,不是門口放了就沒事。這也是為什麼這套不對稱必須搭配 Day 9 的 correlation id:當你選擇 fail-open 放一個匿名請求進來,事後要能循著 req=a1b2c3d4 把「這筆是在身分故障下放行的」追查回來,容錯才不會變成黑箱。

還有一個容易混淆、值得單獨切開的點:「無法確認身分」其實有兩種,不該一律當成同一種故障。一種是可信的身分服務暫時聯絡不上或逾時——這是真的「當下沒辦法驗」,降級成匿名是合理的,前提是下游只給公開能力。另一種是 cookie 格式壞了、簽章對不上、宣告內容不合法:這其實是「確定驗不過」,不是「當下沒辦法驗」,而且它是攻擊者可以操弄的輸入,別把它敘述成一般的服務故障,更別因此推導出「驗章失敗也一律 fail-open」——在匿名並不安全的門前,那會是個漏洞。本篇主線的提問路徑因為匿名頂多拿到公開資訊,這兩種剛好收斂到同一個安全結果;但把這套 fail-open 搬去別的場景之前,先把這兩種分清楚。

放行不是不管,是「放行+留痕+下游自負其責」三件事一起成立。

明天 Day 11,我們把視角從「行員怎麼進 Portal 的門」轉到「Portal 怎麼進下游 Engine 的門」——下游憑什麼相信來敲門的真的是 Portal?答案不是在程式裡塞長期金鑰,而是服務間(service-to-service)認證:s2s OIDC(OpenID Connect)。


上一篇
Day 9|是誰在那裡??
下一篇
Day 11|服務間認證 s2s OIDC
系列文
轉生到全端工程師沒多久就要負責公司的大平台??18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言