Certified Kubernetes Security Specialist (CKS) #2: NetworkPolicy の深掘り: default deny、ingress/egress (Cluster Setup)

CKS #1 で試験環境と 6 つのドメインの全体像をつかみました。ここから最初のドメインである Cluster Setup に入ります。このドメインの核心は ネットワーク隔離 であり、その道具がまさに NetworkPolicy です。Kubernetes は基本的にすべての Pod が互いに自由に通信できるフラットなネットワークです。攻撃者が 1 つの Pod を掌握すると、クラスター全体への横移動 (lateral movement) が可能になるということです。NetworkPolicy はこのフラットなネットワークを小さな区画に切り分け、必要な通信だけを残してそれ以外を遮断するファイアウォールです。

NetworkPolicy の基本は CKA #20K8s 中級 #7 で扱いました。今回の記事はその上で、CKS が実際に要求する深さ、すなわち default deny の設計egress と DNS の落とし穴セレクター組み合わせの AND・OR の違い に集中します。

NetworkPolicy の基本動作からおさらい #

CKS で NetworkPolicy を正確に扱うには、まず 基本動作の 2 つ を取り違えないことが必要です。

ポリシーがなければ all-allow #

NetworkPolicy が 1 つもない名前空間では、すべての Pod があらゆる方向に自由に通信します。これが Kubernetes のデフォルト値です。隔離は明示的にオンにする機能であって、最初からオンになっているものではありません。

ポリシーが付いた Pod はホワイトリスト #

ある Pod に NetworkPolicy が 1 つでもマッチした瞬間、その Pod の 当該方向 (ingress または egress) のトラフィックはホワイトリスト方式 に変わります。ポリシーが明示的に許可したトラフィックだけが通り、それ以外はすべて遮断されます。ここで重要なルールが 2 つあります。

  • ポリシーは additive です。同じ Pod に複数のポリシーがマッチすると、各ポリシーが許可するトラフィックの 和集合 が許可されます。NetworkPolicy には deny ルール自体がなく、allow の和集合だけが存在します。
  • 1 つの Pod に ingress ポリシーだけが付くと、egress は依然として all-allow です。方向は独立して制御されます。そのため policyTypes フィールドでこのポリシーがどの方向を制御するのかを明確に宣言することが CKS の核心です。

この 2 つのルールから default deny パターンが出てきます。どのトラフィックも許可しないポリシーをすべての Pod にマッチさせると、和集合の出発点が「すべて遮断」になります。

default deny: すべて遮断してから始める #

CKS 試験と実務で最もよく使う出発点は、名前空間全体に default deny を敷き、必要な通信だけを別のポリシーで開けること です。podSelector: {} は名前空間の すべての Pod を選択し、policyTypes に遮断する方向を書きます。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: secure
spec:
  podSelector: {}          # 名前空間のすべての Pod に適用
  policyTypes:
    - Ingress
    - Egress
  # ingress/egress ルールがないので両方向すべて遮断

ingressegress キー自体がなければ、当該方向で許可されるトラフィックが 1 つもないという意味です。policyTypes に両方向を書いたので、この名前空間のすべての Pod は入ってくるトラフィックも出ていくトラフィックもすべて遮断されます。

方向ごとに別々にかけるパターンも知っておくと試験で役立ちます。

# ingress だけ default deny (egress はそのまま all-allow)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: secure
spec:
  podSelector: {}
  policyTypes:
    - Ingress

policyTypesIngress だけを書いて ingress ルールを空けておくと、入ってくるトラフィックはすべて遮断されつつ、出ていくトラフィックは制御しません。「この名前空間に入ってくるトラフィックだけをロックせよ」というタイプにそのまま使えます。

ingress 制限: 誰から入れるか #

default deny を敷いた後は、必要なトラフィックを開けるポリシーを追加します。ingress ルールの from送信元 を、ports到達ポート を指定します。送信元を選ぶセレクターは 3 種類です。

  • podSelector: 同じ名前空間の中で label で送信元 Pod を選ぶ
  • namespaceSelector: 特定の label を持つ名前空間全体を送信元として選ぶ
  • ipBlock: CIDR で IP 帯域を選ぶ。クラスター外の送信元に使用
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: secure
spec:
  podSelector:
    matchLabels:
      app: api          # このポリシーは app=api Pod へ入ってくるトラフィックを制御
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend   # app=frontend Pod から来るトラフィックだけ
      ports:
        - protocol: TCP
          port: 8080

