インフラ

宣言型ファイアウォール同期がルールをプルーニングしてアクセスを遮断する場合

ファイアウォールの同期によって、手動でのみ存在していたルールがプルーニングされ、本番トラフィックが遮断されました。以下に、ポストモーテム、根本原因、そしてプルーニングを安全に行う方法を記します。

この記事は英語の原文をAIモデルが翻訳したものです。表現が原文と異なる場合があります。 英語の原文を読む

コード内の信頼できる情報源に対してファイアウォールルールの整合性を取る宣言的な同期ループは、指示されたとおりに動作しました。そして、それが問題でした。ある日の午後、そのループは稼働中のルール一式を削除し、何ヶ月も機能していたトラフィックを遮断してしまいました。誰もコードを変更していませんでした。それが削除したルールは、宣言的なソースの外で、コンソールから手動で追加されたものでした。そして、同期処理はソースにないものをすべて、剪定すべきゴミとして扱いました。これは、意図的に一般化して書かれたそのインシデントの事後検証であり、自動化された剪定が武器になるのを防ぐための設計変更について述べたものです。

何が起きたか

環境にはリコンサイラがありました。バージョン管理下にある宣言的なソースには、各ネットワーク境界に対して望ましいファイアウォールとセキュリティグループのルールが記述されていました。スケジュールに従って、ある自動化システムがそのソースを読み込み、インフラストラクチャ上の本番のルールセットと比較し、本番のセットを一致させました。ソース内の新しいルールは作成されました。本番環境には存在するがソースにはないルールは削除されました。その一致させるための削除という動作は、宣言的なリコンシリエーションの要点そのものであり、また、この障害の全ての原因でもあります。

数週間前、緊急の要件に対応するため、誰かがインフラストラクチャのコンソールで直接いくつかのルールを追加していました。あるサービスが依存していた、特定の送信パスといくつかのポートです。それらのルールは、宣言的なソースに書き戻されることはありませんでした。それらは本番のインフラストラクチャ上にのみ存在していました。しばらくの間、その問題が表面化することはありませんでした。なぜなら、手動での編集以降、その境界に対して同期が実行されていなかったか、あるいは、整理(prune)を行わないモードで実行されていたからです。

その後、通常の同期が実行されました。それは、本番側にありながら宣言的なソースには現れないルールを検知しました。その仕様に従い、それらのルールは修正されるべきドリフト(乖離)と見なされ、削除されました。送信パスは失われました。整理されたポートの1つを経由していたヘルスチェックが失敗し始めました。手動で追加されたルールに依存していたトラフィックは停止しました。自動化システムは成功を報告しました。なぜなら、その視点からは、本番の状態がソースと完全に一致するようになったからです。重要なアラートは、同期システムから来たものではありませんでした。それは、必要とするものに到達できなくなったサービスから来たものでした。

簡単な時系列

このシーケンスは短く、多くのチームで繰り返されるため、はっきりと述べる価値があります。

  1. ルールセットが宣言的に定義され、自動的に調整されます。ここまでは正しいです。
  2. 緊急の必要性が生じます。誰かがコンソールで手動でルールを追加して、すぐに対応します。宣言的なソースは更新されないため、2つ目の信頼できる情報源が静かに生まれます。
  3. 時間が経過します。手動のルールは機能し続けます。誰も見ていない間に、ライブの状態と宣言的なソースとの間のギャップが広がります。
  4. pruneが有効になった定期的な同期が実行されます。それは手動のルールを管理されていないドリフトと見なし、それらを削除します。
  5. アクセスが壊れます。障害は、それを引き起こしたツールではなく、下流で表面化するため、インシデントの最初の数分間は間違った場所を探すことに費やされます。

原因が明らかになると、復旧自体は迅速でした。失われたルールは、今度は宣言的なソースに再追加され、アクセスは回復しました。厄介だったのは、修正そのものではありませんでした。自動化が、スケジュールされた実行1回でこの結果を引き起こす状態に何週間も置かれていたこと、そしてその設計がこの結果を不運ではなく必然的なものにしていたと気づいたことでした。

根本原因: 1つのルールセットに対する2つの信頼できる情報源

主な原因は、同期処理のバグではありませんでした。同期処理は設計どおりに動作しました。原因は、2つの場所が両方とも実体を定義でき、そのうちの1つだけが存続を許されていたことでした。

宣言的なソースは、唯一の信頼できる情報源であると主張していました。コンソールでは人間が直接ルールを書き込むことができ、これによりコンソールは事実上、2番目の信頼できる情報源となっていました。両方が使用されている限り、システムには互いに矛盾する2つの権威が存在し、リコンサイラは、ソースに含まれていないものをすべて削除することによって、ソースを優先してあらゆる不一致を解決するように構築されていました。手動で追加されたルールは、システムが保持する追加項目ではありませんでした。それは、システムが消去するように設計された差分でした。

