NetworkPolicy — ラベルで描くファイアウォール
一言でいうと
Kubernetesのデフォルトは「すべてのPodがすべてのPodと通信できる」状態で、NetworkPolicyは、そのデフォルトをPod単位で覆す唯一の標準的な手段です。
なぜ必要なのか
Podが1つ突破されたとします。デフォルト設定のクラスターでは、そのPodはデータベースにも、決済サービスにも、内部の管理APIにも、そのまま接続できます。ファイアウォールがクラスターの外側の境界にしかなく、内側は平らな野原だからです。侵害が1つのPodで終わらず、横へ広がっていくこの現象を、ラテラルムーブメントと呼びます。
NetworkPolicyは、この野原に壁を立てます。ただし、壁の立て方が従来のファイアウォールとは違います。IPではなくラベルで対象を選びます。Podは落ちて再び起動するとIPが変わりますが、ラベルは変わらないからです。ポリシーがIPの代わりに「app=apiのPod」と言っていれば、オートスケールでPodが10個になっても、ノードが移っても、ポリシーはそのまま有効です。
どう動くのか
3つのルールを理解すれば、ほとんどの動作が説明できます。
1つ目は、ポリシーは受け取る側に付くことです。podSelectorはこのポリシーが適用されるPodを選び、ingress.fromはそのPodへの通信を許可する相手を選びます。webからapiへのトラフィックを許可したいなら、ポリシーのpodSelectorはapiでなければなりません。この方向を逆に取ってしまう間違いが、圧倒的によくあります。
2つ目は、ポリシーは許可リストだということです。あるPodを選ぶポリシーが1つでも作られた瞬間、そのPodのその方向のトラフィックは「ポリシーに書かれたものだけを許可」に変わります。そのため、デフォルト拒否は、ルールが1つもないポリシーで表現されます。
spec:
podSelector: {} # 네임스페이스의 모든 파드
policyTypes: [Ingress] # 인그레스에 대해
# ingress 규칙 없음 → 전부 거부
3つ目は、from配列の項目同士はOR、1つの項目内のセレクター同士はANDだということです。これが最も微妙な点です。
from:
- namespaceSelector: {matchLabels: {purpose: monitoring}}
podSelector: {matchLabels: {app: prom}} # ← 같은 항목: AND
これは「monitoringネームスペースのprom Pod」を意味しますが、ハイフンをもう1つ入れて項目を2つに分けると、「monitoringネームスペースのすべてのPod、または任意のネームスペースのprom Pod」になります。インデント1段の違いが、意味をまるごと変えてしまいます。
エグレスには、もう1つ落とし穴があります。エグレスをデフォルト拒否で塞ぐと、まずDNSが死にます。Podはサービス名をIPに変換するために、kube-systemのCoreDNSに問い合わせる必要がありますが、その経路が塞がれるからです。そのため、エグレスのデフォルト拒否には、常にDNSの例外がセットでついてきます。さらに、DNSが使うのはUDP 53だけではありません。応答が大きいとTCP 53に切り替わるので、2つのプロトコルの両方を開ける必要があります。
IP範囲で開けるipBlockもあります。cidrで広く開け、exceptで狭く除外する方式です。ただし、クラスター内部の通信にIPを使うのは、ラベルベースのポリシーの長所を捨てることになるため、オフィスのIP範囲や外部ゲートウェイのようなクラスター外のアドレスにだけ使うのがよいです。
現場での姿
1つ目は、CNIが対応していなければ、ポリシーは飾りにすぎないことです。NetworkPolicyオブジェクトは作成されても、実際の遮断はCNIプラグインが行います。Flannelのように対応していないCNIでは、ポリシーをいくら作成しても何も起こりません。筆者のホームラボはCiliumをeBPFモードで使っているので、L7ルールやHubbleによる観測まで可能です。このラボ環境はPod間で実際のトラフィックが流れないシミュレーションクラスターなので、採点はパケットが落とされるかどうかではなく、ポリシーを正確に書けているかとセレクターの意味を正しく理解しているかで行われます。文法と意味をここで身に付け、実際の遮断の検証は、CNIのある環境でHubbleやcalicoctlを使って行えばよいのです。
2つ目は、デフォルト拒否を先に入れてしまう事故です。実際にあった障害です。運用チームが本番環境にデフォルト拒否をテストなしで適用したところ、DNSの許可が抜けていました。すべてのサービスディスカバリが失敗し、readiness probeが連鎖的に失敗しました。正しい順序は、許可ポリシーを先にすべて配置し、デフォルト拒否を最後に入れることです。
3つ目は、ラベルの衛生管理です。ポリシーはラベルだけで対象を選ぶので、ラベルのないPodやネームスペースはポリシーの死角になります。ネームスペースを選ぶときによく使うkubernetes.io/metadata.nameは、Kubernetesがすべてのネームスペースに自動で付けてくれるラベルなので便利です。
次のラボですること
1つのネームスペースにweb・api・dbの3つのPodを置き、イングレスのデフォルト拒否から始めて、webからapiへの8080だけを開けるポリシー、エグレスのデフォルト拒否とDNSの例外、別のネームスペースから来るモニタリングのトラフィックの許可、IP範囲の許可と除外、そして双方向を1つのポリシーにまとめる総合問題まで作成します。最後に、どのPodにどのポリシーが適用されるかのマップをJSONで描きます。