基礎設施

當 Declarative Firewall Sync 修剪規則並切斷存取時

防火牆同步作業修剪了僅由手動建立的規則,並切斷了線上流量。以下是事後檢討報告、根本原因,以及如何讓修剪作業變得安全。

本文由 AI 模型從英文原文翻譯而來,用字可能與原文有所出入。 閱讀英文原文

一個根據程式碼中真實來源來核對防火牆規則的宣告式同步迴圈,完全照著指示執行,而這正是問題所在。某個下午,它移除了一組線上規則,切斷了已運作數月的流量。沒有人變更過程式碼。它所刪除的規則,是在宣告式來源之外,於主控台中手動新增的;而同步機制將任何不在來源中的東西都視為待修剪的垃圾。本文是該事件的事後檢討,內容刻意寫得通用,並探討了能防止自動化修剪變成一種武器的設計變更。

發生了什麼事

該環境中有一個調解器。一個在版本控制中的宣告式來源,描述了每個網路邊界所需的防火牆和安全群組規則。一個自動化程序會按排程讀取該來源,將其與基礎設施上的線上規則集進行比較,並使線上規則集與之匹配。來源中的新規則會被建立。存在於線上但來源中沒有的規則會被刪除。這種「為達匹配而刪除」的行為是宣告式調解的重點,也正是這次服務中斷的全部原因。

數週前,有人為了應對緊急需求,直接在基礎設施主控台中新增了幾條規則。一條特定的對外路徑和幾個服務所依賴的連接埠。這些規則從未被寫回宣告式來源中。它們只存在於線上基礎設施上。有一段時間,這個問題沒有浮現,因為自從手動編輯後,同步作業就沒有針對該邊界執行過,或者是以不進行修剪的模式執行。

然後,一次正常的同步作業執行了。它在線上端看到了宣告式來源中沒有出現的規則。根據其合約,這些規則是需要被修正的漂移,所以它刪除了它們。對外路徑消失了。通過其中一個被修剪連接埠的健康檢查開始失敗。依賴於手動新增規則的流量停止了。自動化程序回報成功,因為從它的角度來看,線上狀態現在與來源完美匹配。重要的警報並非來自同步作業。而是來自那些再也無法連線到所需資源的服務。

時間線簡述

這個順序很短,且在許多團隊中重複發生,這就是為什麼值得將其清楚說明。

  1. 一組規則以宣告式方式定義並自動進行核對。到目前為止都正確。
  2. 出現了緊急需求。有人在主控台中手動新增規則以立即修復問題。宣告式來源沒有被更新,因此悄悄地出現了第二個真實來源。
  3. 時間過去了。手動新增的規則持續運作。即時狀態與宣告式來源之間的差距在無人注意的情況下擴大。
  4. 一次啟用了清理 (prune) 功能的例行同步執行。它將手動新增的規則視為未受管理的漂移並將其刪除。
  5. 存取中斷。故障出現在下游,而非造成故障的工具中,因此事件發生的最初幾分鐘都在錯誤的地方尋找問題。

一旦原因明確,復原本身很快。遺失的規則被重新加入,這次是加入到宣告式來源中,存取權限也恢復了。令人不安的不是修復過程,而是意識到這個自動化機制在數週以來,距離這個結果只差一次排程執行,並且這個設計使得此結果是必然的,而非不幸。

根本原因:一個規則集的兩個真實來源

主要原因並非同步機制的錯誤。同步機制依設計運作。原因是,有兩個地方都可以定義現實情況,而其中只有一個被允許存留。

宣告式來源聲稱是唯一的真實來源。主控台讓人類可以直接寫入規則,這使得主控台在實務上成為了第二個真實來源。只要兩者同時被使用,系統中就存在兩個相互矛盾的權威,而協調器的建構目的,就是藉由刪除來源未包含的任何內容,來解決所有分歧,並以來源為準。一條手動新增的規則並非系統會保留的增補。它是一個系統設計上要去抹除的差異。

三個具體的決策將該結構性缺陷轉變為一場服務中斷。

