本記事には広告(アフィリエイトリンク)が含まれます。

NetworkPolicyで通信制御|CKAD第16回

広告

新卒インフラエンジニア向け「Kubernetes 実践教科書① CKAD アプリケーション開発編」の第16回です。第15回で RBAC と SecurityContext を使って fanclub-api の Backend を「誰が操作できるか」「どう制限して動かすか」の面から守りました。今回はNetworkPolicyで「どの Pod がどの Pod と通信できるか」を絞ります。既定では素通しの Pod 間通信を default-deny(全拒否)から始め、必要な経路だけを段階的に許可して、fanclub-api の通信を最小限に閉じ込めます。CKAD ドメイン D5 の要です。

動作確認バージョン:kind v0.31.0(K8s v1.35.0)/ kubectl v1.35.6 / fanclub-backend:0.3.2(再ビルドなし)/ Calico v3.32.0(NetworkPolicy をエンフォース)/ 疎通確認は Frontend Pod 内蔵の wget(busybox)(2026-07-03 実機検証時点)

広告

今ここマップ(全 19 回中の現在地)

現在地は第 5 部「セキュリティ基礎」の第 16 回、第 5 部の締めくくりです。前回の「守り」に、通信経路の「絞り込み」を足します。

  • 第 1 部 コンテナと Docker(第 1〜4 回)
  • 第 2 部 Kubernetes 基礎(第 5〜6 回)
  • 第 3 部 アプリリソース(第 7〜11 回)
  • 第 4 部 ワークロード戦略(第 12〜14 回)
  • 第 5 部 セキュリティ基礎(第 15〜16 回)← 今ここ
  • 第 6 部 パッケージ管理と HTTPS 公開(第 17〜19 回)

この回のゴール

  • NetworkPolicy で Pod 間通信(Ingress / Egress)をホワイトリスト制御できる。
  • default-deny-all のベースラインから、必要な経路だけを段階的に許可して 3 層構成を閉じ込められる。
  • Egress を絞ると DNS が壊れる落とし穴を理解し、allow-dns で回避できる。

NetworkPolicy とは(どの Pod と通信できるか)

NetworkPolicy は、Pod 単位で「入ってくる通信(Ingress)」と「出ていく通信(Egress)」を、送信元・宛先・ポート(L3/L4)でホワイトリスト制御する仕組みです。ポイントは 2 つです。

  • ポリシーが 1 つも無い Pod は「全許可」。既定では、どの Pod もどことでも通信できます。
  • ある Pod を podSelector で対象にするポリシーを 1 つでも適用すると、その Pod は「許可したもの以外は拒否(default-deny)」に切り替わる。許可を明示的に足していく方式です。

重要な前提として、NetworkPolicy を実際に効かせる(エンフォースする)のは CNI プラグインです。本シリーズは第5回Calico v3.32.0 を導入済みで、これが NetworkPolicy を確実にエンフォースします(kind 既定の CNI は NetworkPolicy のエンフォースが不確実なため、本シリーズでは第5回から Calico を採用しています。CKAD 本番試験や第2巻の kubeadm 環境も NetworkPolicy をエンフォースする CNI 前提です)。

fanclub-api は Frontend(Nginx)→ Backend(Payara)→ DB(PostgreSQL)の 3 層です。目標は「隣り合う層だけが通信でき、それ以外は塞ぐ」状態です。まず現状のラベルを確認します。NetworkPolicy の podSelector はこのラベルで対象を指定します。実行コマンド:

$ kubectl get pods -L app

実行結果(例):各 Pod に app ラベル(fanclub-frontend / fanclub-backend / fanclub-db)が付いています。これを selector に使います。第11回で入れたログ収集の fanclub-logcollector も同じ default にいますが、本回は 3 層の通信制御に集中します。

NAME                               READY   STATUS    RESTARTS   AGE   APP
fanclub-backend-6c54b488d5-j4g6j   2/2     Running   0          9h    fanclub-backend
fanclub-db-0                       1/1     Running   0          5d    fanclub-db
fanclub-frontend                   1/1     Running   0          5d    fanclub-frontend
fanclub-logcollector-km8bb         1/1     Running   0          5d    fanclub-logcollector
fanclub-api の 4 層ホワイトリスト図。Gateway→Frontend→Backend→DB の順に隣接層だけが通信でき、各経路は宛先の Ingress 許可と送信元の Egress 許可の両方で開ける。Gateway→Frontend は第18回で追加予定のため破線で示す。
図1:fanclub-api の通信ホワイトリスト(隣接層のみ許可・Gateway→Frontend は第18回で追加)