3つの特定の決定が、その構造的な欠陥をサービス停止へと変えました。

第一に、自動化はプルーニングによってソースを唯一の真実として強制しましたが、その一方で手動編集のパスは開いたままでした。両立はできません。もしソースが権威であり、プルーニングがオンになっている場合、手動編集はショートカットではなく、いつ爆発するかわからない時限爆弾です。

第二に、同期処理は差分のプレビューも承認もなく、変更を即座に適用しました。削除しようとしているルールのリストを人間に見せることはありませんでした。「アウトバウンドパス1つを含む3つのルールを削除しようとしています」と表示するドライランがあれば、1行を読む時間でインシデントを防げたでしょう。

第三に、そして最も重要なこととして、設計では削除と追加が同じ種類の変更として扱われていました。ソースには含まれているがインフラストラクチャにはないルールを追加することは、リスクが低いです。最悪の場合でも、すでにブロックされていたトラフィックが少し長くブロックされ続けるだけです。ルールの削除は危険な方向性の操作です。なぜなら、アクティブに使用されているアクセスを遮断する可能性があり、その影響範囲は事後にしかわからないからです。両方の操作に同じ自動的で無防備な扱いをすることは、安全な操作と破壊的な操作が同じ信頼レベルで実行されることを意味していました。

根本原因と対策

修正は原因と1対1で対応しており、それぞれが独立して障害の範囲を狭めるため、1つに不備があっても障害が再発することはありません。

根本原因 それによって何が許されたか 対策
2つの信頼できる情報源 (コードとコンソール) 同期プロセスがドリフトとして認識した手動ルール 信頼できる情報源の単一化: 手動でのコンソール編集を無効化し、すべての変更は宣言的なソースを介して行われるようにする。ドリフトはサイレントに作成されるのではなく、検出され、アラートが通知される
プレビューや承認なしで適用されたPrune 稼働中のルールのサイレントな削除 Pruneは常にドライランの差分を生成し、何かを削除する前に明示的な人間の承認を必要とする
削除が追加と同様に扱われた 安全な操作であるという信頼のもとで、破壊的な操作が実行された 非対称な処理: 追加は自動的に適用され、削除はレビューを必須とする
管理アクセスに対する保護の欠如 同期プロセスが、決して触れるべきではないルールを削除できてしまった 保護リストにより、管理ルールとヘルスチェックルールが調整プロセスから完全に除外される
ロールバックポイントなしで変更が断片的に適用された 部分的な状態と遅い復旧 変更前のスナップショットを使用したアトミックな適用により、ワンステップでのロールバックを可能にする
一括削除に関するシグナルがなかった 障害は数分遅れて下流で表面化した 同期によって小規模な閾値を超える数のルールが削除される場合にアラートが発せられる

予防策: pruneをガード付きの操作にする

宣言的なシステムは、その削除の扱いと同程度にしか安全ではない、というのが指針です。作成は寛容です。削除はそうではありません。この再設計では、安全な変更の大部分は自動のままにしながら、削除をゲートを通過しなければならない操作としました。

pruneは現在、2つのステージで実行されます。最初のステージでは、ソースとライブの状態の差分を計算し、方向ごとにグループ化して表示します。一方に追加、他方に削除。追加は既存のアクセスを遮断することがないため、追加は自動的に適用されます。削除は一つも適用されません。その代わりに、処理は停止し、それぞれを判断するのに十分なコンテキストと共に、承認のために削除項目を提示します。人間がリストを読み、承認するか、あるいは承認しません。明示的な承認があった後でのみ、削除ステージが実行されます。

その非対称性が、この修正の核心です。定常状態では、ほとんどのsyncは追加のみを含むか、あるいはまったく変更を含まないため、人間の介在なしに、そのまま実行されます。何かを削除しようとするまれなsyncこそが、人間が介在することになるまれなsyncなのです。ゲートのコストは、停止を引き起こす可能性のある操作にのみかかります。これはまさに、摩擦をかけたい場所なのです。

しきい値アラートがこれをバックアップします。提案されたpruneが一度に少数のルール以上を削除しようとする場合、それは大規模だが日常的な変更としてではなく、入力に何か問題があるという信号として扱われます。突然20個のルールを削除しようとするsyncは、20個のルールを削除するという実際の意図を反映しているというよりは、壊れているか空のソースを読み込んでいる可能性の方がはるかに高いです。このアラートは、差分が承認される前に発せられます。

防止策: 信頼できる唯一の情報源と保護リスト

