我們對自己的一套服務做過一次完整健檢:程式碼、主機、網域設定、版本庫全部走一遍。

結論是沒有找到任何可利用的漏洞。那套系統的安全推理其實相當完整:幾十份架構決策紀錄、CI 裡的秘密掃描與相依性稽核、在格式化層而不是過濾層做的日誌遮蔽、沒有 unsafe-inline 的內容安全政策、用 DOM 組裝而不是字串拼接的 Markdown 渲染、Argon2id、以及一道裝在 Docker 自己的鏈上的防火牆規則——因為有人實測過作業系統層的防火牆看不見已發布的容器埠。

真正的問題是另一件事:沒有人在看,而且有一整排檢查不可能失敗。

這篇文章整理那次健檢與後續維運盤點歸納出來的六種形狀。它們的共同點是:從外面看,監控是存在的。

一、不可能失敗的檢查

版本庫裡那句話說得最準:一個不可能失敗的檢查比沒有檢查更糟,因為它佔住了真檢查的位置。

那次找到的包括:

  • 一支本機驗證腳本回報成功,實際上什麼都沒跑。版本庫裡每個 shell 腳本的權限都是 644,說明文件要你執行的那支回的是「Permission denied」;外層腳本用 exec 接它,而呼叫端讀到的是管線的狀態碼。
  • 驗收腳本裡的 suite() 印出一個名字就計一次通過,不檢查它指名的測試是否存在。三個名字指向的路徑已經消失好幾週。
  • 一個「1 GB 負載測試」是寫死的通過,裡面還留著當初那天的日期。
  • 架構文件說整個設計建立在某道邊界上,而那道邊界兩種位址家族都沒有任何自動探測。

要分辨這種檢查只有一個方法:**讓它失敗一次。**把被檢查的東西弄壞,確認它真的會紅;不紅就是它根本沒在檢查。這件事只有在你刻意做的時候才會發生,因為正常情況下它每天都是綠的。

另一個產品上有一個比較溫和的版本:日誌巡檢在沒有設定日誌服務網址時會直接成功返回。這在本機開發是對的,在正式環境設錯就是整支巡檢靜默失效,排程照跑、什麼都不會紅。

二、告警寫給沒有人看的地方

同一次健檢裡最貴的一項:每月預算上限的開關預設是關的,而正式環境的設定檔裡根本沒有這個變數。跨過上限之後,系統做的唯一一件事是每一輪在日誌裡多印一行錯誤。那是警告給沒有人聽的。

更隱蔽的是通往同一個地方的第二條路:沒有定價資料的模型一律被計成零成本,因此對預算上限完全隱形。把一個模型放進這個狀態只需要改一行環境變數——而就在三週前,有一次決策把判官模型換掉並附帶了價格資料的 migration,一次「只帶設定、沒帶 migration」的部署就會跑出一個以零元計費、而且不會有人發現的模型。

修法有兩層:預設改成真的擋下來;以及在啟動時就把「有設定、但沒有價格」的模型列出來,並讓就緒檢查把它當成一項條件。

健康檢查本身也有同一種形狀。存活檢查刻意只回答「這個行程的事件迴圈還在轉嗎」,不查資料庫——一個會查資料庫的存活檢查,會在資料庫打嗝時把健康的應用重啟成無限崩潰迴圈。這個設計是對的。但容器的健康檢查打的正是存活端點,而只有就緒端點知道背景工作行程已經死了,於是沒有任何東西在問那個知道答案的端點。

三、告警管道依賴被監控的東西

這條最容易在設計時漏掉:用 Email 監控 Email 額度,額度耗盡時警報信也寄不出去。

所以那支額度監控刻意不走信件,也刻意不收進後台面板——它用的是持續整合服務上那個排程工作的紅燈。那是唯一還能主動推、又完全不依賴被監控對象的管道。

