Skip to main content

「K3sはNetworkPolicyを無視する」は嘘だった。それでも通信が漏れていた、ServiceLBという死角

deny-allのNetworkPolicyを書いたのに、外からの通信だけ普通に通ってしまった

section01

検証環境を本番相当に近づけるため、ある名前空間に podSelector: {} の deny-all ingress ポリシーを適用した。 Pod間の通信は狙い通り遮断された。別の名前空間からの curl は案の定タイムアウトする。ところが、 Service を type: LoadBalancer で外部公開しているエンドポイントに対して、クラスタの外から同じ疎通確認をすると、 NetworkPolicyが存在しないかのように普通にレスポンスが返ってきた。

k3s networkpolicy が期待通り動いていないと分かった瞬間、まず疑うのは「K3sのデフォルトCNIであるFlannelは NetworkPolicyを無視するから」という、よく聞く説明だろう。実際、素のFlannelにはNetworkPolicyを強制する機能が ない。だが結論から言うと、**この説明はK3sには当てはまらない。**K3sには最初からNetworkPolicyを強制する 仕組みが組み込まれているからだ。ではなぜ通信が漏れたのか。原因はNetworkPolicyの実装そのものではなく、 K3s標準のLoadBalancer実装である ServiceLB の経路にあった。

「K3sはNetworkPolicyを無視する」という誤解を正す

K3s公式ドキュメントによれば、K3sはFlannelに加えて kube-routerのNetworkPolicyコントローラをライブラリとして内蔵し、デフォルトで有効化している。 kube-router自体の他の機能(BGP等)は使わず、NetworkPolicyを強制するコントローラ部分だけを取り込んでいる形だ。 つまりK3sクラスタを構築した時点で、追加のCNIを何もインストールしなくても、NetworkPolicyリソースをapplyすれば その内容が実際に反映される。「K3s = Flannelだから NetworkPolicy は無視される」という単純化は誤りで、 これはむしろ一般的なFlannel単体構成(例えばkubeadmで素のFlannelだけを入れた場合)との重要な違いだ。

この内蔵コントローラを無効化したい場合は、サーバー起動時に --disable-network-policy を明示的に指定する 必要がある。K3sのネットワークオプション公式ドキュメント では、Calico や Cilium のような独自のポリシーエンジンを持つCNIに切り替える際は、内蔵コントローラとの 競合を避けるためにこのフラグを併用することが推奨されている。

つまり検証環境で deny-all ポリシーが部分的に効いていたという事実そのものが、 K3sの内蔵コントローラが正常に動作している証拠でもある。問題はコントローラの有無ではなく、 別のところにあった。こうした「デフォルトで何が有効になっているか」を都度ドキュメントで確認する作業自体が、 自前でK3sクラスタを運用する上での地味なコストになる。Kubo のようなマネージド K3sであれば、この種のデフォルト構成の確認・検証はプラットフォーム側の責務として吸収できる。

真犯人はServiceLB。NetworkPolicy APIには「ノード自身からの通信」を遮断できないという制約がある

section02

Kubernetes公式ドキュメントのNetworkPolicyページ には、あまり知られていないが重要な一文がある。Podはlocalhostへのアクセスをブロックできず、 自分が乗っているノード自身からのアクセスも原理的にブロックする手段を持たない、という記述だ。 これはK3s固有の制約ではなく、Kubernetes の NetworkPolicy API そのものが持つ恒久的な仕様であり、 どのCNI・どのポリシーコントローラを使っても解消できない。