default-deny から始める

ホワイトリストの土台として、まず「全部拒否」のベースラインを敷きます。podSelector: {}(空=Namespace 内の全 Pod が対象)で、Ingress と Egress の両方を対象にします。なお本回で作る NetworkPolicy マニフェスト(この後の np-*.yaml 計 4 ファイル)は、いずれも developer のホームに一般ユーザーで作成します(kubectl apply はクラスタへ送るだけなので root は不要)。まずは全拒否のベースライン np-default-deny.yaml です。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: default
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]

ingress / egress のルールを 1 つも書かないので、「許可ゼロ=全拒否」になります。適用し、Frontend から Backend への通信を試します。Frontend Pod(Nginx)には wget(busybox 由来)が入っているので、そこから確認します。Frontend の Pod 名を変数 $FE に入れておき、以降のコマンドでも同じターミナルで使い回します。実行コマンド:

$ kubectl apply -f np-default-deny.yaml
$ FE=$(kubectl get pod -l app=fanclub-frontend -o name | head -1)
$ kubectl exec "$FE" -- wget -q -O- --timeout=5 \
    http://fanclub-backend:8080/health/ready

実行結果(例):通信できず、しかもbad address(名前解決の失敗)で止まります。

wget: bad address 'fanclub-backend:8080'

ここが最初の勘所です。default-deny-allEgress(出ていく通信)も止めたため、Frontend が DNS サーバー(CoreDNS)へ問い合わせる通信まで塞がれfanclub-backend という名前を IP に解決できなくなりました。「接続が拒否された」より手前の、名前解決の段階で失敗しているわけです。次の節でこれを直します。

Egress と DNS の落とし穴

Egress を絞ったときに陥りやすいのが、このDNS の落とし穴です。Pod はサービス名でアクセスするたびに DNS(kube-system の CoreDNS・53 番ポート)へ問い合わせます。Egress を default-deny にした時点で、この問い合わせも拒否されます。そこで、全 Pod に「CoreDNS 宛の 53 番だけは許可」を足します。次を np-allow-dns.yaml として作成します。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: default
spec:
  podSelector: {}
  policyTypes: ["Egress"]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
      podSelector:
        matchLabels:
          k8s-app: kube-dns
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

namespaceSelectorkube-system)と podSelectork8s-app: kube-dns)の両方を 1 つの to 要素にまとめている点に注目してください。こうすると「kube-system かつ CoreDNS の Pod」という AND 条件になります(別々の - に分けると OR になり、範囲が広がりすぎます)。なお kubernetes.io/metadata.name ラベルは Kubernetes が全 Namespace に自動付与するものなので、自分で付け直す必要はありません。CoreDNS 側の k8s-app: kube-dns ラベルは環境によって異なることがあるため、kubectl get pod -n kube-system --show-labels で実値を確認しておくと確実です(kind は k8s-app: kube-dns)。適用して、もう一度 Backend にアクセスします。実行コマンド:

$ kubectl apply -f np-allow-dns.yaml
$ kubectl exec "$FE" -- wget -q -O- --timeout=5 \
    http://fanclub-backend:8080/health/ready

実行結果(例):エラーが bad address から download timed out(タイムアウト)に変わりました。

wget: download timed out

変化が意味を持ちます。名前解決は成功する(=DNS は通った)ようになり、その先の Backend への接続で止まっている状態です。DNS の落とし穴は解消し、あとは通したい経路を許可するだけになりました。

1 つの通信を通すには宛先の Ingress 許可と送信元の Egress 許可の両方が要ることを示す図。Frontend の Egress 許可と Backend の Ingress 許可が揃って初めて Frontend から Backend への矢印が通る。片方だけでは通らない。
図2:1 経路=Ingress 許可+Egress 許可のペア(default-deny 下では両方そろって初めて通る)

段階的に許可する(3 層ホワイトリスト)

ここが本回で最も大切な考え方です。default-deny-allIngress と Egress の両方を止めているので、1 つの経路を通すには「宛先側の Ingress 許可」と「送信元側の Egress 許可」の両方が要ります。片方だけでは通りません。まず Frontend → Backend を開けます。次を np-allow-backend.yaml として作成します。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backend-from-frontend
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: fanclub-backend
  policyTypes: ["Ingress"]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: fanclub-frontend
    ports:
    - protocol: TCP
      port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-frontend-to-backend
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: fanclub-frontend
  policyTypes: ["Egress"]
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: fanclub-backend
    ports:
    - protocol: TCP
      port: 8080

