我們的代理程式拒絕信任了三週的那個 CA
我們的一個代理程式,在每一條連線上拒絕了一張完全有效的上游憑證,整整三週。憑證沒問題。簽發它的那個 CA,被代理程式所在的主機所信任。代理程式就是看不見這件事—因為它在開機時讀過一次系統信任庫,之後世界就向前走了,而它沒跟上。
背景
當代理程式接合一條 TLS 連線時,它會在轉送任何東西之前,先驗證上游伺服器的憑證—憑證鏈必須能對著主機的系統 CA 庫通過驗證,否則代理程式就拒絕該連線,並回報 upstream_cert_invalid。那個拒絕是安全的方向:面對一個它無法擔保的上游,它選擇失敗關閉,而不是轉送它無法驗證的流量。這個設計本身沒有錯。錯在一個字:一次。
錯誤所在
信任庫只在啟動時載入了一次。在一個長時間運行的機群上,那些代理程式從五月底起就一直開著。簽發其上游憑證的那個 CA,是之後才被加進主機系統庫的—一個正常的佈建步驟,在代理程式已經運行時進行。沒有任何東西告訴它們信任庫變了。於是每個代理程式都對著它開機時拍下的那張快照在驗證,而那張快照早於那個 CA,於是它持續拒絕一張連主機自己—若被即時詢問—都會毫無異議接受的憑證。它在三週裡這麼做了數十萬次。
它為何藏住了
那個錯誤是完美的偽裝。upstream_cert_invalid 正是你面對一個真正不受信任的上游時會看到的—一個真實的設定錯誤、一張真實的壞憑證。那個訊號裡沒有任何東西在說「你對信任的視角過期了」;它看起來與憑證真的是壞的一模一樣。它發生在一個測試桶上,不是客戶路徑。而且因為這個故障是失敗關閉的—拒絕一個好的上游,從不信任一個壞的—它沒有觸發任何安全警報。一個保守的錯誤,安靜地犯著錯,披著一場合理拒絕的戲服。
是什麼結束了它
一次例行的自我更新滾過機群,重啟了那些代理程式。拒絕瞬間歸零。這就是全部的線索:當重啟就能修好,你看到的就是過期的記憶體狀態。重啟後的程序在開機時重新讀了信任庫,看見了那個已經躺在那裡數週的 CA,驗證通過上游,然後安靜了。在那一刻,憑證、CA 或網路都沒有任何改變。改變的,只有代理程式的那張快照。
教訓
四點,而且沒有一點與密碼學有關:
• 一個長時間運行的程序對系統設定的視角,是一張快照,不是一份訂閱。在啟動時讀一次某樣東西,你就被凍結在開機那一刻—CA 庫、DNS、設定檔全都向前走了,而你沒有。任何被一個程序當作輸入的東西,都必須是可重新整理的,否則它就成了一顆緩慢的定時炸彈。
• 「重啟就好了」是診斷,不是解法。如果重啟程序能清掉錯誤,別急著慶祝,去找出什麼東西被讀了一次、卻從未重新整理。重啟沒有修好那個 bug;它只是重設了症狀。
• 一個看起來合理的故障,是 bug 最好的藏身處。要把「憑證是壞的」與「我對信任的視角過期了」分辨開來,需要兩個來源:主機(它信任那個 CA)對上代理程式的記憶(在那之前載入的)。那個落差,就是 bug。一個與自己一致的單一來源,什麼也沒告訴我們。
• 重新整理必須是頭等的操作。修法不是硬接上一個輪詢計時器;而是讓代理程式在收到一個訊號時重新載入它的信任庫,好讓加入或輪替一個上游 CA,無須重啟就能生效。一個真實的部署,會在操作人員輪替內部 CA 的那一天撞上這道牆—同一道牆,發生在一條要緊的路徑上。
這與讓後量子消失的錯誤是同一個教訓,只是從另一個方向來:在那裡,我們自己的常數彼此一致、卻全都錯了;在這裡,我們自己的記憶與即時的系統相左,而那個記憶才是錯的那個。兩者的修法是同一種直覺—相信即時的狀態,而非快取的那個。這也是為什麼遷移密碼學更關乎營運、而非數學的一小塊原因,也是我們花時間之處。
← 所有文章