首先,自動化機制透過修剪來強制將來源作為唯一的真實來源,但同時手動編輯的路徑卻保持開放。你不能兩者兼得。如果來源具權威性且修剪功能已開啟,那麼手動編輯就不是捷徑,而是引信未知的定時炸彈。

其次,同步機制在沒有差異預覽也無需批准的情況下,立即套用變更。它從未向任何人展示即將刪除的規則清單。一次模擬執行,若能印出「即將移除 3 條規則,包含一條出站路徑」,就能在讀完一行的時間內阻止這次事件。

第三,也是最重要的一點,該設計將移除和新增視為同一種類型的變更。新增一條來源中包含但基礎設施中缺少的規則,風險很低。最壞的情況是,已經被阻擋的流量會再多被阻擋片刻。移除規則是危險的操作方向,因為它可能切斷正在使用中的存取,而其爆炸半徑只有在事後才能知曉。給予這兩種操作同樣的自動化、無防護處理,意味著安全的操作和具破壞性的操作在同等的信任下執行。

根本原因與對策

這些修復措施與原因一一對應,且每一項措施都獨立地縮小了故障範圍,因此任何一項措施的缺失都不會重新引發服務中斷。

根本原因 其所導致的後果 對策
存在兩個真實來源(程式碼和主控台) 同步程序將手動設定的規則視為漂移 單一真實來源:禁用主控台手動編輯,所有變更都透過宣告式來源進行,偵測到漂移時會發出警示,而非靜默產生
在沒有預覽或核准的情況下套用刪減 (prune) 靜默刪除線上規則 刪減 (Prune) 操作前一律會產生試運轉 (dry-run) 的差異報告,並需要明確的人工核准才能刪除任何內容
將移除操作視同新增操作處理 一個破壞性操作在被當作安全操作的信任下執行了 非對稱處理:新增操作會自動套用,移除操作則需經過審查
對於管理存取權限沒有保護措施 同步程序可能會刪除它絕不應觸碰的規則 一份保護清單將管理規則和健康檢查規則完全排除在核對 (reconciliation) 範圍之外
變更被零碎地套用,沒有復原點 導致部分狀態和緩慢的復原 透過變更前的快照進行原子化套用,以實現一步到位的復原
大量刪除時沒有任何信號 服務中斷在幾分鐘後才於下游系統浮現 當同步操作將移除超過一個小閾值的規則時,會觸發警示

預防:使 prune 成為受保護的操作

指導原則是,一個宣告式系統的安全性取決於它處理刪除的方式。建立是可容錯的。刪除則不然。重新設計後,移除成為必須通過閘門才能執行的操作,而將大多數安全的變更保持自動化。

Prune 現在分兩個階段執行。第一階段會計算來源與即時狀態之間的差異,並按方向分組印出。一邊是新增,另一邊是移除。它會自行套用新增的項目,因為新增不會中斷現有的存取權限。它不會套用任何一個移除項目。反之,它會停止並提出移除項目以供核准,同時提供足夠的上下文來判斷每一項。由人來閱讀清單並決定是否核准。只有在明確核准後,移除階段才會執行。

這種不對稱性是此修復的核心。在穩定狀態下,大多數的同步作業只包含新增或完全沒有變更,這些作業會自動執行,無需人為介入。少數想要刪除某些東西的同步作業,就是少數需要人為介入的同步作業。閘門的成本只落在可能導致服務中斷的操作上,而這正是你希望產生阻力的地方。

閾值警報為此提供了支援。如果一個提議的 prune 操作會一次移除超過少數幾條規則,這會被視為輸入有問題的信號,而非一個大型但例行的變更。一個突然想要刪除二十條規則的同步作業,更有可能是讀取到損壞或空的來源,而不是反映了要移除二十條規則的真實意圖。該警報會在差異比對被核准之前觸發。

預防:單一事實來源與保護清單

修剪閘門限制了漂移所造成的損害。消除漂移則是另一半的工作。如果只有一個地方可以定義規則,協調器就永遠不會將合法的線上規則視為垃圾,因為每個合法的規則都存在於其進行協調所依據的來源中。