反過來,其他巡檢的告警出口統一進後台面板,理由是同一條原則:**面板不依賴 Email 服務,也不依賴訊息推播,而那兩者正好是被巡檢的對象。**只有最嚴重的等級才額外寄信。

代價是面板要登入才看得到,所以兩條管道必須互補,而不是重複。設計監控時值得問一句:**這則告警要送達,需要哪些東西是好的?**那份清單與被監控對象的交集應該是空的。

四、被雜訊淹掉

某次盤點時,面板上有五則未讀告警。其中三則不是問題:我們自己的入口巡檢、人工驗證設定時打進去的測試請求、以及一批使用者連線階段過期造成的授權失敗。

而真正該響的那件事完全沒有告警:近十四天有二十筆「工具執行成功、但回覆沒送出去」的對話輪次,沒有任何一條規則在看它。

噪音的來源常常是自己。兩個固定節奏的例子:入口巡檢每三小時用兩種瀏覽器識別字串各打一次某個維持 404 的路徑,六十小時累積四十筆;部署流程的煙霧測試會故意送一個錯誤的簽章去確認它被拒絕,於是每次部署都會留下一筆「簽章驗證失敗」的警告。這兩種都正確、都該留著,但它們應該被辨識出來,而不是每次都讓人重查一遍。

門檻的設計也會自己製造雜訊。一個投遞品質檢查用七日滾動窗口與 95% 的比率門檻,而週寄送量大約一百三十封——等於「最多容許六封問題信」。一位收件人信箱滿了、系統重試兩次,就吃掉三分之一的容許額度,然後那個紅燈會連續亮七天。而那兩筆根本不是我們的問題:暫時性的退信與永久性的退信被混在同一格計算。

修法是兩件事一起做:把暫時性與永久性分開,以及給比率門檻加一個絕對值下限。光是把暫時性退信排除,可送達率就從 93% 變成 95.45%。

還有一種雜訊來自基線的定義。「新出現的錯誤」這種告警取決於「見過的簽章」存在哪裡——原本存在一個設定為淘汰最少使用鍵的快取裡,鍵被淘汰就讓舊錯誤重新變成新的。改存進資料庫之後才穩定。兩個仍然成立的性質:巡檢第一次執行只是在建立基線,那天真正新的錯誤會被無聲吸收;而簽章算法一改,所有舊錯誤都會變成新的。

五、巡檢本身會改變狀態

診斷線上問題時,「跑一下巡檢看看」是個陷阱。這些指令通常不是唯讀的。

  • 日誌巡檢會把當下所有錯誤簽章寫進「已見過」的基線,等於親手吃掉一個還沒發出的真警報。
  • 入口巡檢在非嚴格模式下發現問題會直接送出最高等級告警,並消耗掉那個告警鍵的三小時冷卻,接下來三小時裡排程那支真正的告警會被整段壓掉。

通則很簡單:**跑之前先確認它有沒有唯讀模式;沒有就別跑,改去查它讀的那份資料。**這也反過來說明每支巡檢都該有一個唯讀模式——不只是為了安全,而是因為沒有唯讀模式的檢查,在最需要它的時候反而不能用。

六、覆蓋面的缺口:同一句話描述兩種原因

最後一種最難發現,因為所有東西都正常運作。

每日推播有三種結果:成功、失敗、略過。告警規則只看失敗。而「略過」底下其實有兩種完全不同的原因:使用者自己把所有內容區塊都關掉了(慢性狀態,不該叫),以及資料來源全部抓取失敗(真故障,應該叫)。兩者共用同一句說明文字,所以後者沒有任何專屬告警。

那次事故被接住純屬運氣:日誌巡檢因為那行錯誤剛好含有一個沒見過的行政區名,把它當成新簽章報了出來。同一個行政區七天內再壞一次就不會叫了。

查下去還量到一個有意思的背景事實:那個上游氣象服務在整點的第一次請求逾時率是 4.1%(515 次裡 21 次),半點是 0.1%,其餘分鐘是 0%。而推播時間幾乎都設在整點或半點。平常看不出來,是因為有舊快取的區域會走降級路徑。