このポリシーは app=api Pod に対して、同じ名前空間の app=frontend Pod から TCP 8080 で入ってくるトラフィックだけ を許可します。default deny が一緒に敷かれていれば、それ以外のすべての ingress は遮断された状態なので、この 1 行がそのままホワイトリストになります。

egress 制限: DNS の落とし穴 #

egress ルールは to で到達先を、ports で到達ポートを指定します。構造は ingress と対称ですが、egress を default deny で遮断した瞬間にほぼすべての通信が壊れる落とし穴 が 1 つあります。まさに DNS です。

Pod が api.secure.svc.cluster.local のような Service 名で通信するには、まずその名前を IP に変える DNS 照会をしなければなりません。この照会は CoreDNS へ向かう 53 番ポート (UDP と TCP) のトラフィックです。egress default deny を敷くとこの DNS トラフィックまで遮断されてしまい、Pod はどの名前も解決できず、すべての接続が失敗します。宛先 IP を直接許可していても、名前を IP に変えられなければ意味がありません。

そのため egress をロックするときは、DNS の許可を一緒に入れることが事実上必須 です。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: secure
spec:
  podSelector: {}          # 名前空間のすべての Pod
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns      # CoreDNS Pod
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

kubernetes.io/metadata.name は Kubernetes がすべての名前空間に自動で付ける label なので、CoreDNS が住む kube-system を安全に選べます。DNS は通常 UDP 53 を使いますが、大きな応答は TCP 53 に切り替わるので、試験では 両方のプロトコルを許可する のが安全です。

ここに業務用の egress を別のポリシーで追加すると、「この名前空間は DNS と特定のバックエンドにだけ出られる」という隔離が完成します。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-to-db
  namespace: secure
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

default deny egress、allow-dns、allow-egress-to-db の 3 つのポリシーが一緒にマッチすると、app=api Pod の出ていくトラフィックは CoreDNS の 53 番と app=postgres の 5432 番 にだけ制限されます。和集合ルールのおかげで、このように小さなポリシーを積み上げる設計が可能です。

セレクター組み合わせ: AND vs OR の落とし穴 #

CKS で最も間違いが多い箇所が namespaceSelectorpodSelector を一緒に使うときの意味です。1 つの項目の中に 2 つのセレクターを一緒に書くと AND、別々の項目に分けると OR です。YAML のインデント 1 つの差で意味が正反対になります。

# (A) AND: prod 名前空間「の中にいながら」app=client な Pod からだけ許可
ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            env: prod
        podSelector:
          matchLabels:
            app: client

上は from リストの 1 つの要素 の中に 2 つのセレクターが入っています。そのため「env=prod 名前空間に属しながら同時に app=client label を持つ Pod」という積集合の条件になります。ダッシュ (-) が 1 つだけであることに注目します。

# (B) OR: prod 名前空間のすべての Pod「または」どこであれ app=client な Pod から許可
ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            env: prod
      - podSelector:
          matchLabels:
            app: client

上は from2 つの要素 が入っているので、「env=prod 名前空間のすべての Pod」または「現在の名前空間で app=client な Pod」のどちらかが合うだけで許可されます。意図よりはるかに広く開いてしまうよくある事故なので、ダッシュの数で AND なのか OR なのかを必ず確認します。

ipBlock と except #

ipBlock は CIDR 帯域を許可しつつ、except でその中の一部を再び除外できます。

ingress:
  - from:
      - ipBlock:
          cidr: 10.0.0.0/16
          except:
            - 10.0.5.0/24      # このサブネットだけ除外

10.0.0.0/16 全体を許可しつつ 10.0.5.0/24 は遮断します。特定の信頼帯域だけを開けつつ、その中の危険な区間をくり抜くときに使います。except の帯域は必ず cidr の範囲内に含まれていなければなりません。

試験常連シナリオ 2 つ #

CKS で繰り返し出る NetworkPolicy のタイプは定型化されています。手が先に覚えるよう、2 つのタイプを整理します。

タイプ 1: 名前空間を隔離しつつ DNS だけ許可 #