プルーンのゲートはドリフトによる損害を制限します。ドリフトを排除することが、もう一方の半分の課題です。ルールを定義できる場所が1つだけであれば、リコンサイラーは正当なライブ ルールをゴミとして認識することはありません。なぜなら、すべての正当なルールは、リコンサイラーが照合するソースに存在するからです。

それはコンソール経由のパスを閉じることを意味しました。人間によるコンソールを介したファイアウォールやセキュリティグループのルールへの直接編集は無効化されたため、宣言的なソースが、意図だけでなく事実上の信頼できる唯一の情報源となりました。緊急の変更は依然として発生しますが、それらは同じパイプラインを流れるソースへの小さく迅速な変更として行われます。これはコンソールでの編集よりわずかに遅いだけで、2つの権限源を競合させるのではなく、一致した状態に保ちます。別のドリフト検出器がスケジュールに従って実行され、ライブの状態とソースが少しでも一致しない場合にアラートを発するため、プルーンが作用するずっと前に、乖離が警告として捕捉されます。

保護リストが最後の穴を塞ぎます。ソースの内容に関わらず、決してプルーンしてはならないルールがいくつかあります。なぜなら、それらはオペレーターがアクセス可能な状態を維持するためのルールだからです。管理アクセスやヘルスチェックが通過するパスがその明白な例です。それらは保護対象としてマークされ、リコンシリエーションから完全に除外されます。完全に空であったり破損していたりするソースでさえ、それらを削除することはできません。これは、不正なソースによって誰もが修正に必要なインフラストラクチャから締め出されるという障害モードが排除されることを意味します。保護リストは設計上短く、各エントリは「このルールが消えた場合、我々はまだ損害を修復するためにアクセスできるか」という1つの質問に答えることによって、その場所を確保します。

以下に、これらの保証を実装するリコンサイラー設定の形式を示します。正確なスキーマは重要ではなく、そのプロパティが重要です。

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

防止: アトミック性、スナップショット、可観測性

最後のレイヤーは、いずれは不正な変更が通過してしまうため、それが通過することを想定し、どれだけ速く元に戻せるかを問います。適用 (apply) の前に、reconciler は現在のライブのルールセットのスナップショットを取得します。適用 (apply) によって壊れた状態になった場合、復旧とは、プレッシャーの中でメモリからルールを再構築するのではなく、スナップショットを復元することです。プラットフォームが許す限り、変更はアトミックに適用されます。そのため、実行によってルールの半分が更新され、半分が更新されないままになることはありません。これは、クリーンな失敗よりも原因究明が困難な、それ自体が一種の障害です。

可観測性がループを閉じます。最も有用な単一のシグナルは、最も粗雑なものであることが判明しました。それは、急激に減少したときにアラートが発せられる、境界ごとのアクティブなルールの数です。1回の同期でルール数が40から34に減少することは、直ちに問う価値のある疑問であり、個々のルールが何をするかを理解することには依存しません。それを、ライブの状態とソースの不一致を監視する drift detector と組み合わせると、この2つが合わさって、人間がトラフィックの障害に気づくずっと前に、(ドリフトが蓄積する) 遅い問題と (同期によって一括削除される) 速い問題の両方を捕捉します。

一般的な教訓:宣言的は安全と同じではない

宣言的オートメーションに関する心地よい話は、それがヒューマンエラーをなくすというものです。望ましい状態を記述し、マシンにそれを強制させ、手動でのミスをやめる。その話は半分だけ真実です。宣言的なリコンシリエーションは、ある種のドリフト(乖離)を確かに取り除き、追加に関しては手作業での編集よりも本当に安全です。それがしないことは、削除を安全にすることです。そして、複数の信頼できる情報源(source of truth)を持つことのリスクを静かに高めます。

2つの場所が現実を定義できるようになった瞬間、あらゆる競合を削除によって解決する強制実行者(enforcer)は安全機構ではありません。それは、間違った真実に基づいてマシンの速度で行動するための自動化された方法です。ここでの失敗は、オートメーションが使われたことではありませんでした。それは、2つ目の非公式な信頼できる情報源が存在することを許されたシステムにおいて、破壊的な操作が安全な操作と同じ信頼を与えられたことでした。

そこから導き出されるルールは短いものです。信頼できる情報源を厳密に1つに保ち、ドリフトが形成されるのを許すのではなく、そこからのドリフトを検出すること。オートメーションには安全な方向の操作を自由に適用させましょう。危険な方向の操作には人間のゲートを設けましょう。なぜなら、削除はアクセスを断つ操作であり、どれだけ宣言的に整理されていてもその事実は変わらないからです。枝刈りの前に人間を介さずに枝刈りを行うパイプラインは、慎重なオペレーターよりも信頼性が高いわけではありません。それは、慎重なオペレーターの最悪の午後が、タイマーで実行されるようにスケジュールされたものです。