這意味著要關閉主控台路徑。透過主控台直接編輯防火牆和安全群組規則的功能已對人類禁用,因此宣告式來源在事實上而不僅僅是意圖上,成為了單一事實來源。緊急變更仍然會發生,但它們是以對來源進行小而快速的變更形式出現,並流經相同的管線,這僅比透過主控台編輯稍慢一些,並使兩個權威來源保持一致而非衝突。一個獨立的漂移偵測器會按排程執行,並在線上狀態與來源有任何不一致時發出警示,因此在修剪動作可能對其產生影響之前,就能以警告的形式及早發現差異。

保護清單則補上了最後一個漏洞。有些規則無論來源如何規定,都絕不能被修剪,因為它們是保持操作人員可連線的規則。管理存取以及健康狀態檢查所經過的路徑是明顯的例子。這些規則被標記為受保護,並完全從協調過程中排除。即使來源完全空白或損毀,也無法刪除它們,這意味著因錯誤來源導致所有人都被鎖在他們需要用來修復問題的基礎設施之外的這種故障模式,已不再可能發生。保護清單在設計上很短,每個條目都必須回答一個問題來證明其存在的必要性:如果此規則消失,我們是否仍能進入以修復損害。

以下是一個帶有這些保證的協調器設定的樣貌。確切的綱要並不重要,重要的是其屬性。

reconcile:
  # Additions apply on their own. Removals never do.
  auto_apply:
    additions: true
    removals: false
  # Removals produce a diff and wait for a human.
  prune:
    require_approval: true
    dry_run_first: true
    # A prune larger than this is treated as a bad input, not a big change.
    alert_threshold: 5
  # Rules the reconciler is forbidden to delete, whatever the source says.
  protect:
    - name: management-access
      match: { port: 22, direction: inbound }
    - name: health-check
      match: { port: 8080, direction: inbound }
  # Snapshot before any change so a bad apply rolls back in one step.
  snapshot:
    before_apply: true
    retain: 10

預防:原子性、快照與可觀測性

最後一層的防護假設不良的變更終究會通過,因為總有一天會發生,並探討你能多快地復原它。在進行任何套用之前,調諧器會為當前的即時規則集建立一份快照。如果套用產生了損壞狀態,復原的方式是還原快照,而不是在壓力下從記憶體中重建規則。在平台允許的情況下,變更會以原子方式套用,因此一次執行不會留下半數規則已更新、半數未更新的狀態,這種情況本身就是一種中斷,且比明確的失敗更難以理解。

可觀測性形成了閉環。事實證明,最有用的單一信號是最粗略的那個:每個邊界的活躍規則數量,當其急遽下降時發出警報。在一次同步中,規則數量從四十條降至三十四條,這是一個值得立即探究的問題,而且這不依賴於對任何單一規則作用的理解。將其與漂移偵測器配對——該偵測器會監控即時狀態與來源之間的不一致——兩者結合起來,可以在人類注意到流量失敗之前,就捕捉到緩慢的問題(漂移累積)和快速的問題(同步時大量刪除)。

通則:宣告式不等於安全

關於宣告式自動化,一個令人安心的說法是它能消除人為錯誤。寫下期望狀態,讓機器來強制執行,並停止手動犯錯。這個說法只對了一半。宣告式調和確實能消除某一類的漂移,且對於新增操作而言,它確實比手動編輯更安全。但它無法讓刪除變得安全,而且它悄悄地提高了擁有多個真實來源的風險。

一旦有兩個地方可以定義現實,一個透過刪除來解決所有衝突的執行者就不是一個安全機制。它是一種以機器速度根據錯誤的真實情況採取行動的自動化方式。這裡的失敗之處不在於使用了自動化,而是在一個允許第二個、非正式的真實來源存在的系統中,一個破壞性操作被賦予了與安全操作相同的信任。

從中得出的規則很簡短。只保留一個真實來源,並偵測與其之間的漂移,而不是任其形成。讓自動化自由地應用在安全的方向上。在危險的方向上設置人工閘門,因為刪除是切斷存取權限的操作,再怎麼宣告式的整潔也無法改變這一點。一個在修剪前沒有人工審核的管線,並不會比一個謹慎的操作員更可靠。它是排定在計時器上運行的、一個謹慎操作員最糟糕的午後。