この仕様がK3sで特に顕在化しやすい理由は、K3sの標準LoadBalancer実装である ServiceLB(旧称 Klipper LB) の動き方にある。type: LoadBalancer のServiceを作成すると、ServiceLBは各ノード上に DaemonSetのPodをデプロイし、そのPodがノード自身のiptablesにDNATルールを書き込んで、 ホストポート宛のトラフィックをServiceのClusterIPへ転送する仕組みになっている (What does ServiceLB (Klipper LB) actually do? · Discussion #9927、 Networking Services | K3s)。

つまりLoadBalancer経由で届いたトラフィックは、Pod側から見ると**「ノード自身が転送してきた通信」**に 分類されうる。先述の「ノード資格の通信はブロックできない」という制約と組み合わさることで、 外部からLoadBalancer経由で届いたトラフィックに対して、NetworkPolicyのingress拒否ルールが 期待通りに機能しないケースが生まれる。実際に、あるK3sユーザーが ipBlock を使った拒否ルールで LoadBalancer Service へのアクセスを遮断しようとしたが効かなかった、という事例が K3sのIssue #7624 に報告されている。 同じ構成をAWS EKSで試したところ意図通りブロックできたという報告もあり、 LoadBalancerの実装方式がNetworkPolicyの実効性に影響することを裏付けている。

気づくための検証手順

section03

この種の問題は kubectl get networkpolicy を見ても異常なく表示され、apply もエラーなく通る。 静かに漏れるからこそ厄介だ。以下の手順で切り分けると気づきやすい。

Pod間通信とLoadBalancer経由通信を分けて確認する

  • 同一クラスタ内の別Podから 対象Podへ直接アクセスし、NetworkPolicyで遮断されることを確認する
  • クラスタ外から、LoadBalancer Service経由で 同じPodへアクセスし、遮断されるかどうかを比較する
  • 後者だけ通ってしまう場合、原因はNetworkPolicyの設定ミスではなく、経路自体がポリシー評価の対象外に なっている可能性が高い

ノード上のiptablesルールを確認する

kube-routerが書き込んだNetworkPolicy関連のiptablesルールと、ServiceLBが書き込んだDNATルールは 別々のチェーンに存在する。対象ノードで iptables-save を実行し、KUBE-ROUTER プレフィックスの チェーンと、ServiceLB(svc- プレフィックスのPodが管理する)DNATルールの両方を確認することで、 どちらの経路をトラフィックが通っているかを追跡できる。kube-router公式のトラブルシューティングガイド によれば、NetworkPolicyによって拒否されたトラフィックは iptables の NFLOG group 100 に 記録される仕組みになっており、実際に拒否ログが記録されているか・記録されていないのに通信が 成立しているかを見比べることでも、経路の切り分けができる。

対処法 — L7での制御、または別CNIへの切り替え

section04

選択肢1: L4のNetworkPolicyに頼らず、L7で制御する IngressコントローラやService Meshのアクセス制御機能を使えば、送信元IPやパスベースでより柔軟に 制限できる。ノード資格通信の制約を回避しつつ、外部公開エンドポイントへのアクセス制御を実現できる。

選択肢2: NetworkPolicy機能がより高度なCNIへ切り替える CalicoのGlobalNetworkPolicyなど、より高度なポリシーエンジンを持つCNIに切り替える方法もある。 その場合は --flannel-backend=none --disable-network-policy を明示的に指定し、内蔵のkube-router コントローラと競合しないようにする(Issue #1308)。

ただし切り替え時には注意点がある。network policy controllerを無効化しても、それまでに kube-routerが書き込んだiptablesルールは自動的にはクリーンアップされない (Issue #7244)。古いルールが残ったまま新しいCNIの ルールが追加されると、意図しない通信可否が発生しうる。k3s-killall.sh の実行や iptables-save/ iptables-restore による手動クリーンアップを、切り替え手順に必ず組み込む必要がある。 kube-routerのNetworkPolicyコントローラの実装詳細は kube-router公式ユーザーガイド、K3sにおける NetworkPolicy運用の基本的な考え方はSUSE/Rancher公式ブログ にもまとまっている。

こうしたCNI切り替え作業の手順そのものを個別のクラスタごとに確立し、iptablesの残骸まで含めて検証するのは 運用負荷が高い。Kubo Cloud はCaptain UIでCNI構成とNetworkPolicyの 強制状態を可視化した状態で提供しており、こうした切り替え判断を都度ゼロから検証する必要がない。

まとめ — 「デフォルトで安全」という思い込みを、経路ごとに検証する

K3sはNetworkPolicyを無視しない。それどころかkube-routerの内蔵コントローラによって、追加の設定なしで NetworkPolicyが機能する数少ないディストリビューションの一つだ。しかし「NetworkPolicyが有効」であることと 「すべての通信経路がその対象になっている」ことは別の話だ。今回のケースでは、K3s標準のServiceLBが ノードのiptables経由でトラフィックを転送する実装であることと、NetworkPolicy APIがノード資格通信を 原理的にブロックできないという恒久的な制約が重なったことで、外部公開したサービスへの通信だけが 静かに漏れていた。

こうした「経路ごとの見落とし」は、構成を自前で組み上げるほど発生しやすい。Kubo はK3sベースのマネージドKubernetesとして、CNI構成やNetworkPolicyの強制範囲を運用チームが個別に 検証しなくても済む状態で提供することを目指している。特にデータ主権やネットワーク隔離の要件が厳しい 環境では、Kubo On-Premise を使えば、 自社インフラ上でも同様に検証済みの構成をそのまま利用できる。「デフォルトで安全なはず」という前提を 経路単位で疑う習慣は、K3sに限らずKubernetes運用全般で有効な視点だ。ネットワーク構成の検証コストを 下げたい場合は、お問い合わせ から相談してみてほしい。

Related articles

← Back to all posts