1 枚目が「Backend は Frontend からの 8080 を受け入れる(Ingress)」、2 枚目が「Frontend は Backend の 8080 へ出てよい(Egress)」です。適用して確認します。実行コマンド:

$ kubectl apply -f np-allow-backend.yaml
$ kubectl exec "$FE" -- wget -q -O- --timeout=5 \
    http://fanclub-backend:8080/health/ready

実行結果(例):UP が返り、Frontend → Backend が通りました。

{"status":"UP","checks":[{"name":"fanclub-api-ready","status":"UP","data":{}}]}

補足:Service(ClusterIP)経由でも NetworkPolicy が効く理由fanclub-backend は ClusterIP の Service なので、Frontend は Pod へ直接ではなく Service 宛てに通信します。それでも podSelector のポリシーが効くのは、NetworkPolicy が対象にするのが Service ではなく Pod(の IP)だからです。Frontend が Service 名でアクセスすると、DNS で ClusterIP を得たあと、kube-proxy が ClusterIP を実際の Backend Pod の IP に変換(DNAT)します。この変換は Calico がポリシーを判定するより手前で起きるため、Calico は変換後の Pod IPで可否を決めます。結果、Egress 許可(to podSelector: app=fanclub-backend)は変換後の Backend Pod に、Ingress 許可(from podSelector: app=fanclub-frontend)は送信元の Frontend Pod にマッチします。to/from に Service を書けず必ず Pod をラベルで選ぶのは、このためです。

Service(ClusterIP)経由でも NetworkPolicy が効く仕組みの図。Frontend Pod が Service 名で送ると、CoreDNS で ClusterIP を得(allow-dns)、kube-proxy が ClusterIP を Backend Pod IP に DNAT する。NetworkPolicy は変換後の Pod IP で Egress と Ingress を判定するため、podSelector の許可がそのまま効く。
図3:Service 経由の通信も NetworkPolicy は Pod IP で判定(kube-proxy の DNAT で ClusterIP→Pod IP に変換)

同じ要領で Backend → DB(PostgreSQL・5432)も開けます。次を np-allow-db.yaml として作成します。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-db-from-backend
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: fanclub-db
  policyTypes: ["Ingress"]
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: fanclub-backend
    ports:
    - protocol: TCP
      port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-backend-to-db
  namespace: default
spec:
  podSelector:
    matchLabels:
      app: fanclub-backend
  policyTypes: ["Egress"]
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: fanclub-db
    ports:
    - protocol: TCP
      port: 5432

適用して、今度はアプリ全体(Frontend → Backend → DB)が通るかを、DB 参照を伴う /api/members で確認します。実行コマンド:

$ kubectl apply -f np-allow-db.yaml
$ kubectl exec "$FE" -- wget -q -O- --timeout=6 \
    http://fanclub-backend:8080/api/members

実行結果(例):会員一覧の JSON が返り、3 層すべてが必要な経路だけで通信できています。

[{"createdAt":"2026-06-27T10:28:41.933973","email":"taro@example.com","id":1,"name":"山田太郎","plan":"premium"}, ...]

これで default-deny-allallow-dns +(Frontend↔Backend の 2 枚)+(Backend↔DB の 2 枚)の計 6 枚で、隣接層だけが通信できる状態になりました。外部(ブラウザ)→ Frontend を通す allow-frontend-from-gateway は、Gateway API を導入する第18回で追加し、4 層のホワイトリストが完成します(今回はまだ Gateway が無いので追加しません)。

適用順序のベストプラクティス

本回では学習のため deny を先に入れて「壊れる様子」を観察しましたが、本番稼働中のクラスタでは順序が逆です。default-deny を先に入れると、許可を足し終えるまでの間、通信が止まるダウンタイムの窓が生まれます。したがって本番では次の順序が安全です。

  1. 先に allow-dns と各 allow-*(必要な経路の Ingress / Egress ペア)をすべて適用する。
  2. 通信が維持されていることを確認する。
  3. 最後に default-deny-all を適用して、許可していない通信だけを閉じる。