同一類缺口還有另一種面貌:卡在中間狀態、沒有人掃的資料列。那次健檢在資料庫裡找到一個永遠不會被執行、也永遠不會被標成失敗的工作——領取邏輯會過濾掉重試次數已滿的列,而兩支清理程序只看「執行中」的列。它既跑不起來也不算失敗,所以不會出現在維運人員唯一會看的那張「沒做完的工作」清單上。另外還有九十六個連線階段在過期之後仍然寫著「使用中」,因為那個狀態轉換被指派給一支從來沒有真的去寫它的工作——驗證邏輯讀的是到期時間,所以這不是授權漏洞,但任何「目前在線數」的數字都是虛構的。

修法不是加告警,是讓狀態欄位誠實:沒有人會去掃的狀態,就不該存在。

會自己悄悄失效的保護

把上面六種形狀串起來的,是一個更一般的性質:保護措施會自己悄悄失效,而「悄悄」正是它們的定義。

最好的例子是那次健檢裡的夜間備份。它和資料清理被寫成同一個一次性服務單元的兩行指令,而系統服務管理器在第一行失敗時就會停止——第一行偏偏是「進入應用程式容器執行」。於是**每一個那個容器不健康的夜晚(記憶體不足後重啟、換映像檔的空窗),備份都沒有跑,而且沒有任何東西說一聲。**唯一會注意到的機制,是文件裡記著「沒有人開始過」的一個習慣。

順序本身也有意義:備份排在清理之前,會讓「對話保留三十天」在備份目錄裡變成三十七天。所以修法是把兩者放進同一支腳本、任一失敗就以非零狀態結束,並寫下一個時間戳給驗收腳本讀。

這個修法還有第二幕:**它到不了正式環境。**版本庫裡從來沒有任何東西把服務單元檔複製到機器上,那台機器跑的是當初手動安裝的版本。部署手冊因此補上了安裝步驟,以及怎麼分辨「檔案複製過去了」與「系統真的重新讀取了它」。

另一個產品上有一個更乾脆的版本:官網曾經保留一個備援來源當作逃生閥。某天實測時它還能正常回應,三天後同樣的請求已經變成 404——主網域轉向之後,原本那個服務的網域驗證失敗就自動解除了綁定。沒有被驗證的備援,等於沒有備援。

可以帶走的原則

  • 不可能失敗的檢查比沒有檢查更糟,因為它佔住了真檢查的位置。要分辨只有一個方法:把被檢查的東西弄壞一次,確認它真的會紅。
  • 「超過上限就在日誌裡警告」是警告給沒有人聽的。上限要真的擋,而且要確認沒有一條路徑可以繞過計量(例如沒有定價的項目被計成零)。
  • 存活檢查不該查外部相依,否則外部打嗝會變成無限崩潰迴圈;但那也表示只問存活的人永遠不會知道背景工作死了。
  • 告警要送達所需要的那串東西,不可以和被監控的對象有交集。用 Email 監控 Email 額度不會成立。
  • 噪音的來源常常是自己的探針與部署流程。那些告警是對的,但要能被一眼辨識,否則會把人訓練成不看面板。
  • 比率門檻在小樣本上會失真,要加絕對值下限;「新出現」這種判斷取決於基線存在哪裡,以及基線被重建時會發生什麼。
  • 巡檢指令多半不是唯讀的,手動執行會消耗告警狀態與冷卻。每支巡檢都該有唯讀模式。
  • 同一句說明文字描述兩種原因,其中一種就會永遠沒有告警。要讓狀態誠實,而不是在模糊的狀態上加規則。
  • 沒有人會去掃的中間狀態不該存在,否則用它算出來的任何指標都是虛構的。
  • 保護措施會自己悄悄失效。備援、備份、逃生閥都要被定期驗證,而驗證的方式是實際使用它。