secure 名前空間を外部と隔離しつつ、DNS 解決はできるようにせよ」というタイプです。default deny と allow-dns の 2 つのポリシーを一緒に適用します。上で作った default-deny-allallow-dns の組み合わせがまさにこの答えです。egress を遮断するときに DNS を一緒に開けるのを忘れないことが採点ポイントです。

タイプ 2: 特定の label からだけ ingress #

app=db Pod には app=app Pod からだけ接続できるようにせよ」というタイプです。app=db を対象に default-deny-ingress を敷き、app=app から来る ingress だけを開けるポリシーを追加します。

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-app-to-db
  namespace: secure
spec:
  podSelector:
    matchLabels:
      app: db
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: app

ports を省略するとすべてのポートが許可されるので、問題でポートを指定していたら必ず ports を入れます。

検証: 通信テストで確認する #

ポリシーを適用したら、実際に遮断されるか・開くかをテスト しなければなりません。採点はポリシーの効果で行われるので、YAML だけ合っていて動作が異なれば点数はありません。使い捨ての Pod で直接接続を試みます。

# 適用されたポリシーの一覧を確認
kubectl get networkpolicy -n secure

# 特定のポリシーの詳細ルールを確認
kubectl describe networkpolicy default-deny-all -n secure
# secure 名前空間から一時的な Pod で api Service に接続を試みる
kubectl run test -n secure --rm -it --image=busybox --restart=Never -- \
  wget -qO- --timeout=3 http://api:8080

# DNS 解決だけを別に確認
kubectl run test -n secure --rm -it --image=busybox --restart=Never -- \
  nslookup api

wget がタイムアウトすれば ingress が遮断されており、応答が来れば開いています。nslookup が失敗すれば DNS egress が遮断されているという合図なので、allow-dns ポリシーを点検します。ただし、一時的な Pod にポリシーがマッチするには、その Pod の label と名前空間がポリシーの条件に合っていなければならないので、送信元の label を正確に合わせて立ち上げることが重要です。

1 つ注意する点は、NetworkPolicy を 実際に強制するのは CNI プラグイン だという事実です。Calico、Cilium のようなポリシー対応 CNI が入っていてこそ効果が出ます。一部の環境ではポリシーを適用しても制御が起きないことがあるので、試験環境の CNI が NetworkPolicy をサポートしているという前提の上で作業します。

試験ポイント #

  • 基本動作。ポリシーがなければ all-allow。ポリシーがマッチすれば当該方向はホワイトリスト。NetworkPolicy に deny ルールはなく、allow の和集合だけが存在します。
  • default denypodSelector: {} ですべての Pod を選び、policyTypes に遮断する方向を書きます。ingressegress キーがなければその方向はすべて遮断です。
  • DNS の落とし穴。egress を default deny で遮断すると 53 番ポートが遮断され、名前解決が壊れます。allow-dns ポリシーで kube-system の CoreDNS に UDP・TCP 53 を一緒に開けます。
  • AND vs ORfromto の 1 つの項目の中に 2 つのセレクターを書くと AND、別々の項目に分けると OR です。ダッシュの数で見分けます。
  • ipBlock・except。CIDR で外部帯域を許可し、except でその中の一部を除外します。
  • 検証。一時的な Pod で通信を直接テストします。強制する主体は CNI プラグインです。

まとめ #

NetworkPolicy はフラットな Kubernetes ネットワークを区画に切り分けるホワイトリストファイアウォールです。default deny ですべて遮断し、必要な通信だけを小さなポリシーで足していく設計 が CKS の正解パターンです。egress を遮断するときは DNS を一緒に開けるのを忘れず、セレクターを組み合わせるときは AND と OR をインデントで正確に区別し、最後には一時的な Pod で効果を検証します。この 3 つさえ手に馴染めば、Cluster Setup のネットワーク隔離の作業は安定して点数になります。

次へ: CIS benchmark #

ネットワーク隔離を押さえたので、次はクラスター自体の設定を点検する番です。

#3 CIS benchmark (kube-bench)、コンポーネントセキュリティ、Ingress TLS、バイナリ検証 では、kube-bench で CIS benchmark を自動点検する方法、API server・kubelet・etcd のようなコンポーネントのセキュリティ設定を点検項目に合わせて直す方法、Ingress に TLS を適用する方法、そしてダウンロードしたバイナリのハッシュ・署名を検証する方法まで、直接実行しながら整理します。

X