「許可を先に、拒否を最後に」——これでダウンタイムなしにホワイトリストへ移行できます。

やってみよう

  1. default-deny-all を適用し、Frontend から wget http://fanclub-backend:8080/health/readybad address で失敗する(DNS も止まる)ことを観察する。
  2. allow-dns を適用し、エラーが download timed out に変わる(名前解決は復旧・接続は未許可)ことを確認する。
  3. allow-backend-from-frontendallow-egress-frontend-to-backend を適用し、UP が返ることを確認する(Ingress と Egress の両方が要る点を確かめる)。
  4. allow-db-from-backendallow-egress-backend-to-db を適用し、/api/members が会員 JSON を返す(3 層が通る)ことを確認する。

理解度チェック

次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。

  1. NetworkPolicy が 1 つも無い Namespace では、Pod 間通信はすべて許可される。
  2. ある Pod を podSelector の対象にするポリシーを 1 つ適用すると、その Pod は許可した通信以外が拒否される。
  3. Egress を default-deny にした状態では、宛先側に Ingress 許可があれば、送信元側に Egress 許可が無くても通信できる。
  4. Egress を絞ると、53 番(DNS)への許可を入れない限りサービス名での名前解決が失敗する。
  5. podSelector: {}(空)は、その Namespace 内の全 Pod を対象にする。
  6. NetworkPolicy を実際にエンフォースするのは CNI で、Calico は対応している。
  7. 本番稼働中のクラスタでは、default-deny-all を先に適用してから許可ルールを足すのが安全な順序である。
  8. allow-dns の宛先は kube-system の CoreDNS(k8s-app: kube-dns)で、ポートは 53(UDP / TCP)である。

解答

  • 1. ○:ポリシー不在の Pod は全許可。
  • 2. ○:対象になった時点で default-deny 化し、許可を明示した通信だけ通る。
  • 3. ×:default-deny は Ingress・Egress の両方を止めるため、送信元の Egress 許可と宛先の Ingress 許可の両方が要る。
  • 4. ○:DNS 問い合わせも Egress。53 番を許可しないと名前解決が落ちる。
  • 5. ○:空の podSelector は Namespace 内の全 Pod が対象。
  • 6. ○:エンフォースは CNI の役割。本シリーズは Calico(第5回導入)。
  • 7. ×:逆。許可を先に入れ、default-deny は最後にするとダウンタイムを避けられる。
  • 8. ○:CoreDNS(k8s-app: kube-dns)宛の 53(UDP / TCP)を許可する。

まとめ

本記事では、NetworkPolicy で Pod 間通信をホワイトリスト制御しました。default-deny-all で全拒否のベースラインを敷き(Ingress・Egress 両方)、Egress を絞ると DNS が壊れる落とし穴allow-dns(CoreDNS 宛 53)で回避し、1 経路=宛先 Ingress 許可+送信元 Egress 許可のペアという原則で Frontend↔Backend・Backend↔DB を段階的に開けて、3 層を必要な経路だけに閉じ込めました。本番では「許可を先に・拒否を最後に」でダウンタイムを避けます。外部→Frontend の許可は Gateway 導入の第18回で足して 4 層が完成します。

第 5 部を通じて、fanclub-api は RBAC(最小権限の ServiceAccount で「何をできるか」を絞る・第15回)・SecurityContext(非 root・読み取り専用ルートFS・capability 剥奪で「どう動くか」を固める・第15回)・NetworkPolicy(default-deny から必要な経路だけ許可して「誰と通信できるか」を絞る・第16回)の 3 層で守られました。権限・実行環境・通信をそれぞれ最小化する多層防御が、これでアプリの土台になります。次の第 6 部では、この状態のアプリをパッケージ化して公開まで進めます。これで第 5 部「セキュリティ基礎」は終わりです。

次回予告

次回(第17回)からは第 6 部「パッケージ管理と HTTPS 公開」に入ります。ここまで手で kubectl apply してきた大量のマニフェストを、Helm v4 で 1 つの chart にまとめ、install / upgrade / rollback のフルサイクルを体験します。fanclub-api を「パッケージ」として扱えるようになります。

シリーズ一覧

第1部:コンテナと Docker

第2部:Kubernetes 基礎

第3部:アプリリソース

第4部:ワークロード戦略

第5部:セキュリティ基礎

第6部:パッケージ管理 + HTTPS 公開

広告
kubernetes
スポンサーリンク