ポリシーは残っているのにWebhookが消えた
一言でいうと
ポリシーファイルが残っていること、APIサーバーにWebhookが登録されていること、そのWebhookが応答することは、それぞれ別の事実です。Webhook障害の実験では、この3つを別々に観測しなければなりません。
なぜ必要なのか
デプロイ担当者が、こう言います。「セキュリティポリシーをFailにしておいたのに、エンジンを止めても違反のPodが作成されました」。すぐにKubernetesのバグだと結論づけてよいのでしょうか。コントローラーを正常終了させる過程で、呼び出しの登録が消えた可能性もあります。呼び出すWebhookそのものがなければ、そのWebhookの呼び出し失敗をどう処理するかを決めたfailurePolicyを、テストしたことにはなりません。
2026年9月12日のLabHubの専用検証VMでも、この状況を観測しました。コントローラーをreplicas=0に減らしてPodが消えたことを確認したのに、リクエストが通過しました。そのとき保存したValidatingWebhookConfigurationの一覧には、該当のリソースのWebhookがありませんでした。これを「Failが壊れている」という証拠としては使いませんでした。その後、呼び出しの登録を残したまま、同じコントローラープロセスの応答だけを短く止めたところ、マッチするリクエストが3秒後にタイムアウトで拒否されました。正常終了と応答の停止は、同じ実験ではありません。
どう動くのか
この単元は、Kyverno v1.19.1とk3s v1.35.8+k3s1で再現した現象を扱います。ほかのバージョンやインストールオプションでも、正常終了のときにまったく同じようにWebhookが消えると一般化はしません。実験を移すときは、インストールされたバージョンと実際の登録を、あらためて確認する必要があります。
まず、3つの設定を分けます。
- validationActionsのDeny: 検証の結果が違反のときに、リクエストを拒否する動作です。
- failurePolicyのFail・Ignore: ポリシーを正常に評価できないエラーをどう扱うかを決めます。ここでは、実際のWebhook呼び出しのタイムアウトを観測します。
- webhookConfiguration.timeoutSeconds: 呼び出しを待つ制限です。正常な違反の応答を待ってから許可に変えるタイマーではありません。
Ignoreは、「検証しない」という意味ではありません。サーバーが正常に違反を判断して拒否の応答を送ったなら、その拒否は維持されます。したがって、Ignoreの実験でも、まず正常なサーバーに違反のリクエストを送って、拒否されることを確認しなければなりません。このベースラインなしに、障害中の成功だけを見ると、ポリシーが最初から適用されていなかったことと区別できません。
Kyverno ValidatingPolicyの公式説明で、ポリシーのフィールドと自動生成の動作を確認できます。このAPIは、policies.kyverno.io/v1です。古いClusterPolicyの例のフィールドの位置をそのまま移さず、インストールされたCRDとkubectl explainで照らし合わせてください。Kubernetes自体のValidatingAdmissionPolicyで実行する経路も別なので、エンジンのプロセス停止実験には混ぜません。
ネームスペースがそのまま実験の範囲
対象のネームスペースには、environmentラベルが必要なPod作成のルールを適用し、対照のネームスペースは、その選択範囲の外に置きます。namespaceSelectorは、Podのラベルではなく、ネームスペースのラベルを選びます。Podに付けるenvironmentと、ネームスペースを選ぶkubernetes.io/metadata.nameは、別の層です。
ポリシーの宣言だけを読んでも十分ではありません。生成されたWebhookで、実際のserviceのパス、failurePolicy、timeoutSeconds、namespaceSelector、CREATE podsのルールを確認します。追加のobjectSelectorやmatchConditionsがあると、リクエストが検証を迂回できるので、一緒に見る必要があります。範囲外の正常な動作は対照群であって、範囲内の障害がなかったという証拠ではありません。
Kubernetesの動的アドミッションの公式ドキュメントは、リクエストのマッチングと、Webhook呼び出し失敗の処理を区別しています。「すべての作成リクエストが止まる」ではなく、「そのWebhookにマッチするリクエストが、呼び出しの失敗のために止まる」と説明してはじめて、正確になります。
同じリクエスト2つと対照群1つ
実験ごとに、新しいPod名を使います。すでに存在する名前を再利用すると、AlreadyExistsが発生して、ポリシーの拒否やタイムアウトと混ざります。APIの応答だけでなく、実際に保存されたかどうかとUIDも照会して残します。成功の文言1行だけで、保存されたと推定しないでください。
| 条件 | 対象の正常なラベルのリクエスト | 対象の違反リクエスト | 範囲外の違反リクエスト |
|---|---|---|---|
| Webhook正常、Deny | 保存 | 明示的なポリシー拒否 | 別途、範囲を検査 |
| 登録を維持・応答停止、Fail | タイムアウト・未保存 | タイムアウト・未保存 | 保存 |
| 登録を維持・応答停止、Ignore | 制限時間のあとに保存 | 制限時間のあとに保存 | 保存 |
| Webhookの復旧 | 既存のPodを別途確認 | 再び明示的に拒否 | 復旧の範囲に合わせて検査 |
表の障害の2行は、上の専用VMで観測した結果です。リクエストごとに、実際のエラーの原文を残しました。Failのエラーは、該当のWebhookを呼び出している途中にcontext deadline exceededが発生したもので、正常な違反のエラーは、該当のポリシーがdenied the requestを返したものです。どちらもターミナルでは失敗のように見えますが、原因は違います。DNSエラーやconnection refusedは、また別の故障の種類であり、応答だけが止まる今回の実験の成功の代わりには数えません。
すでに実行中のPodが残る理由
アドミッションは、リクエストが保存される前のゲートです。今回のルールの対象はCREATE podsで、バックグラウンド評価はオフにしておきます。先に許可されて実行中のPodを自動で削除するコントローラーを作ったわけではありません。したがって、既存のPodの同じUIDとRunningを、別途観測します。同じ名前で新しく作ったPodを「生き残った」と数えてはいけません。
現場での姿
金曜日のデプロイ直前に、セキュリティエンジンの応答が遅くなったと仮定してみましょう。Ignoreに変えると、デプロイの可用性は改善するかもしれませんが、評価されていない変更を受け入れるリスクが生じます。Failを維持すると、該当のリクエストは止まるかもしれませんが、検証なしに保存される経路を閉じます。どちらにしても、目的・範囲・障害を許容する時間を先に決める必要があります。「たいていはIgnoreのほうがよい」という一文で決めることではありません。
Kubernetes Webhookの設計指針のfail-openの勧告は、mutating Webhookと最終状態の検証を一緒に見る文脈のものです。これを、すべてのセキュリティ検証Webhookに対する無条件のIgnoreの勧告として読まないでください。高可用性・呼び出し範囲の縮小・十分なリソース・障害の通知を、一緒に設計する必要があります。
replicasの復旧とポリシーの復旧は同じ時刻ではない
コントローラーを再び1つに増やしたからといって、すぐに検証が戻るわけではありません。新しいPodの起動、リーダーの引き継ぎ、Webhookの登録、実際のポリシーの応答を、分けて確認する必要があります。この単元の最初の受講生の操作経路でのテストでは、23:49:53 UTCにreplicasを復旧しましたが、新しいプロセスがリーダーのLeaseを取得したのは23:50:27でした。20秒の登録待ちは、その前に終わっていました。これはFailのポリシー違反の判定ではなく、ラボ実行ツールの準備待ちの失敗でした。
Kyverno v1.19.1のリーダー選出の実装はLeaseを使い、終了時にすぐ返却しないように設定します。実際の待ち時間は、インストールと負荷によって変わります。観測した34秒を、すべての環境の定数として使わず、ログと登録の状態を確認してください。ラボは、復旧中の待ちに失敗しても、スケールダウン中に観測したポリシー・登録・リクエストの結果を保存します。再試行ボタンを押す前に、何が復旧して、何がまだ確認されていないのかを、まず区別する必要があります。
障害を作るツールにも復旧の設計が必要
一時停止は、受講生専用VMの内部の単一のKyvernoコンテナにだけ適用します。コンテナID・Pod UID・PIDの開始時刻を確認し、Linuxのpidfdにシグナルを結び付けて、数字のPIDの再利用で別のプロセスに触れないようにします。別の監視プロセスが20秒以内に自動で再開し、停止と復旧を、次のステップのクリックの間に分けません。監視プロセスは、実験の実行ツールが強制終了されたり、準備の応答の前に消えたりした経路でも、対象の再開を試みます。
この仕組みは、正常終了の手順、高可用性のテスト、運用障害の訓練のすべてを代替するものではありません。監視プロセス自体まで停止させたり、VMを終了させたりする状況は、別の失敗の領域です。採点ツールは、障害を再び作らず、保存した結果を読み取ります。ファイルベースの採点は、悪意あるrootによるすべての証拠の偽造を防ぐセキュリティ認証でもありません。学習において、観測の根拠が空だったり、互いに矛盾したりすることを検出するために使います。
次のラボですること
最初のラボは、正常なスケールダウンと登録の不在を扱います。対象と対照の範囲を記録し、ValidatingPolicyを直接書いたあと、正常なベースラインと、正常なスケールダウンのときの登録状態を観測します。復旧リクエスト・登録の確認・実際の拒否応答の時刻を区別し、サーバーdry-runで、検証と保存が別の段階であることを確認します。
2つ目のラボは、新しいVMで独立して始めます。最初のラボのファイルは必要ありません。ポリシーと正常なベースラインを再び作ったあと、呼び出しの登録を維持した短い応答の停止で、FailとIgnoreを比べます。どちらのラボも、ポリシーの存在・登録・応答・既存のPodの生存を分けて、レポートを書きます。結論よりも先に、生データがなければなりません。
dry-runもどこで実行したかを確認する
Kubernetesのサーバーdry-runの公式説明によれば、サーバーdry-runは、変更を保存せずに、そのリクエストの検証経路を実行します。一方、client dry-runは、ローカルで出力を組み立てるので、サーバーWebhookが拒否するかどうかを立証できません。ファイルの文法を確認すること、サーバーがリクエストを受け入れること、オブジェクトが実際に保存されることは、それぞれ別の検証です。
このラボでは、一意の名前で、正常なリクエストと違反のリクエストをそれぞれserver dry-runし、同じ名前のPodを別途照会します。正常な応答のkind・名前・namespace・ラベルが合っていて、保存の結果はない必要があります。違反のリクエストは、ポリシーの目印がある明示的な拒否でなければなりません。成功の出力にPodオブジェクトが見えるという理由だけで保存されたと記録したり、AlreadyExistsをポリシーの拒否として記録したりしないでください。リクエストを受けるWebhookも、dry-runと副作用の処理に対応している必要があるので、ほかのインストールに移すときは、実際のWebhookのsideEffectsの設定も一緒に確認しなければなりません。