新卒インフラエンジニア向け「Kubernetes 実践教科書 ③ CKS セキュリティ・ハードニング編」(全19回)の第3回です。第2回では kube-bench で CIS ベンチマークを評価し、設定の抜けを塞ぎました。本回はそこから「通信の経路を塞ぐ」へ進みます。ただし本回は白紙から始めません。fanclub Namespace には第2巻の時点で NetworkPolicy が 9 本入っており、default-deny-all も適用済みです。本回の到達点は、既にあるポリシーを疑って読み、穴を見つけて締められることです。実務でも試験でも、白紙から書き起こす場面より「渡されたポリシーを直す」場面のほうが多くあります。
第2回の予告では「default-deny を起点に最小許可を設計する」という言い方をしましたが、実機を確認すると設計の土台はすでに出来上がっていました。そこで本回は設計ではなく監査から始めます。ノードメタデータの遮断についても、fanclub はすでに egress の default-deny が効いていて到達できない状態にあるため、専用の Namespace を用意して except の効き方を検証する形を取ります。予告とのズレを隠さず説明することも、本回の主題(思い込みで判断しない)と地続きです。
- ラボ Kubernetes v1.36.2
- CKS 試験環境 v1.35
- Calico v3.32.0(NetworkPolicy を実際に適用する CNI)
- cert-manager v1.20.0
- Gateway API v1.4.0
- Traefik v3.6.x
- containerd v2.2.6
- AlmaLinux 10.2
- 確認日 2026-07-26
目次
- 第3回のスコープ・今ここマップ
- この回のゴール
- 起点の確認 ——fanclub には既に NetworkPolicy が 9 本ある
- NetworkPolicy の判定規則 ——許可リスト型・AND / OR・クロス Namespace
- やってみよう①:既存 9 本を棚卸しして「何が通るか」を表にする
- やってみよう②:過剰許可 2 件を締める
- egress 側の点検 ——DNS 許可と「広く許可しないこと」の価値
- ノードメタデータ(169.254.169.254)の保護
- やってみよう③:except が効いていることを到達できる宛先で証明する
- TLS Ingress ——試験の標準 Ingress とラボの Gateway API
- やってみよう④:試験形式で TLS Ingress 一式を組み立てる
- 暗記必須コマンド(第3回)
- まとめ
- 理解度チェック(○×形式・全 9 問)
- 次回予告
第3回のスコープ・今ここマップ
本回は CKS ドメイン D1: Cluster Setup(配点 15%) の後半にあたります。D1 は 5 つのコンピテンシーからなり、第2回で「CIS ベンチマークによるレビュー」と「プラットフォームバイナリの検証」の 2 つを扱いました。本回は残る 3 つ——NetworkPolicy によるクラスタレベルのアクセス制限 / TLS 付き Ingress の設定 / ノードメタデータとエンドポイントの保護——を担当します。ここまで終えると D1 は完了します。
第3巻 19 回のうち、現在位置は次のとおりです。
第1部 第3巻オリエンテーション
第1回: 第3巻スコープ + CKA 境界 + 4C/脅威モデル + Kubestronaut
第2部 クラスタ堅牢化(D1/D2)
第2回: kube-bench + CIS ベンチマーク修復 + バイナリ検証(D1)
★ 第3回: NetworkPolicy 完全設計 + ノードメタデータ保護 + TLS Ingress(D1) ← 今ここ
第4回: RBAC 監査 + ServiceAccount トークン管理(D2)
第5回: API Server 堅牢化 + kubeadm アップグレード(D2)
起点となる環境は第2回の完了状態です。第2巻から引き継いだ 9 VM 構成(Control Plane Node 3 台 + Workload Node 2 台 + 作業端末・レジストリ・LB・プロキシ)が稼働し、CIS 修復を適用済みで、5 ノードが Ready・Kubernetes は v1.36.2 という前提で進めます。
NetworkPolicy は三部作で 3 回目の登場になる
NetworkPolicy そのものは、本シリーズで初めて扱うテーマではありません。同じ機能を 3 回に分けて積み上げてきたので、それぞれの文脈を先に整理します。
| 巻 | 既習内容 | 文脈 |
|---|---|---|
| 第1巻 第16回 | default-deny-all + 4 つの許可ポリシー + DNS 53/UDP の罠 + 適用順序 | 通信を成立させる |
| 第2巻 第10〜13回 | 詳細設計 + Calico / Cilium の比較 + トラブルシュート。現在 fanclub に入っている 9 本はここで揃った(chart 同梱の 7 本が第10回、Gateway 用の 1 本が第11回、監視スクレイプ用の 1 本が第13回) | 通信を設計する |
| 本回(第3巻 第3回) | 既存ポリシーの監査 / from・to の AND・OR の厳密理解 / ノードメタデータ保護 / TLS Ingress | 通信を疑う |
本回は「同じ話の 3 周目」ではありません。第1巻と第2巻は通信を通すためにポリシーを書きました。本回は通してしまっているものを見つけるためにポリシーを読みます。書く技術と読む技術は別物で、CKS が問うのは後者の比重が大きくなります。
この回のゴール
本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。
- 既存の NetworkPolicy 群を読み、「どの Pod へ、どこから、何が通るのか」を表に起こせる
from/toのリスト要素が AND になるか OR になるかを、書き方の違いで判別できるnamespaceSelector単独の許可が「Namespace の箱ごと開ける」ことだと説明でき、podSelectorとの AND で締められる- 締めた後に正規の通信が壊れていないことを確認する手順を持てる
ipBlockのcidrとexceptの関係を説明でき、ノードメタデータ(169.254.169.254/32)への egress を遮断できる- 「最も強い保護は広く許可しないこと」であり、
exceptが要るのは広い egress を許可せざるを得ない Pod だけだと説明できる - TLS Ingress を「試験の標準 Ingress」と「ラボの Gateway API HTTPRoute」の両形式で扱える
本回で扱わない範囲も先に確認します。NetworkPolicy に隣接するテーマは多いので、どこで回収するかを明示しておきます。
| 本回で扱わない領域 | 理由 | 回収する回 |
|---|---|---|
| L7 制御(HTTP メソッド / パス単位)・CiliumNetworkPolicy | 標準 networking.k8s.io/v1 の NetworkPolicy は L3/L4 のみ | 第11回 |
| Pod 間通信の暗号化(WireGuard 透過暗号化) | NetworkPolicy は「通す / 通さない」であって暗号化はしない | 第11回 |
| Namespace を「セキュリティ境界」として設計する話 | マルチテナンシーの文脈でまとめて扱う | 第10回 |
| RBAC(誰が NetworkPolicy を書き換えられるか) | 権限の話 | 第4回 |
| インシデント時の NetworkPolicy による緊急隔離 | 対応フローの中で扱う | 第18回 |
本回で扱う NetworkPolicy はすべて標準の networking.k8s.io/v1 です。本書ラボではこれを Calico v3.32.0 が実際に適用します。CiliumNetworkPolicy や NetworkSet のような CRD は本回では使いません。CKS 試験でも、明示がない限り標準のリソースで解答します。
起点の確認 ——fanclub には既に NetworkPolicy が 9 本ある
まず現状を見ます。監査は現物の確認から始まります。
実行コマンド(k8s-ops 上・developer):
$ kubectl get networkpolicy -n fanclub
実行結果:
NAME POD-SELECTOR AGE
allow-backend-from-frontend app=fanclub-backend 11h
allow-backend-from-monitoring app=fanclub-backend 11h
allow-db-from-backend app=fanclub-db 11h
allow-dns <none> 11h
allow-egress-backend-to-db app=fanclub-backend 11h
allow-egress-batch-to-db app=fanclub-batch 11h
allow-egress-frontend-to-backend app=fanclub-frontend 11h
allow-frontend-from-gateway app=fanclub-frontend 11h
default-deny-all <none> 11h
AGE は環境の稼働時間で変わります。並び順は名前のアルファベット順で、重要度順ではありません。土台である default-deny-all が最下行に来ているのは、たまたま名前が d で始まるからです。出力の並びから構造を読み取ろうとしない——これが監査の最初の注意点になります。
9 本の内訳を整理すると次のようになります。default-deny-all が土台にあり、そこへ 8 本の許可ポリシーが重なる構成です。
| ポリシー名 | POD-SELECTOR | 方向 | 許可内容 |
|---|---|---|---|
default-deny-all | <none>(podSelector: {}) | Ingress + Egress | なし(全遮断) |
allow-dns | <none> | Egress | kube-system の k8s-app: kube-dns へ UDP/TCP 53 |
allow-frontend-from-gateway | app=fanclub-frontend | Ingress | namespaceSelector: traefik から 80/TCP |
allow-backend-from-frontend | app=fanclub-backend | Ingress | podSelector: app=fanclub-frontend から 8080/TCP |
allow-backend-from-monitoring | app=fanclub-backend | Ingress | namespaceSelector: monitoring から 8080/TCP |
allow-db-from-backend | app=fanclub-db | Ingress | app=fanclub-backend または app=fanclub-batch から 5432/TCP |
allow-egress-frontend-to-backend | app=fanclub-frontend | Egress | backend へ 8080/TCP |
allow-egress-backend-to-db | app=fanclub-backend | Egress | db へ 5432/TCP |
allow-egress-batch-to-db | app=fanclub-batch | Egress | db へ 5432/TCP |
第2巻の到達点をまず認める
この 9 本は第2巻で段階的に揃えられたもので、設計としては正しい形です。第10回で chart 同梱の 7 本を設計し、第11回で Gateway 用の allow-frontend-from-gateway を、第13回で監視スクレイプ用の allow-backend-from-monitoring を足して 9 本になりました。default-deny を土台に置き、必要な経路だけを許可で足すという構造になっています。第2巻が「通信を成立させながら最小許可へ絞る」という課題を達成した結果がこれです。
ですから本回は、この 9 本を「間違い」として否定する回ではありません。第2巻と本回では文脈が違うだけです。第2巻は動かすために書き、本回は疑うために読みます。同じ Namespace の中に、締まっている書き方と粒度の粗い書き方が同居している——実務のポリシー群はたいていそうなります。だからこそ棚卸しが要ります。
「default-deny が入っている=安全」ではない
ここが本回の主題です。default-deny は「明示的に許可したものだけを通す」状態を作ります。しかし許可ルールの粒度が粗ければ、開けた穴はその粗さのまま残ります。default-deny は出発点であって到達点ではありません。CKS が問うのは「default-deny があるか」ではなく「開けた穴が必要最小限か」です。
この視点で既存ポリシーを読むために、本回を貫く問いを 3 つに固定します。以降のセクションはすべて、この 3 つに答える形で進みます。
- どの Pod が、どのポリシーに選択されているか(選択されていない Pod は素通し)
- 開けた穴は、通す必要がある相手だけに絞られているか(Namespace 単位で開けていないか)
- egress 側は何を許可しているか(広く許可しているなら、そこにメタデータ IP への穴が空いている)
自分で書いた覚えのないポリシーも動いている
NetworkPolicy は fanclub Namespace だけのものではありません。クラスタ全体を数えます。
実行コマンド(k8s-ops 上・developer):
$ kubectl get networkpolicy -A --no-headers | awk '{print $1}' | sort | uniq -c
実行結果:
6 argocd
9 fanclub
実測で確認できているのは、fanclub に 9 本、argocd に 6 本という 2 点です。argocd の 6 本は ArgoCD の Helm chart が同梱しているもので、読者が書いたものではありません。自分で書いた覚えがないポリシーが動いているという状態は、実務では普通に起きます。chart が良かれと思って入れてくれることもあれば、前任者が入れて引き継ぎ資料に載っていないこともあります。だからこそ、まず何があるかを数えるところから始めます。

NetworkPolicy の判定規則 ——許可リスト型・AND / OR・クロス Namespace
棚卸しの前に、読解に必要な判定規則を固めます。ここでは架空の app: a や ns-x ではなく、fanclub に実在する 9 本を教材として説明します。試験で問われるのも、この判定規則の理解そのものです。
許可リスト型の判定順序
- ある Pod に NetworkPolicy が 1 つでも適用されると、その Pod の対象方向(Ingress / Egress)は「いずれかのポリシーが明示的に許可した通信だけ」に絞られ、それ以外は遮断される
- 複数のポリシーは OR で重ね合わさる(許可が加算される)。「拒否ルール」は存在しないため、すでに 1 本以上のポリシーに選択されている Pod なら、そこへポリシーを足して通信が減ることはない。ただしまだどのポリシーにも選択されていない Pod に 1 本目を足すと、話は逆になる——その瞬間に「明示的に許可した通信だけ」へ切り替わり、書いていない通信は落ちる。
default-deny-allがこれを狙って全 Pod に 1 本目を当てている - 逆に、どの NetworkPolicy にも選択されていない Pod は全通信が許可されたままである
3 つ目が監査の入口になります。ポリシーが何本あるかより、選択されていない Pod がいないかを先に見ます。10 本のポリシーで守られた Namespace に、どのポリシーにも一致しない Pod が 1 つ紛れていれば、その Pod は 10 本すべてを迂回します。
「拒否ルールが存在しない」という性質も、監査では効いてきます。ポリシーを追加して問題が解決するのは「通したいものが通らない」ときだけです。「通ってはいけないものが通る」場合は、既存のどれかを狭めるしかありません。本回の中心である やってみよう② が「新しく書き足す」ではなく「既存を書き換える」形になっているのは、この性質があるためです。
default-deny の実体
fanclub の default-deny-all は次の内容です。実機から取得したものを、metadata.managedFields や status といった自動付与のフィールドを落として整形しています。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: fanclub
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
podSelector: {} は「この Namespace の全 Pod」を意味します。空のセレクタは「何も選択しない」ではなく「すべてを選択する」である、という点が最初の落とし穴です。そのうえで policyTypes に Ingress と Egress を書き、ingress / egress のルールを 1 つも書いていません。結果として両方向とも全遮断になります。
ここで policyTypes を書き忘れると、ルールを書いていない方向は制御対象になりません。policyTypes を省略した場合、ingress ルールの有無から Ingress は自動的に対象になりますが、Egress は egress ルールを書いたときにしか対象になりません。つまり policyTypes なしで podSelector: {} だけを書いたポリシーは、egress を一切制御しません。「default-deny を入れたつもりで ingress しか塞げていない」というのは、試験でも実務でもよくある取りこぼしです。
AND と OR の書き分け ——実在の 2 本で見る
CKS で最も時間を失うのがここです。インデントの深さだけが AND と OR を決めます。文章で覚えるのではなく、2 つの形を目で覚えてください。
まず OR の実例です。allow-db-from-backend の ingress 部分を抜き出します(AND / OR の対比が目的なので該当箇所のみ掲載します)。
ingress:
- from:
- podSelector:
matchLabels:
app: fanclub-backend
- podSelector:
matchLabels:
app: fanclub-batch
ports:
- protocol: TCP
port: 5432
- podSelector: が 2 つ並んでいます。- で並ぶリスト要素は OR なので、これは「backend または batch から 5432」という意味です。2 つ目の要素があるのは、CronJob fanclub-report(スケジュール 0 9 * * *)が生成する Pod がラベル app: fanclub-batch を持つためです。
次に AND の実例です。allow-dns の 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
こちらは - namespaceSelector: という 1 つのリスト要素の中に、podSelector: が同じインデントレベルで入っています。同一リスト要素の中に並んだセレクタは AND なので、「kube-system Namespace かつ k8s-app: kube-dns のラベルを持つ Pod へ 53」という意味になります。
2 つの YAML を見比べてください。違いは podSelector: の前に - があるかないか、それだけです。
| 書き方 | 意味 | 本回での実例 |
|---|---|---|
- namespaceSelector: の次の行に - podSelector:(- あり) | OR:その Namespace の全 Pod または 同一 Namespace の該当 Pod | 締める前に間違えやすい形 |
- namespaceSelector: の中に podSelector:(- なし・同一インデント) | AND:その Namespace に属し、かつ そのラベルを持つ Pod | allow-dns/やってみよう② で作る 2 本 |
- を 1 つ付けただけで、絞ったつもりのポリシーがむしろ広くなるという点に注意してください。OR は許可の加算なので、Namespace 全体の許可がそのまま残ります。やってみよう② で書き換える 2 本は、ここを間違えると「apply は成功するのに何も締まっていない」という結果になります。
podSelector 単独は同一 Namespace のみに一致する
from / to に podSelector だけを書くと、そのポリシーが存在する Namespace の Pod にしか一致しません。別 Namespace の相手(traefik の Gateway、monitoring の Prometheus)を許可したいのに podSelector だけを書くと、一致せず遮断されます。
allow-backend-from-frontend は frontend も backend も同じ fanclub Namespace にいるので、podSelector 単独で正しい書き方です。一方、traefik や monitoring を相手にするなら namespaceSelector が必要になります。同じ「Pod を指定する」でも、Namespace をまたぐかどうかで書き方が変わります。
この非対称性は試験でも狙われます。「別 Namespace の特定 Pod からだけ許可せよ」という問題に podSelector 単独で答えると通信は通らず、namespaceSelector 単独で答えると通りすぎます。正解は両方を AND で書くことです。
Namespace の指定には kubernetes.io/metadata.name を使う
kubernetes.io/metadata.name は、API Server が全 Namespace へ自動付与するラベルです(v1.22 で GA)。値は Namespace 名そのものなので、namespaceSelector でこれを使えば、Namespace に手でラベルを付けなくても名前で指定できます。実際のラベルを確認します。
実行コマンド(k8s-ops 上・developer):
$ kubectl get namespace traefik monitoring kube-system --show-labels
実測値を LABELS 列だけ要約すると次のとおりです。
| Namespace | ラベル |
|---|---|
traefik | kubernetes.io/metadata.name=traefik / name=traefik |
monitoring | kubernetes.io/metadata.name=monitoring / name=monitoring |
kube-system | kubernetes.io/metadata.name=kube-system のみ |
traefik と monitoring には name= という手付けのラベルもあります。どちらを使ってもポリシーは書けますが、手付けのラベルは消えることも変わることもあります。Helm chart の更新や、Namespace の再作成で失われます。kubernetes.io/metadata.name は API Server が管理するので消えません。ポリシーでは後者を使う——これを本書の方針とします。
クラスタ外を指す ipBlock
podSelector と namespaceSelector はクラスタ内の相手を指すセレクタです。クラスタ外の宛先(外部 API・コンテナレジストリ・ノードメタデータ)は指定できません。それらは ipBlock の CIDR で書きます。
ipBlock には cidr と except の 2 つのフィールドがあり、except に書く CIDR は同じ ipBlock の cidr に含まれている必要があります。含まれない値を書くと API Server が拒否します。記法と意味論はノードメタデータ保護のセクションで詳しく扱うので、ここでは「クラスタ外は ipBlock」という対応関係だけ押さえてください。
やってみよう①:既存 9 本を棚卸しして「何が通るか」を表にする
この演習を始める前に
- 本演習は読み取りのみです。クラスタの状態は変えません
- 所要時間の目安は 10〜15 分です
- 起点は第2回完了状態のクラスタです。fanclub の Pod がすべて
Runningになってから始めてください
CKS 実技の第一歩は、渡されたポリシーを読むことです。書く前に読めるようになってください。
ステップ1:一覧を取り、対象 Pod ごとに束ねる
kubectl get networkpolicy の既定の出力は名前・セレクタ・経過時間の 3 列だけで、方向が分かりません。-o custom-columns で必要な列を足します。
実行コマンド(k8s-ops 上・developer):
$ kubectl get networkpolicy -n fanclub -o custom-columns='NAME:.metadata.name,POD:.spec.podSelector.matchLabels,TYPES:.spec.policyTypes'
実行結果:
NAME POD TYPES
allow-backend-from-frontend map[app:fanclub-backend] [Ingress]
allow-backend-from-monitoring map[app:fanclub-backend] [Ingress]
allow-db-from-backend map[app:fanclub-db] [Ingress]
allow-dns <none> [Egress]
allow-egress-backend-to-db map[app:fanclub-backend] [Egress]
allow-egress-batch-to-db map[app:fanclub-batch] [Egress]
allow-egress-frontend-to-backend map[app:fanclub-frontend] [Egress]
allow-frontend-from-gateway map[app:fanclub-frontend] [Ingress]
default-deny-all <none> [Ingress Egress]
POD 列が map[...] というかたちで出るのは、matchLabels がマップ(キーと値の対)だからです。<none> の 2 本(allow-dns と default-deny-all)は podSelector: {} で、Namespace の全 Pod が対象という意味になります。TYPES 列を見ると default-deny-all だけが [Ingress Egress] の両方向で、残りは片方向であることも読み取れます。
この出力で「どのポリシーが、どのラベルの Pod の、どちらの方向を制御しているか」が一望できます。TYPES 列に Egress を含む行を数えると、egress を制御しているポリシーの本数が分かります。
ステップ2:1 本ずつ中身を読む
次に個別のポリシーを読みます。まず describe で当たりを付けます。
実行コマンド(k8s-ops 上・developer):
$ kubectl describe networkpolicy allow-frontend-from-gateway -n fanclub
実行結果:
Name: allow-frontend-from-gateway
Namespace: fanclub
Created on: 2026-07-26 03:03:09 +0900 JST
Labels: app.kubernetes.io/managed-by=Helm
velero.io/backup-name=fanclub-backup
velero.io/restore-name=fanclub-backup-20260726030303
Annotations: meta.helm.sh/release-name: fanclub
meta.helm.sh/release-namespace: fanclub
Spec:
PodSelector: app=fanclub-frontend
Allowing ingress traffic:
To Port: 80/TCP
From:
NamespaceSelector: kubernetes.io/metadata.name=traefik
Not affecting egress traffic
Policy Types: Ingress
読むべきは From: のブロックです。ここに NamespaceSelector の 1 行しかありません。PodSelector の行が並んでいないことが、あとで問題になります。
Not affecting egress traffic という行にも注目してください。このポリシーは policyTypes に Ingress しか書いていないので、egress については何も言っていないという意味です。「egress を許可している」でも「egress を遮断している」でもありません。遮断しているのは default-deny-all のほうです。
Labels と Annotations に Helm と Velero の痕跡が残っているのは、このポリシーが fanclub chart の一部として管理され、第2巻第14回のバックアップ・リストア演習を経ているためです。手で書き換えると次の helm upgrade で元に戻ります——本回の演習では手で直しますが、本番では chart 側を直すのが筋である、という点は後段のガードレールで扱います。
読み方を決めておきます。Allowing ingress traffic: の下に From: と To Port: が並びます。NamespaceSelector だけが出ていて PodSelector が出ていないなら、それは Namespace 全体を許可しているということです。ここが本回の勘所になります。
ただし describe には限界があります。describe は AND と OR をインデントで表現しません。複数のセレクタが並んでいるとき、それが AND なのか OR なのかを describe の出力から確実に判別することはできません。したがって「describe で当たりを付け、-o yaml で確定する」という 2 段構えで読みます。
実行コマンド(k8s-ops 上・developer):
$ kubectl get networkpolicy allow-frontend-from-gateway -n fanclub -o yaml
この使い分けは試験でも効きます。時間が限られる中で 9 本すべてを -o yaml で精読する余裕はありません。describe で怪しい 1〜2 本に絞ってから YAML を開きます。
ステップ3:選択されていない Pod がいないかを確認する
判定規則の 1 つ目の問いに答えます。default-deny-all は podSelector: {} なので fanclub の全 Pod を選択しています。したがって素通しの Pod は存在しません。ただし「存在しないはずだ」で終わらせず、実際の Pod とラベルを見て確認します。
実行コマンド(k8s-ops 上・developer):
$ kubectl get pod -n fanclub --show-labels
実行結果(LABELS 列のみ抜粋):
fanclub-backend-585549545f-7vnn8 app=fanclub-backend,pod-template-hash=585549545f
fanclub-backend-585549545f-dksms app=fanclub-backend,pod-template-hash=585549545f
fanclub-backend-585549545f-gp252 app=fanclub-backend,pod-template-hash=585549545f
fanclub-db-0 app=fanclub-db,apps.kubernetes.io/pod-index=0,...
fanclub-frontend-7d9b4667f-j88b8 app=fanclub-frontend,pod-template-hash=7d9b4667f,...
fanclub-frontend-7d9b4667f-jc6vl app=fanclub-frontend,pod-template-hash=7d9b4667f,...
fanclub-logcollector-dlmkk app=fanclub-logcollector,controller-revision-hash=86449768d6,...
fanclub-logcollector-fgnpg app=fanclub-logcollector,controller-revision-hash=86449768d6,...
8 Pod の内訳は backend ×3 / db ×1(StatefulSet)/ frontend ×2 / logcollector ×2(DaemonSet)です。default-deny-all は podSelector: {} なので、この 8 個すべてが選択されています。素通しの Pod はありません。
ラベルは app: fanclub-backend / app: fanclub-db / app: fanclub-frontend / app: fanclub-logcollector の 4 種類が現れます。
ここで app: fanclub-batch が出てこないことに注意してください。allow-db-from-backend の 2 つ目の from 要素と allow-egress-batch-to-db はこのラベルを指しているのに、対応する Pod が存在しません。これは CronJob fanclub-report(0 9 * * *)が実行されたときにだけ生成される Pod のラベルなので、通常時の get pod には現れないのが正常です。
ポリシーが指す Pod が今いないことと、ポリシーが不要であることは別です。棚卸しで実行中の Pod だけを見て「使われていないから消そう」と即断すると、次の 09:00 に夜間バッチが DB へ繋がらなくなります。実行時にしか現れない Pod をポリシー設計から落とさない——これは見落としやすい観点です。
fanclub-logcollector に専用ポリシーが無いのは穴なのか
9 本のどれも app: fanclub-logcollector を対象にしていません。棚卸しをすると必ず気づく点なので、正面から扱います。
まず状態を確認します。default-deny-all は podSelector: {} なのでこの Pod も選択しています。専用の許可ポリシーが無いということは、allow-dns(これも podSelector: {} で全 Pod 対象)による DNS 以外、この Pod は外向きの通信ができない状態にあるということです。
これは穴ではありません。fanclub-logcollector の実体は busybox:1.37 で「log collector started」と出力するだけのダミーで、第1巻の DaemonSet 教材として置かれたものです。Loki へログを送信していないので、egress の許可が要りません。ポリシーが無いのは、必要が無いからです。
逆に、ここで「ポリシーが無い=設定漏れだ」と早合点して egress を開けると、不要な穴を自分で開けることになります。監査の作法として、その Pod が何をしているかを確認してから判断するという順序を守ってください。ポリシーの過不足は、アプリケーションの挙動を知らずには判定できません。
ステップ4:棚卸し表を作る
本演習の成果物です。9 本のポリシーを「対象 Pod ごと」に束ね直し、入ってくる通信と出ていく通信を 1 行にまとめます。ポリシー名の並びではなく守る対象の並びで書き直すのがポイントです。
| 対象 Pod | 入ってくる通信(許可元 / ポート) | 出ていく通信(許可先 / ポート) |
|---|---|---|
fanclub-frontend | traefik Namespace(全 Pod)/ 80 | backend / 8080・CoreDNS / 53 |
fanclub-backend | frontend / 8080・monitoring Namespace(全 Pod)/ 8080 | db / 5432・CoreDNS / 53 |
fanclub-db | backend または batch / 5432 | CoreDNS / 53 のみ |
fanclub-batch(CronJob) | なし | db / 5432・CoreDNS / 53 |
fanclub-logcollector | なし | CoreDNS / 53 のみ |
ステップ5:表を見て違和感のある行を挙げる
表を眺めてください。太字にした 2 箇所——fanclub-frontend の「traefik Namespace(全 Pod)」と fanclub-backend の「monitoring Namespace(全 Pod)」——だけが、個別の Pod ではなく Namespace 単位になっています。他の行はすべて「backend から」「frontend から」と Pod で書かれているのに、この 2 行だけ粒度が違います。
YAML を 9 本並べて読んでいるだけでは、この不揃いには気づきにくいものです。表に起こすと粒度の違いが目に見えます。これが棚卸しをする理由です。監査は「怪しいものを探す」作業ではなく、「揃っているべきものが揃っていない箇所を見つける」作業だと考えてください。
試験での粒度も確認しておきます。CKS では「与えられた Namespace の NetworkPolicy を読み、指定された通信が通るか答えよ」「通るように直せ」という形で問われます。読解だけなら 3〜5 分が目安です。本演習が 10〜15 分かかるのは 9 本を通しで扱っているためで、試験では 2〜3 本のポリシーが与えられます。
やってみよう②:過剰許可 2 件を締める
この演習を始める前に
- 本演習は実際に稼働中のポリシーを書き換えます。スナップショット等で復元できる環境で実施してください
- 変更前に
kubectl get networkpolicy <name> -n fanclub -o yaml > <name>.yaml.bakでバックアップを取ってください(第2回でマニフェスト編集に適用した作法を、そのままポリシーにも適用します) - 戻すときは
kubectl apply -f <name>.yaml.bakです。戻した後はkubectl get networkpolicy <name> -n fanclub -o yamlでfromの要素構造を目視で確認してください - どうなったら戻すかの基準を先に決めます。ステップ5 の 3 点確認のうち1 つでも失敗したら、先へ進まずバックアップから戻します
- 締めた後に必ずステップ5 の 3 点確認を行ってください。正規経路を壊したまま先へ進むと、第4回以降の演習でブラウザ確認が失敗します
ここが本回の中心です。所要時間の目安は 20〜30 分。穴の実証から修正、そして正規経路の保全確認まで通します。
namespaceSelector 単独が何を意味するか
allow-backend-from-monitoring は「monitoring Namespace から backend:8080 を許可」します。では monitoring に何がいるのかを見ます。
実行コマンド(k8s-ops 上・developer):
$ kubectl get pod -n monitoring
実行結果:
NAME READY STATUS RESTARTS AGE
alertmanager-kube-prometheus-stack-alertmanager-0 2/2 Running 8 12h
fluent-bit-fluent-bit-loki-2zctl 1/1 Running 4 11h
fluent-bit-fluent-bit-loki-dtdq9 1/1 Running 4 11h
fluent-bit-fluent-bit-loki-mrq7l 1/1 Running 4 11h
fluent-bit-fluent-bit-loki-v8t7r 1/1 Running 4 11h
fluent-bit-fluent-bit-loki-wzqgb 1/1 Running 4 11h
kube-prometheus-stack-grafana-5b5f4fc89f-ksgt2 3/3 Running 0 4m36s
kube-prometheus-stack-kube-state-metrics-857ccb48f8-q5gdc 1/1 Running 8 12h
kube-prometheus-stack-operator-8568d847d5-phngf 1/1 Running 7 12h
kube-prometheus-stack-prometheus-node-exporter-kjjk7 1/1 Running 4 12h
kube-prometheus-stack-prometheus-node-exporter-m2nnx 1/1 Running 4 12h
kube-prometheus-stack-prometheus-node-exporter-v69mk 1/1 Running 4 12h
kube-prometheus-stack-prometheus-node-exporter-vffrg 1/1 Running 4 12h
kube-prometheus-stack-prometheus-node-exporter-x5jlr 1/1 Running 4 12h
loki-0 2/2 Running 0 4m34s
loki-gateway-75fdc66c6f-crm2s 1/1 Running 8 11h
prometheus-kube-prometheus-stack-prometheus-0 2/2 Running 0 4m34s
17 個います。 Grafana・Alertmanager・kube-state-metrics・Prometheus Operator・node-exporter が 5 台分・Loki と loki-gateway・fluent-bit が 5 台分——そして Prometheus 本体。allow-backend-from-monitoring は namespaceSelector だけで許可しているので、この 17 個すべてが backend の 8080 へ到達できます。
実際に backend をスクレイプする必要があるのは prometheus-kube-prometheus-stack-prometheus-0 の 1 個だけです。1 個で足りるところに 17 個分の経路が開いている——これが「Namespace の箱ごと開けている」ということの具体的な意味になります。
同じことが allow-frontend-from-gateway にも言えます。traefik Namespace から frontend:80 を許可していますが、通す必要があるのは Traefik の Gateway Pod だけです。
この 2 件がなぜ危険なのかを、CKS の文脈で言い直します。Namespace 単位の許可は「その Namespace に Pod を作れる者は誰でも、この経路を使える」という意味になります。監視スタックや Ingress コントローラの Namespace は、Helm chart の更新やオペレータの動作で Pod が増える場所であり、開発者に権限が渡っていることもあります。侵害された Pod が 1 つでもそこに置かれれば、この穴がそのまま横移動(lateral movement)の経路になります。攻撃者は「監視用の Namespace だから安全」という運用側の思い込みを利用します。
ステップ1:穴が実在することを実証する
議論ではなく実測で示します。traefik Namespace にTraefik とは無関係な busybox Pod を立てて、fanclub の frontend を直接叩きます。
実行コマンド(k8s-ops 上・developer):
$ kubectl run np-test --rm -i --restart=Never -n traefik --image=busybox:1.37 -- sh -c 'wget -q -T 5 -O- http://fanclub-frontend.fanclub.svc.cluster.local/'
実行結果(冒頭のみ):
<!DOCTYPE html>
<html lang="ja">
<head>
HTML が返ってきました。この Pod は Traefik でも監視エージェントでもない、ただの busybox です。それでも namespaceSelector: traefik に一致するため、フロントエンドへの経路が開いています。
コマンドの補足です。--rm は終了時に Pod を消すオプション、--restart=Never は Deployment ではなく Pod として起動するための指定です。-T 5 は busybox 版 wget のタイムアウト秒数で、応答が無いときに待ち続けないために付けています。
出力の前に warning: couldn't attach to pod/np-test, falling back to streaming logs という行が出ることがあります。-i での接続が間に合わずログ取得に切り替わっただけで、結果には影響しません。ただしこの経路をたどると --rm が効かず Pod が残ることがあります。ステップ4 で同じ名前の Pod を立てるので、ここで確実に消しておきます。
実行コマンド(k8s-ops 上・developer):
$ kubectl delete pod np-test -n traefik --ignore-not-found
消し忘れたままステップ4 に進むと Error from server (AlreadyExists): pods "np-test" already exists で止まります。
なぜ既存 Pod への exec ではなく一時 Pod を立てるのか、という点も押さえてください。理由は 2 つあります。1 つ目——確かめたいのは「別 Namespace から fanclub へ届くか」なので、テストの起点は traefik や monitoring の側に要ります。 fanclub の Pod から打っても、その問いには答えられません。2 つ目——backend のイメージには curl も wget も nc も入っていません(実測でいずれも exit 127、つまりコマンドが見つからない状態でした)。最小イメージを使っていれば当然のことで、これはセキュリティ上は望ましい状態です。ただし frontend(nginx alpine 系)には /usr/bin/curl と /usr/bin/nc が入っています——同じ Namespace の中では疎通を打てますが、それでは 1 つ目の理由が満たせません。結局、疎通テストはクライアントを持った Pod を、確かめたい側の Namespace に自分で立てる形になります。CKS 試験でも同じ手が要る場面があります。
実務での応用も 1 つ添えます。本番環境では、読者自身が traefik や monitoring Namespace に Pod を作る権限を持たないことが多くあります。その場合はこの手順をそのまま真似できません。代わりに kubectl get networkpolicy -o yaml の読み取りと、kubectl auth can-i create pod -n traefik --as=<対象ユーザー> で「その Namespace に Pod を作れるのは誰か」を確かめます。これは第4回の RBAC 監査にそのままつながる確認です。
ステップ2:相手の Pod ラベルを調べる
締めるには「本当に通すべき Pod」のラベルが必要です。
実行コマンド(k8s-ops 上・developer):
$ kubectl get pod -n traefik --show-labels
$ kubectl get pod -n monitoring --show-labels
実行結果(1 コマンド目・LABELS 列のみ):
traefik-69f4cd68d8-tk7kq app.kubernetes.io/instance=traefik-traefik,app.kubernetes.io/managed-by=Helm,app.kubernetes.io/name=traefik,helm.sh/chart=traefik-39.0.9,pod-template-hash=69f4cd68d8
実行結果(2 コマンド目・Prometheus と、比較用に Grafana / node-exporter を抜粋):
prometheus-kube-prometheus-stack-prometheus-0 app.kubernetes.io/instance=kube-prometheus-stack-prometheus,app.kubernetes.io/managed-by=prometheus-operator,app.kubernetes.io/name=prometheus,app.kubernetes.io/version=3.11.3,...
kube-prometheus-stack-grafana-5b5f4fc89f-ksgt2 app.kubernetes.io/instance=kube-prometheus-stack,app.kubernetes.io/name=grafana,app.kubernetes.io/version=13.0.1,...
kube-prometheus-stack-prometheus-node-exporter-kjjk7 app.kubernetes.io/component=metrics,app.kubernetes.io/instance=kube-prometheus-stack,app.kubernetes.io/name=prometheus-node-exporter,...
3 つを並べたのは、app.kubernetes.io/name の値が prometheus / grafana / prometheus-node-exporter と別々になっていることを見せるためです。node-exporter の値は prometheus ではなく prometheus-node-exporter なので、app.kubernetes.io/name: prometheus で選べば node-exporter は含まれません。前方一致ではなく完全一致で判定される点を確認してください。
出力から拾うべきラベルは次の 2 つです。
| 対象 | ラベル |
|---|---|
| Traefik の Pod | app.kubernetes.io/name: traefik |
| Prometheus の Pod | app.kubernetes.io/name: prometheus |
ラベルは推測しないでください。app / app.kubernetes.io/name / k8s-app のどれを使うかは chart によって違います。fanclub 自身は app: fanclub-backend という形なので、その感覚のまま app: traefik と書くと一致しません。必ず --show-labels で実物を見てから書く——これを作法として固めてください。ポリシーが一致しないと通信が落ちますが、エラーメッセージは「ラベルが違う」とは教えてくれません。
ステップ3:namespaceSelector + podSelector の AND に書き換える
2 本とも全量を掲載します。まず allow-frontend-from-gateway の修正後です。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-from-gateway
namespace: fanclub
spec:
podSelector:
matchLabels:
app: fanclub-frontend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
ports:
- protocol: TCP
port: 80
続いて allow-backend-from-monitoring の修正後です。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-backend-from-monitoring
namespace: fanclub
spec:
podSelector:
matchLabels:
app: fanclub-backend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 8080
変更点は 1 箇所だけです。- namespaceSelector: というリスト要素の中に、同じインデントレベルで podSelector: を足しました。spec.podSelector(守る側の指定)はそのままで、ingress.from の中身だけが変わっています。
ここで podSelector: の前に - を付けてしまうと OR になり、Namespace 全体の許可が残ったまま何も締まりません。判定規則のセクションで扱った AND / OR の書き分けが、そのまま結果を左右する場面です。apply は成功しますし、エラーも出ません。だから次のステップで実際に遮断されたことを測ります。
2 本のマニフェストをそれぞれ allow-frontend-from-gateway.yaml / allow-backend-from-monitoring.yaml として保存し、適用します。
実行コマンド(k8s-ops 上・developer):
$ kubectl apply -f allow-frontend-from-gateway.yaml
$ kubectl apply -f allow-backend-from-monitoring.yaml
既存と同名なので configured として上書きされます。NetworkPolicy の変更は即座に CNI へ反映されるため、Pod の再起動は不要です。
ステップ4:穴が閉じたことを確認する
ステップ1 とまったく同じコマンドを打ちます。条件を 1 つだけ変えて結果を比べるのが検証の基本です。
実行コマンド(k8s-ops 上・developer):
$ kubectl run np-test --rm -i --restart=Never -n traefik --image=busybox:1.37 -- sh -c 'wget -q -T 5 -O- http://fanclub-frontend.fanclub.svc.cluster.local/'
実行結果:
wget: download timed out
pod "np-test" deleted from traefik namespace
pod traefik/np-test terminated (Error)
今度は HTML が返りません。wget: download timed out ——遮断されました。同じコマンド・同じ Namespace・同じイメージで、変えたのはポリシーの書き方だけです。それで結果が反転しました。
ここで遮断の見え方を説明します。NetworkPolicy による遮断は「接続拒否」ではなく、パケットが黙って落ちる形になります。TCP の RST が返るわけではないので、クライアントには即座にエラーが返らず、応答を待ち続けてタイムアウトします。-T 5 を付けている理由がここで効きます。付けていないと、読者は「固まった」と思って Ctrl-C を押すことになります。
つまり「応答が無い」ことが遮断の証拠になります。逆に Connection refused が返った場合、それは NetworkPolicy ではなく「宛先にサービスがいない」ことを意味します。この 2 つを混同すると切り分けを誤ります。
なお、タイムアウトを待たずに Ctrl-C で抜けると --rm が働かず、np-test Pod が traefik Namespace に残ります。念のため削除しておきます。
実行コマンド(k8s-ops 上・developer):
$ kubectl delete pod np-test -n traefik --ignore-not-found
Ingress コントローラの Namespace に検証用 Pod を放置しないこと自体が、このセクションで主張した脅威(その Namespace に Pod を作れる者は経路を使える)と首尾一貫しています。
ステップ5:正規の通信が壊れていないことを 3 点で確認する
締めることより、締めても壊れていないことを確かめるほうが難しい作業です。省略せず 3 つを順に確認してください。
① ブラウザ経路(Traefik → frontend → backend → db)
実行コマンド(k8s-ops 上・developer):
$ curl -sk --noproxy '*' -o /dev/null -w '%{http_code}\n' -H 'Host: fanclub.local' https://192.168.1.124/
$ curl -sk --noproxy '*' -H 'Host: fanclub.local' https://192.168.1.124/api/members
実行結果:
[{"createdAt":"2026-07-25T14:28:03.572932","email":"hanako@example.com","id":1,"name":"鈴木花子","plan":"standard"},{"createdAt":"2026-07-25T14:28:03.572932","email":"ichiro@example.com","id":2,"name":"佐藤一郎","plan":"free"},{"createdAt":"2026-07-25T18:00:53.008964","email":"restore-test@example.com","id":11,"name":"復元テスト太郎","plan":"premium"}]
実測では HTTPS 200 が返り、/api/members は会員データの JSON を返します。--noproxy '*' を付けているのは、本書ラボが Squid のホワイトリスト方式でプロキシを経由する構成であり、環境変数のプロキシ設定を回避する必要があるためです(第2巻から継承した作法)。192.168.1.124 は k8s-lb の IP ですが、ラボ固有の値なのでそのまま覚えないでください。試験環境では別のアドレスになります。
/api/members を叩くことに意味があります。/ だけの確認では frontend の静的 HTML が返るだけで、frontend → backend → db の経路を検証できません。/api/members は Nginx の location /api/ を通って backend に届き、backend が db に問い合わせて JSON を返します。ここまで通って初めて「壊していない」と言えます。締めた 2 本のうち allow-backend-from-monitoring は backend への ingress を制御しているので、書き間違えれば backend への到達が変わる可能性があります。
② Prometheus の scrape が続いていること
締めたのは monitoring 側なので、監視が止まっていないかを確認します。fanclub Namespace には fanclub-backend という ServiceMonitor があり、Prometheus は job="fanclub-backend" として backend の 3 Pod を scrape しています。scrape は Prometheus 側から backend へ入る ingress なので、allow-backend-from-monitoring を締めると真っ先に壊れる箇所です。
実行コマンド(k8s-ops 上・developer):
$ kubectl -n monitoring exec sts/prometheus-kube-prometheus-stack-prometheus -c prometheus -- wget -q -O- 'http://localhost:9090/api/v1/query?query=up{namespace="fanclub"}'
実行結果:
{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"up","container":"backend","endpoint":"http","instance":"10.244.215.248:8080","job":"fanclub-backend","namespace":"fanclub","pod":"fanclub-backend-585549545f-gp252","service":"fanclub-backend"},"value":[1785043035.499,"1"]},{"metric":{"__name__":"up",...,"pod":"fanclub-backend-585549545f-7vnn8",...},"value":[1785043035.499,"1"]},{"metric":{"__name__":"up",...,"pod":"fanclub-backend-585549545f-dksms",...},"value":[1785043035.499,"1"]}]}}
確認すべきは up{namespace="fanclub"} の値が 1 であることです。実測では backend の 3 Pod とも 1 を維持していました。0 になっていれば scrape が失敗しているので、ラベルの指定を見直してバックアップから戻します。Grafana の画面から同じ PromQL を実行しても構いません。
③ 締めた 2 本以外に影響が出ていないこと
実行コマンド(k8s-ops 上・developer):
$ kubectl get pod -n fanclub
実行結果:
NAME READY STATUS RESTARTS AGE
fanclub-backend-585549545f-7vnn8 2/2 Running 6 10h
fanclub-backend-585549545f-dksms 2/2 Running 6 10h
fanclub-backend-585549545f-gp252 2/2 Running 6 10h
fanclub-db-0 1/1 Running 0 5m59s
fanclub-frontend-7d9b4667f-j88b8 1/1 Running 8 11h
fanclub-frontend-7d9b4667f-jc6vl 1/1 Running 8 11h
fanclub-logcollector-dlmkk 1/1 Running 4 11h
fanclub-logcollector-fgnpg 1/1 Running 4 11h
8 Pod がすべて Running であることを確認します。NetworkPolicy の変更で Pod が落ちることは通常ありませんが、readiness probe が Service 経由で通信している構成だと影響が出ることがあります。ここは「変化していないこと」を確かめる確認です。
この演習の到達点
正規の通信は保ったまま、余計な経路だけを閉じられました。CKS の NetworkPolicy 問題は「通す」と「止める」の両方を同時に満たさないと得点になりません。片方だけなら簡単で、全部止めるなら default-deny 1 本で済みます。両立させることが難しい——ここが本回の実質です。
本番ガードレール 本番で同じ締め方をするときは、先に監視・Ingress 側の Pod ラベルを確認し、chart の更新でラベルが変わらないかを確かめてください。app.kubernetes.io/name は Helm chart の慣習ラベルなので比較的安定していますが、chart のメジャー更新で変わることがあります。ラベルが変わればポリシーは静かに一致しなくなり、通信が落ちます。エラーログには「ラベルが変わった」とは出ず、監視の欠測やタイムアウトとしてしか現れません。ラベル変更を検知する仕組み(監視のアラート・GitOps の diff)とセットで運用してください。
この修正は helm upgrade で巻き戻る
ステップ3 で kubectl apply した 2 本には、持ち主がいます。ステップ1 の手前で describe したときに見えた app.kubernetes.io/managed-by=Helm と meta.helm.sh/release-name: fanclub がそれです。この 2 本は fanclub chart の一部として管理されています。
つまり、次に helm upgrade fanclub ./fanclub-chart を打った瞬間、chart 内のテンプレートの内容で上書きされ、締めた 2 本は緩い状態へ戻ります。手で apply した変更は chart の側に存在しないので、Helm から見れば「意図しない差分」でしかありません。ArgoCD で GitOps 運用している場合はさらに早く、同期のタイミングで自動的に巻き戻ります。
本番ガードレール chart が管理しているリソースを直すときは、手で kubectl apply するのではなく chart のテンプレートを直して helm upgrade するのが筋です。GitOps なら Git のマニフェストを直します。手で当てた修正は、次のデプロイまでしか生きません。しかも戻ったことに気づく仕組みがありません——ポリシーが緩くなっても通信は通り続けるので、監視にもエラーにも出ないからです。
本回で手で当てているのは、NetworkPolicy を書いて読んで検証する技術を身につけることが目的だからです。CKS 試験でも kubectl apply で直接当てて解答します。ただし「本番の運用としては chart 側を直す」という原則は、ここで区別して覚えておいてください。第3巻では以降も手で当てる場面が続きますが、いずれも同じ前提です。
なお本書ラボでは第4回以降もこの締めた状態を前提に進めます。helm upgrade を挟む予定はないため、巻き戻りは起きません。
試験相当の粒度
| ステップ | 試験相当の所要時間 |
|---|---|
過剰許可の特定(describe で from を読む) | 約 3〜5 分 |
namespaceSelector + podSelector の AND へ書き換えて適用 | 約 5〜7 分 |
| 正規経路が生きていることの確認(3 点) | 約 3〜5 分 |
試験では「このポリシーを、指定の Pod からのみ許可するように直せ」という 1 問として出題され、ポリシー 1 本あたり 5〜8 分が目安です。本演習が 20〜30 分かかるのは、2 本ぶんに加えて締めた後の 3 点確認を含むためです。試験では 3 点確認まで求められないことが多いのですが、実務では確認まで含めて 1 セットです。試験の型だけを覚えて現場に出ると、通信を止めたことに気づかないまま作業を終えることになります。
白紙から 1 本書くドリル
試験では「渡されたポリシーを直す」だけでなく「新しいポリシーを白紙から書く」形式も出ます。締める演習の直後に、短いドリルを 1 本挟みます。所要 4〜6 分です。
課題
fanclub Namespace の app: fanclub-db に対して、「app: fanclub-backend の Pod からの 5432/TCP だけを受け付ける」Ingress ポリシーを、既存の allow-db-from-backend を見ずに白紙から書いてください。書けたら kubectl get netpol allow-db-from-backend -n fanclub -o yaml と見比べます。
答え合わせの観点は 3 つです。
spec.podSelectorの対象は「守る側」(この課題では db)である。許可元の backend を書いてしまう取り違えが最も多いpolicyTypesにIngressを明記する。省略してもingressルールがあれば Ingress は対象になるが、明記する癖を付けておくほうが安全であるportsを省くと全ポート許可になる。「5432 だけ」という条件を満たすにはportsが必須
既存の allow-db-from-backend は batch からの許可も含んだ 2 要素の OR になっているので、そこだけは差異として読み取ってください。課題の条件は backend のみなので、1 要素で正解です。

egress 側の点検 ——DNS 許可と「広く許可しないこと」の価値
ここまでは ingress(入ってくる通信)の監査でした。ここから egress(出ていく通信)へ視点を移します。監査の 3 つ目の問い「egress 側は何を許可しているか」に答える段です。
egress の default-deny が効いている状態
fanclub の default-deny-all は policyTypes に Egress も含んでいます。9 本のうち egress 許可を持つのは 4 本だけです——allow-dns / allow-egress-frontend-to-backend / allow-egress-backend-to-db / allow-egress-batch-to-db。
読み取りで確認します。ここは疎通ではなく「読んで分かる」ことに徹します。
実行コマンド(k8s-ops 上・developer):
$ kubectl describe networkpolicy allow-dns -n fanclub
実行結果(Spec: 以降):
Spec:
PodSelector: <none> (Allowing the specific traffic to all pods in this namespace)
Not affecting ingress traffic
Allowing egress traffic:
To Port: 53/UDP
To Port: 53/TCP
To:
NamespaceSelector: kubernetes.io/metadata.name=kube-system
PodSelector: k8s-app=kube-dns
Policy Types: Egress
ここが正しい書き方の実例です。To: の下に NamespaceSelector と PodSelector が並んでいます。これが AND で、「kube-system Namespace の、かつ k8s-app: kube-dns ラベルを持つ Pod」だけに絞られています。先ほど見た allow-frontend-from-gateway の From: には NamespaceSelector しかありませんでした。同じ 9 本の中に、締まっている書き方と緩い書き方が混在しているわけです。
UDP と TCP の両方を許可している点も見てください。DNS は通常 UDP/53 ですが、応答が大きい場合や再送時に TCP/53 へフォールバックします。UDP だけだと名前解決が断続的に失敗するという、原因の掴みにくい障害になります。
やってみよう① のステップ1 で取った custom-columns の出力に戻り、TYPES 列に Egress を含む行を数えるのが早い確認方法です。default-deny-all を除けば 4 本、対象は frontend・backend・batch と、全 Pod 向けの DNS 許可だけです。
ここから言えることは、allow-dns 以外の egress 許可を持たない Pod(db と logcollector)は、DNS 以外の外向き通信ができないということです。インターネットにも、他のアプリケーションにも、ノードにも出られません。allow-dns は podSelector: {} で fanclub の全 Pod に効いているので、kube-system の CoreDNS へは出られます——ここは正確に押さえてください。「他 Namespace には一切出られない」ではなく、「CoreDNS への 53 番以外には出られない」です。
DNS を許可し忘れると全部壊れる
egress を絞ったときに最初に壊れるのは名前解決です。Pod は Service 名を解決するために CoreDNS(kube-system)へ問い合わせます。egress の default-deny だけを入れてこの問い合わせを許可しないと、アプリケーションは「名前解決できない」状態になります。しかも症状は「接続できない」ではなく「名前が引けない」なので、原因を NetworkPolicy に結びつけるまでに時間がかかります。
allow-dns の全量は判定規則のセクションで掲載したので、ここでは 2 点だけ確認します。
- UDP と TCP の両方を許可する。DNS は通常 UDP/53 を使いますが、応答が大きい場合や再送時に TCP/53 へフォールバックします。UDP だけを許可していると、名前解決が断続的に失敗するという最も切り分けにくい症状になります
namespaceSelector+podSelectorの AND で書かれている。kube-systemNamespace 全体ではなくk8s-app: kube-dnsの Pod だけを許可しています
2 つ目に注目してください。allow-dns は既存 9 本の中で最も締まった書き方をしている 1 本です。やってみよう② で書き換えた 2 本は、この allow-dns と同じ形になりました。つまりお手本は最初から同じ Namespace の中にありました。
これは第2巻の書き方が悪かったという話ではありません。同じ Namespace の中に、締まっている書き方と粒度の粗い書き方が同居していたということです。書いた時期・書いた人・そのときの目的が違えば、粒度は揃いません。実務のポリシー群はたいていそうなります。だから定期的な棚卸しが要ります。
最も強い保護は、広く許可しないこと
ここで本回のもう 1 つの主題を立てます。次のセクションで扱うノードメタデータ保護の前提になる考え方です。
ノードメタデータへのアクセスを塞ぎたいとき、真っ先に思いつくのは「メタデータ IP への egress を拒否するルールを書く」ことです。しかしNetworkPolicy に「拒否ルール」は存在しません。書けるのは許可だけです。したがって、メタデータを塞ぐ最も確実な方法は、そもそも外向きの egress を広く許可しないことになります。fanclub の Pod はすでにこの状態にあります。
言い換えると、egress の default-deny が効いている Pod に対して、メタデータ遮断のための追加ルールは必要ありません。むしろ「メタデータを塞ぐために except を書こう」と考えると、except を書くための土台として 0.0.0.0/0 の広い許可が必要になり、塞ぎたかった穴以外をすべて開けてしまいます。
except が必要になるのは「広い egress を許可せざるを得ない Pod」に限られます。外部 API を叩くバッチ処理、レジストリからイメージを取得する処理、SaaS へログを送るエージェントなどです。それらは業務要件として外へ出る必要があるので、egress を閉じきれません。その場合の書き方を次のセクションで扱います。
ノードメタデータ(169.254.169.254)の保護
CKS D1 のコンピテンシーに「Protect node metadata and endpoints」という項目があります。ここから 2 セクションかけて扱います。
何が危険なのか
クラウド環境では、各ノードがメタデータエンドポイント(169.254.169.254)を持ちます。ここからインスタンスの識別情報や、ノードに割り当てられた IAM ロールの一時クレデンシャルなどを取得できます。これはリンクローカルアドレスで、ノード上からしか到達できない代わりに、認証なしで応答する設計になっているものが多くあります。「ノード上にいる=そのノードの権限を持つはず」という前提に立った設計です。
問題は、このエンドポイントがリンクローカルアドレスとして、そのノード上のどのコンテナからも素通りで見えてしまうことです。Pod は自分の IP を持っていますが、169.254.169.254 宛のパケットはノードの外へ出ず、ノード自身のメタデータサービスへ届きます。侵害された Pod がこのエンドポイントを叩けると、ノードに割り当てられたクラウド権限を丸ごと奪えます。攻撃者にとっては、コンテナ 1 つの侵害からクラウドアカウント全体への足がかりになります。CKS が D1 でこれを名指ししているのはそのためです。
コンピテンシー名は「node metadata and endpoints」です。メタデータ以外にも、ノードが露出しているサービス面があります。本巻でどこが回収するかを先に示しておきます。
| 対象 | 何が危険か | 本巻での回収 |
|---|---|---|
169.254.169.254(ノードメタデータ) | クラウド権限の奪取につながる | 本回 |
| kubelet API(10250)/ read-only port(10255) | ノード上の Pod 一覧・ログ・exec | 第2回の CIS 評価 + 第5回(kubelet の認証認可) |
| etcd(2379-2380) | クラスタの全データ | 第2回の CIS 評価 + 第7回(ホスト OS の攻撃面最小化) |
NetworkPolicy で塞げるのは Pod から出ていく通信です。kubelet API や etcd をノード外から守るのはホスト側のファイアウォールと認証認可の仕事なので、担当する回が分かれます。「メタデータとエンドポイントの保護」は 1 つの機能ではなく、複数の層にまたがる要件だと理解してください。
記法 ——ipBlock の cidr と except
外部宛の egress を許可しつつ、メタデータだけを除外する形は次のように書きます。次のセクションの演習でそのまま使うマニフェストです。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-external-except-metadata
namespace: nptest
spec:
podSelector:
matchLabels:
app: probe
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
押さえる点を 3 つに絞ります。
exceptに書く CIDR は、同じipBlockのcidrに含まれている必要がある。0.0.0.0/0に対する169.254.169.254/32は含まれるので有効。含まれない値を書くと API Server が拒否するexceptはそのipBlockの中でだけ効く。別の- ipBlock:要素で同じ範囲を広く許可していると、OR で重なって穴が復活する。ポリシーが複数本あるときも同じで、許可は加算される0.0.0.0/0はクラスタ内の Pod IP も含む。この 1 本で「外部にもクラスタ内にも出られる」状態になるため、実務ではexceptにプライベート CIDR やサービス CIDR も並べることが多い
2 つ目は監査の観点として重要です。「メタデータを except で塞いだ」と報告されていても、同じ Pod を対象にした別のポリシーが 0.0.0.0/0 を無条件で許可していれば、穴は開いたままです。except の有無ではなく、その Pod に効いているポリシーを合算した結果を見てください。
ラボには実メタデータ API が無い ——これを隠さない
ここで正直に書いておきます。本書ラボは Hyper-V 上の AlmaLinux であり、169.254.169.254 で待ち受けるサービスは存在しません。したがってポリシーが無い Pod から叩いても届きません(実測でも BLOCKED でした)。
これは検証設計として厄介な状況です。「遮断できた」ことを、届かなかったという結果だけで示すことはできません。もともと届かないのですから、ポリシーが効いているのかどうか区別がつきません。
この制約をごまかして「遮断できました」と書くと、読者は動いた気になって理解していない状態になります。そこで次のセクションでは、到達できる宛先を使って except が効くことを証明します。これは実務の検証手法としても正しい進め方です——「効くはずの設定が効いているか」は、効いていれば結果が変わる条件で確かめます。
実クラウドでの注意
クラウドによっては IMDSv2(メタデータ取得の前にトークンの要求を必須にする方式)のような仕組みや、CNI 側でメタデータへのアクセスをブロックする機能が提供されています。NetworkPolicy はその一手段であって唯一の手段ではありません。多層で守るのが定石です。
もう 1 点、実装差にも触れておきます。ノード宛(リンクローカルアドレスを含む)のトラフィックをどう扱うかは CNI 実装によって差があります。本書ラボは Calico で、次のセクションで except が効くことを実測します。ただし「NetworkPolicy を書いたのだから、どの CNI でも同じように遮断される」とは考えないでください。採用している CNI で実際に測るのが確実です。
ただし試験で問われるのは NetworkPolicy による遮断の記法なので、ipBlock + except の形は手に覚えてください。

except は広い許可を選ばざるを得ないときの緩和策やってみよう③:except が効いていることを到達できる宛先で証明する
この演習を始める前に
- 本演習は専用の Namespace
nptestを作って行います。fanclub には手を入れません - 終わったらNamespace ごと削除します(ステップ5)
- 所要時間の目安は 15〜20 分です
ステップ1:検証用の Namespace と Pod を用意する
実行コマンド(k8s-ops 上・developer):
$ kubectl create namespace nptest
$ kubectl run probe -n nptest --image=busybox:1.37 --labels=app=probe --command -- sleep 3600
$ kubectl wait --for=condition=Ready pod/probe -n nptest --timeout=120s
実行結果:
namespace/nptest created
pod/probe created
pod/probe condition met
やってみよう② では --rm の使い捨て Pod を使いましたが、ここでは同じ Pod に対して 3 回測るので、sleep 3600 で常駐させます。--labels=app=probe を付けているのは、この後の NetworkPolicy の spec.podSelector で選択するためです。ポリシーの対象になるラベルは、Pod を作るときに決めておきます。
到達できる宛先として k8s-registry(192.168.1.123:5000) を使います。理由は 3 つあります。クラスタ外にあるので ipBlock で指定できること、実在する IP で応答が返ることが確認済みであること、そしてラボ内のホストなのでプロキシ(Squid のホワイトリスト)の影響を受けないことです。外部インターネットを宛先にすると、遮断されたのかプロキシに弾かれたのかが分からなくなります。検証では、測りたいもの以外の変数を減らします。
もう 1 点、先に疑問を潰しておきます。egress の点検セクションで「DNS を許可し忘れると全部壊れる」と扱ったばかりなので、「このポリシーには DNS の許可が無いけれど大丈夫なのか」と気になるはずです。本演習では宛先を IP で直接指定するため、名前解決が発生しません。したがって allow-dns に相当する egress 許可は不要です。Service 名やホスト名で叩く設計なら、DNS 許可が別途必要になります。
ステップ2:パターン① ポリシーなしの状態を測る
まず基準値を取ります。NetworkPolicy を 1 本も適用していない状態です。
実行コマンド(k8s-ops 上・developer):
$ kubectl exec -n nptest probe -- wget -q -T 5 -O- http://192.168.1.123:5000/v2/
$ kubectl exec -n nptest probe -- wget -q -T 5 -O- http://169.254.169.254/
結果は次のとおりです。
| 宛先 | 結果 |
|---|---|
192.168.1.123:5000(k8s-registry) | REACHABLE |
169.254.169.254 | BLOCKED(宛先が存在しない) |
ここで正直に読んでください。169.254.169.254 が届かないのは、ポリシーのおかげではなく、そこに何も無いからです。この行を「遮断できている」と読んではいけません。
ステップ3:パターン② except にメタデータ IP だけを入れる
前のセクションで示した egress-external-except-metadata をファイルに保存して適用し、同じ 2 つの宛先を測り直します。
実行コマンド(k8s-ops 上・developer):
$ kubectl apply -f egress-external-except-metadata.yaml
$ kubectl exec -n nptest probe -- wget -q -T 5 -O- http://192.168.1.123:5000/v2/
$ kubectl exec -n nptest probe -- wget -q -T 5 -O- http://169.254.169.254/
結果は次のとおりです。
| 宛先 | 結果 |
|---|---|
192.168.1.123:5000 | REACHABLE |
169.254.169.254 | BLOCKED |
ここで一度立ち止まってください。パターン① と結果がまったく同じです。レジストリには届き、メタデータ IP には届かない。表だけを見れば「except が効いている」と言いたくなりますが、この時点では「ポリシーが効いているのか、もともと届かないだけなのか」を区別できません。
実務でも試験でも、ここで満足して終わる人が多くいます。「設定した」「エラーが出ない」「期待どおりの結果に見える」——3 つ揃っていても、証明にはなっていません。
ステップ4:パターン③ 到達できていた宛先を except に入れる
決着を付けます。except に 192.168.1.123/32 を追加します。全量を掲載します。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-external-except-metadata
namespace: nptest
spec:
podSelector:
matchLabels:
app: probe
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
- 192.168.1.123/32
実行コマンド(k8s-ops 上・developer):
$ kubectl apply -f egress-external-except-metadata.yaml
$ kubectl exec -n nptest probe -- wget -q -T 5 -O- http://192.168.1.123:5000/v2/
$ kubectl exec -n nptest probe -- wget -q -T 5 -O- http://169.254.169.254/
結果は次のとおりです。
| 宛先 | 結果 |
|---|---|
192.168.1.123:5000 | BLOCKED |
169.254.169.254 | BLOCKED |
到達できていた宛先が、except に入れた途端に遮断されました。3 パターンを並べて見ます。
| ポリシー | 192.168.1.123:5000 | 169.254.169.254 |
|---|---|---|
| ポリシーなし | REACHABLE | BLOCKED(宛先が存在しない) |
0.0.0.0/0 + except: [169.254.169.254/32] | REACHABLE | BLOCKED |
0.0.0.0/0 + except: [169.254.169.254/32, 192.168.1.123/32] | BLOCKED | BLOCKED |
3 行目が決め手です。これで except は確かに穴を塞いでいると言えます。そして同じ仕組みが 169.254.169.254/32 にも働いています——実クラウドであれば、メタデータ API への到達も同じように遮断されます。ラボにメタデータ API が無いという制約は、これで迂回できました。
この検証手法は一般化できます。「効くはずの設定が効いているか」を確かめるには、効いていれば結果が変わる条件を用意します。届かないものが届かないままであることは、何の証拠にもなりません。これは NetworkPolicy に限らず、ファイアウォール・RBAC・Admission でも同じ考え方が使えます。第4回の RBAC 監査でも、kubectl auth can-i で「できないはずのことを試す」という同じ型が出てきます。
ステップ5:後片付け
実行コマンド(k8s-ops 上・developer):
$ kubectl delete namespace nptest
Namespace を消せば、中の Pod も NetworkPolicy もまとめて消えます。検証用リソースを残さないのは監査側の作法です。残った probe Pod は、次に誰かが棚卸ししたときに「これは何だ」という調査コストになります。自分が作ったものは自分で消してください。
fanclub 側には何もしない理由
ここまで演習をやると「同じことを fanclub にも適用しよう」と考えたくなります。適用しません。理由を明示します。
fanclub の Pod はすでに egress の default-deny が効いていて、allow-dns 以外の外向き許可を持たない Pod(db / logcollector)はそもそも外に出られません。メタデータ遮断のために except を書き足す必要はありません。書き足すと「広い egress を許可する」ことになり、かえって穴が増えます。
egress の点検セクションで立てた「最も強い保護は広く許可しないこと」が、ここで回収されます。演習で覚えた記法を、必要のない場所へ持ち込まないでください。セキュリティ設定は、足すほど強くなるわけではありません。
試験での粒度も確認します。「指定の Pod からノードメタデータへのアクセスを遮断せよ。ただし他の外部通信は維持すること」という形で問われうる内容です。ipBlock + except を書いて適用するまでで 5〜8 分が目安になります。「他の外部通信は維持」という条件があるからこそ 0.0.0.0/0 + except が正解になる、という条件の読み取りが得点の分かれ目です。
TLS Ingress ——試験の標準 Ingress とラボの Gateway API
D1 の残り 1 つ、「Properly set up Ingress with TLS」に入ります。ここは試験形式とラボ形式が違うので、最初に宣言しておきます。
| 試験(CKS) | 本書ラボ | |
|---|---|---|
| リソース | 標準 Ingress(networking.k8s.io/v1)+ spec.tls | Gateway + HTTPRoute(Gateway API v1.4.0) |
| 実装 | NGINX Ingress Controller 想定 | Traefik v3.6.x |
| 証明書 | kubernetes.io/tls タイプの Secret を手で作る | cert-manager v1.20.0 が発行・更新 |
| ドキュメント参照 | kubernetes.io/docs 内なので参照可 | gateway-api.sigs.k8s.io は CKS では参照不可 |
4 行目が重要です。Gateway API のドキュメントは CKA では参照できますが、CKS では参照できません。本書ラボの実装が Gateway API であることに慣れきると、試験本番で手が止まります。試験では標準 Ingress で解答する——この方針をここで徹底しておきます。ラボで日常的に触るものと試験で書くものが違うのは、学習者にとって負担のある状況です。だからこそ、両方を扱えるようにしておくのが本セクションの目的になります。
なぜラボは ingress-nginx を使わないのか
ingress-nginx は 2026 年 3 月で EOL となったため、本シリーズでは採用していません。新規に本番環境を設計するなら、後継である Gateway API を選ぶのが妥当です。
一方、試験の許可ドキュメントに NGINX Ingress Controller が含まれるのは、試験環境の構成がそうであるためです。「試験で問われる形式」と「今から本番で選ぶべき実装」は別だと切り分けてください。本書が古い形式を教えているわけでも、試験対策を捨てているわけでもありません。試験は現時点の出題に合わせて解き、設計は現時点の推奨に合わせて選びます。この 2 つを両立させるために、本セクションでは両形式を並べます。
標準 Ingress の TLS 設定(試験形式)
試験で書く形の全量です。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: exam-ingress
namespace: fanclub
spec:
tls:
- hosts:
- fanclub.local
secretName: exam-tls
rules:
- host: fanclub.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: fanclub-frontend
port:
number: 80
押さえる点は 3 つです。
spec.tls[].hostsとspec.rules[].hostは両方書く。前者は「どのホスト名の TLS 終端に使うか」、後者は「どのホスト名のリクエストをこのルールで振るか」で、役割が違う。片方だけだと TLS 終端かルーティングのどちらかが機能しないsecretNameが指す Secret はkubernetes.io/tlsタイプでなければならない(Opaqueでは動かない)。キー名はtls.crtとtls.keyで固定pathTypeは必須。Prefix/Exact/ImplementationSpecificの 3 種があり、省略するとリソースの作成自体が失敗する
リソース名を exam-ingress / exam-tls にしている理由も書いておきます。fanclub-tls は cert-manager の管理下にある稼働中の証明書だからです。読者がこの YAML をそのまま適用しても本番経路を壊さないよう、演習用の名前で統一しています。
試験中に参照できるドキュメント
Ingress の記法は、許可 1 番の kubernetes.io/docs にあります。ingress-nginx のサイトも許可されていますが、kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/ というサブパス配下に限定されています。トップページや Examples 配下は開けません。
ここが実務的な注意点です。user-guide/nginx-configuration/ はアノテーションと ConfigMap の章なので、Ingress リソースの spec.tls サンプルは置かれていません。試験本番で Ingress の YAML をこのドメインから取ろうとすると空振りします。
- Ingress の記法そのものは
kubernetes.io/docsから取るのが正解ルート - ingress-nginx 側は「アノテーションで NGINX の挙動を変える」問題が出たときの参照先
- Gateway API のドキュメント(
gateway-api.sigs.k8s.io)は CKS では参照不可。HTTPRoute で答えないこと
ラボの実装を読む ——HTTPRoute は「/」の 1 ルートだけ
本書ラボで実際に HTTPS を終端しているのは Gateway API の側です。中身を確認します。
実行コマンド(k8s-ops 上・developer):
$ kubectl get httproute fanclub-route -n fanclub -o yaml
実行結果(spec: のみ抜粋):
spec:
hostnames:
- fanclub.local
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: fanclub-gateway
rules:
- backendRefs:
- group: ""
kind: Service
name: fanclub-frontend
port: 80
weight: 1
matches:
- path:
type: PathPrefix
value: /
rules は1 つだけで、/ を frontend の 80 へ振っています。/api を backend へ振るルールはありません。では /api/members はどう届いているのかというと、frontend の Nginx が location /api/ { proxy_pass http://fanclub-backend:8080; } で中継しています。この事実が、allow-backend-from-frontend と allow-egress-frontend-to-backend の 2 本が必須である根拠です。棚卸しのときに「Gateway から直接 backend へ行くはずだから frontend → backend は不要では」と推測で判断すると、削った瞬間に /api が 502 になります。
読み取るべき事実は次の 3 点です。rules は 1 つだけ。backendRefs は name: fanclub-frontend / port: 80。matches は path が type: PathPrefix / value: /。つまり/api を backend へ振るルールは存在しません。Gateway に届いたリクエストは、パスに関係なくすべて frontend へ送られます。
では /api/members はどうやって backend まで届いているのでしょうか。frontend の Nginx が中継しています。
実行コマンド(k8s-ops 上・developer):
$ kubectl exec -n fanclub deploy/fanclub-frontend -- cat /etc/nginx/conf.d/default.conf
実行結果(該当箇所):
location /api/ {
proxy_pass http://fanclub-backend:8080;
ここで確定している事実は、frontend の Nginx が location /api/ で backend へリバースプロキシしていることです。
この事実が NetworkPolicy の設計に直結します。ルーティングの実体が「Gateway → frontend → backend」である以上、allow-backend-from-frontend と allow-egress-frontend-to-backend は必須です。もし「Gateway が /api を backend へ直接振っている」と思い込んでこの 2 本を削れば、/api/members は即座にエラーになります。
NetworkPolicy の設計は、ルーティングの実体を確認してから行ってください。マニフェストの見た目や思い込みで判断しないことです。「Gateway API を使っているのだから、Gateway が全部のルーティングをしているはずだ」という推測は、この構成では成り立ちませんでした。本回で最も実務的な教訓であり、やってみよう① の棚卸しが効く理由でもあります。
cert-manager が作っている Secret を見る
実行コマンド(k8s-ops 上・developer):
$ kubectl get secret -n fanclub --field-selector type=kubernetes.io/tls
実行結果:
NAME TYPE DATA AGE
fanclub-tls kubernetes.io/tls 3 11h
fanclub Namespace には fanclub-tls という Secret があり、タイプは kubernetes.io/tls、キーは 3 個です。これは cert-manager の Certificate リソース(fanclub/fanclub-tls)が管理しており、期限が近づけば自動で更新されます。同じ仕組みで cert-manager Namespace には CA 用の fanclub-ca(Secret fanclub-ca-key-pair)があります。
ここで押さえてほしいのは、cert-manager が発行した Secret も、手で作る Secret も同じ kubernetes.io/tls タイプだということです。Ingress や Gateway から見れば区別がありません。違うのは「誰が作って誰が更新するか」だけです。試験では手で作り、実務では cert-manager に任せる——この対応関係が分かれば、両方が同じものに見えてきます。
やってみよう④:試験形式で TLS Ingress 一式を組み立てる
この演習を始める前に
- 本演習で作る Ingress と Secret は試験形式の練習です。本書ラボの HTTPS 経路は Gateway API のままで、そちらを置き換えるものではありません
- Secret 名は
exam-tlsを使ってください。fanclub-tlsは使いません。fanclub-tlsは cert-manager がCertificateリソース(fanclub/fanclub-tls)で管理している稼働中の証明書で、同名で作ろうとすると失敗し、削除すると第4回以降のブラウザ確認が壊れます - cert-manager が管理している Secret には触れないでください
- 作ったリソースはステップ5 で必ず削除します(削除するのは
exam-tlsとexam-ingressだけです) - 所要時間の目安は 8〜12 分です
この演習の到達点
先にゴールを明示します。「標準 Ingress を組み立てて describe まで確認し、ADDRESS が付かない理由を説明できる」ことが到達点です。ルーティングが通ることは求めません。ラボの構成では原理的に通らないためです。
理由を先に説明しておきます。本書ラボの Traefik は、起動引数に --providers.kubernetescrd と --providers.kubernetesgateway を持っていますが、--providers.kubernetesingress を持っていません。つまり標準の Ingress リソースは Traefik の監視対象外です。IngressClass(traefik・controller は traefik.io/ingress-controller)は Helm chart が作っているので存在しますが、それを処理するプロバイダが動いていません。
結果として、Ingress を適用するとリソースは作成されるが ADDRESS は空のまま、describe の Events も空という状態になります。Gateway API 経路とは干渉しないことも実測済みで、適用中も HTTPS は 200 を維持します。だから実適用して構いません。
試験では Ingress を扱うタスクが出る以上、それを処理するコントローラが用意されているはずです(許可ドキュメントに ingress-nginx が含まれているのも傍証です)。ただしどの実装が動いているかは公式に明記されていないので、決めつけずに kubectl get ingressclass で確認してから書き始めてください。ラボで ADDRESS が空になるのはマニフェストが間違っているからではなく、処理するコントローラがいないからです。この切り分けができることが、本演習の実質的な価値になります。第2回の「/healthz が ok を返しているのに kubectl logs は壊れていた」という構図と同じで、コマンドが成功することと機能していることは別です。
ステップ1:証明書ファイルを用意する
試験では、証明書と秘密鍵のファイルが問題文で与えられるのが普通です。生成から問われることは多くありません。そこで本演習でも「手元に tls.crt と tls.key がある」状態を作るところから始めます。
本書ラボの k8s-ops には openssl が入っていません(AlmaLinux 10 の最小構成のためです)。パッケージを足さずに済ませるため、cert-manager が既に発行している証明書を Secret から取り出してファイルにします。Secret の中身は base64 でエンコードされているので、デコードして書き出します。
実行コマンド(k8s-ops 上・developer):
$ cd /tmp
$ kubectl get secret fanclub-tls -n fanclub -o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt
$ kubectl get secret fanclub-tls -n fanclub -o jsonpath='{.data.tls\.key}' | base64 -d > tls.key
$ ls -l tls.crt tls.key
$ head -1 tls.crt
$ head -1 tls.key
実行結果:
-rw-r--r--. 1 developer developer 1103 7月 26 14:18 tls.crt
-rw-r--r--. 1 developer developer 1675 7月 26 14:18 tls.key
-----BEGIN CERTIFICATE-----
-----BEGIN RSA PRIVATE KEY-----
この操作は fanclub-tls を読むだけで、変更も削除もしません。jsonpath の tls\.crt でバックスラッシュを入れているのは、キー名に含まれるドットを「階層の区切り」ではなく文字としてのドットとして扱わせるためです。ここは試験でも手が止まりやすい記法なので覚えておいてください。
Secret から取り出すこの手順自体、実務では「今動いている証明書の期限を確認したい」「別のクラスタへ持っていきたい」という場面でそのまま使えます。
自分で証明書を作りたい場合は sudo dnf install -y openssl(baseos リポジトリにあります)を入れたうえで、次のコマンドで生成できます。
実行コマンド(k8s-ops 上・developer):
$ openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout tls.key -out tls.crt -subj "/CN=fanclub.local"
-nodes は秘密鍵を暗号化しない(パスフレーズを付けない)指定です。Kubernetes の Secret に入れる鍵はパスフレーズ無しである必要があります。OpenSSL 3.x では -nodes は非推奨のエイリアスで、正式には -noenc です。-nodes でも動作します。
なお、ノードに評価用のパッケージを増やさないという判断は第7回(ホスト OS の攻撃面最小化)の主題そのものです。「入れずに済む方法があるなら入れない」を選べる場面では選んでおきます。
試験では、与えられるファイル名が tls.crt / tls.key でない場合があります。次のステップの --cert / --key に渡すパスを、問題文に合わせて読み替えてください。
ステップ2:kubernetes.io/tls タイプの Secret を作る
本回の暗記対象の 1 つです。
実行コマンド(k8s-ops 上・developer):
$ kubectl create secret tls exam-tls --cert=tls.crt --key=tls.key -n fanclub
タイプを確認します。
実行コマンド(k8s-ops 上・developer):
$ kubectl get secret exam-tls -n fanclub -o jsonpath='{.type}{"\n"}'
実行結果:
kubernetes.io/tls
kubectl create secret generic では作れません。generic で作るとタイプは Opaque になり、Ingress の secretName に指定しても TLS 終端に使えません。tls サブコマンドを使うことで、タイプが kubernetes.io/tls になり、キー名も tls.crt / tls.key に固定されます。
試験で最も手が止まりやすい 1 行なので、暗記対象に入れます。「Secret を作る」と考えて反射的に generic を打つと、後の Ingress が動かず、原因も分かりにくくなります。
ステップ3:Ingress を組み立てる
手書きせず、generator からひな形を出します。
実行コマンド(k8s-ops 上・developer):
$ kubectl create ingress exam-ingress -n fanclub --rule="fanclub.local/*=fanclub-frontend:80,tls=exam-tls" --dry-run=client -o yaml
実行結果:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: exam-ingress
namespace: fanclub
spec:
rules:
- host: fanclub.local
http:
paths:
- backend:
service:
name: fanclub-frontend
port:
number: 80
path: /
pathType: Prefix
tls:
- hosts:
- fanclub.local
secretName: exam-tls
status:
loadBalancer: {}
generator が出したものを鵜呑みにせず、3 点を目で確認してください。pathType が Prefix になっていること(/* と書いたので Prefix に変換されました)、spec.tls[].hosts と spec.rules[].host の両方に fanclub.local が入っていること、secretName が exam-tls であること。
ingressClassName が出力に含まれていない点にも注意してください。本書ラボでは traefik が既定の IngressClass に指定されているため、省略しても既定が使われます。試験環境で既定が設定されていない場合は、--class=nginx を付けるか YAML に spec.ingressClassName を書き足す必要があります。
出力が得られたら、そのまま信じずに 2 箇所を目で確認してください。1 つは spec.tls が期待どおりに入っているか、もう 1 つは pathType が何になっているかです。generator の /* という書き方は pathType: Prefix になる場合と ImplementationSpecific になる場合があり、前のセクションで示した全量 YAML と細部が食い違うことがあります。generator は入力の手間を減らす道具であって、正解を出す道具ではありません。
試験でのコツを 1 つ。--dry-run=client -o yaml でひな形を出し、ファイルに落として直すほうが、公式ドキュメントから YAML をコピーするより速く済みます。第1回で示した export do="--dry-run=client -o yaml" がここで効きます。kubectl create ingress ... $do > exam-ingress.yaml と打てば、編集対象のファイルが手元にできます。
ステップ4:適用して describe で読む
本演習の核です。ひな形を exam-ingress.yaml として保存し、適用します。
実行コマンド(k8s-ops 上・developer):
$ kubectl apply -f exam-ingress.yaml
$ kubectl get ingress -n fanclub
実行結果:
NAME CLASS HOSTS ADDRESS PORTS AGE
exam-ingress traefik fanclub.local 80, 443 25s
ADDRESS 列が空であることを確認してください。20 秒待っても 1 分待っても変わりません。次に describe で中身を読みます。
実行コマンド(k8s-ops 上・developer):
$ kubectl describe ingress exam-ingress -n fanclub
実行結果:
Name: exam-ingress
Labels: <none>
Namespace: fanclub
Address:
Ingress Class: traefik
Default backend: <default>
TLS:
exam-tls terminates fanclub.local
Rules:
Host Path Backends
---- ---- --------
fanclub.local
/ fanclub-frontend:80 (10.244.119.162:80,10.244.119.157:80)
Annotations: <none>
Events: <none>
ここが本演習の核心です。3 つの事実を並べて読んでください。
TLS: exam-tls terminates fanclub.local—— TLS の記述は正しく解釈されている/ → fanclub-frontend:80 (10.244.119.162:80,10.244.119.157:80)—— バックエンドの Pod IP まで解決できている(Service の Endpoints が引けている)Address:が空、Events:も<none>—— それでも誰も処理していない
マニフェストは正しいのです。API Server は受け付け、内容も正しく解釈しています。にもかかわらず何も起きないのは、traefik.io/ingress-controller という IngressClass を名乗るコントローラが、Ingress リソースを監視していないからです。Events が空なのがその証拠で、エラーすら出ません。失敗すら記録されない——これが「処理する主体がいない」ということの見え方です。
本番でこの状態に出会ったら、疑うのは YAML ではなくコントローラの側です。IngressClass に対応するコントローラが動いているか、そのコントローラが Ingress リソースを見る設定になっているかを確認します。
読むべき点は 2 つあります。1 つは、TLS の記述とルールが正しく解釈されて表示されていることです。TLS の行には Secret 名とホスト名の対応が出て、ルールの行にはパスと backend Service、そして解決された Pod の IP が出ます。マニフェストは正しく解釈されています。
もう 1 つは Events が空であることです。コントローラが処理していれば、何らかのイベントが記録されます。空だということは、誰もこのリソースを見ていないということです。
この 2 点から切り分けが立ちます。「マニフェストは正しいのに ADDRESS が付かない」→「処理するコントローラがいない」。試験でも実務でも、Ingress が効かないときはまずこの順で見ます。マニフェストを疑って何時間も直し続ける前に、IngressClass とコントローラの稼働状況を確認してください。
最後に、ラボの実経路が無傷であることを確認します。
実行コマンド(k8s-ops 上・developer):
$ kubectl get httproute -n fanclub
$ curl -sk --noproxy '*' -o /dev/null -w '%{http_code}\n' -H 'Host: fanclub.local' https://192.168.1.124/
実行結果:
200
HTTPRoute はそのまま残り、HTTPS も 200 を返します。標準 Ingress と Gateway API は干渉しません。処理する主体が別なので、同じクラスタに両方が存在しても互いに影響しないためです。
ステップ5:後片付け
実行コマンド(k8s-ops 上・developer):
$ kubectl delete ingress exam-ingress -n fanclub
$ kubectl delete secret exam-tls -n fanclub
$ rm -f tls.crt tls.key
削除対象は本演習で作った exam-ingress と exam-tls だけです。fanclub-tls には触れないでください。cert-manager が管理するラボの稼働証明書で、削除すると HTTPS のブラウザ確認が止まります。本演習のリソースを第4回以降に持ち込まないようにしてください。
試験相当の粒度
| ステップ | 試験相当の所要時間 |
|---|---|
ステップ2(kubectl create secret tls) | 約 1〜2 分 |
ステップ3(generator から Ingress を組み立て、spec.tls と pathType を確認) | 約 4〜6 分 |
ステップ4(適用して describe で検証) | 約 2〜3 分 |
証明書は問題文で与えられることが多いので、試験では kubectl create secret tls から Ingress 作成までで 5〜8 分が目安になります。本演習は通しで 8〜12 分。本回の 4 本の中で、試験の出題粒度に最も近い演習です。
暗記必須コマンド(第3回)
本回で扱った、試験中に手早く打てる必要があるコマンドを集約します。
| 用途 | コマンド | 補足 |
|---|---|---|
| TLS Secret 作成 | kubectl create secret tls <name> --cert=tls.crt --key=tls.key -n <ns> | generic では kubernetes.io/tls にならない。最頻出 |
| Ingress のひな形生成 | kubectl create ingress <name> --rule="<host>/*=<svc>:<port>,tls=<secret>" --dry-run=client -o yaml | 出力の pathType と spec.tls は目で確認する |
| NetworkPolicy 一覧 | kubectl get networkpolicy -n <ns> | 短縮形は netpol |
| ポリシーの中身を読む | kubectl get networkpolicy <name> -n <ns> -o yaml | AND / OR の判定は必ず YAML で。describe では区別できない |
| 対象 Pod のラベル確認 | kubectl get pod -n <ns> --show-labels | ラベルは推測しない |
| Namespace のラベル確認 | kubectl get namespace <ns> --show-labels | kubernetes.io/metadata.name は自動付与 |
| 疎通テスト(一時 Pod) | kubectl run np-test --rm -i --restart=Never -n <ns> --image=busybox:1.37 -- sh -c 'wget -q -T 5 -O- http://<svc>.<ns>.svc.cluster.local/' | 既存 Pod に curl / wget が無い前提で組み立てる |
| 常駐の検証 Pod | kubectl run probe -n <ns> --image=busybox:1.37 --labels=app=probe --command -- sleep 3600 | 複数回テストするとき |
NetworkPolicy に generator はありません(kubectl create networkpolicy というサブコマンドは存在しません)。ただし kubernetes.io/docs は試験の許可ドキュメントに含まれるので、サンプル YAML は公式から取れます。したがって暗記が要るのは YAML の全文ではなく、次の 4 点です。
from/toの AND / OR の書き分け(-を付けるか、同一要素内に並べるか)podSelector単独が同一 Namespace のみに一致することipBlockのexceptはcidrに含まれる範囲でしか書けないこと- クライアントが無いイメージでの疎通テストの立て方
この 4 点は、公式ドキュメントから探すより手で覚えたほうが速く済みます。逆に言えば、ここさえ押さえていればサンプルは公式から取ればよいということです。暗記の範囲を広げすぎないでください。
試験環境の作法も再掲します。CKS 試験環境には alias k=kubectl がプリインストールされています。本文は読みやすさのため kubectl の全綴りで統一していますが、試験では k で打ってください。
まとめ
本回では次の点を確認しました。
- fanclub Namespace には第2巻の時点で NetworkPolicy が 9 本あり、
default-deny-allも入っていた。本回は白紙から書く回ではなく、既にあるものを疑って読む回だった - default-deny は出発点であって到達点ではない。開けた穴の粒度が粗ければ、リスクはその粗さのまま残る
from/toは-で並べれば OR、同一リスト要素の中に並べれば AND。インデントの深さだけが意味を変えるpodSelector単独は同一 Namespace のみに一致する。別 Namespace の相手はnamespaceSelectorとの AND で指定するallow-frontend-from-gatewayとallow-backend-from-monitoringはnamespaceSelector単独で Namespace 全体を許可していた。podSelectorとの AND に締めても、HTTPS 200・/api/membersの JSON・Prometheus の scrape はすべて維持できた- ルーティングの実体は Gateway → frontend(Nginx) →(
/api/のみ)→ backend → db。allow-backend-from-frontendは必須であり、削ってはならない - egress を絞るときは CoreDNS への 53(UDP + TCP)を許可する。
allow-dnsはk8s-app: kube-dnsとの AND で書かれた良い実例だった - ノードメタデータを塞ぐ最も強い方法は、広い egress をそもそも許可しないこと。
ipBlockのexceptが要るのは、広い egress を許可せざるを得ない Pod に限られる exceptが効いていることは、到達できる宛先をexceptに入れて遮断されることで証明できる。届かないものが届かないままであることは証拠にならない- TLS Ingress は試験=標準 Ingress +
spec.tls+kubernetes.io/tlsSecret、ラボ=Gateway API HTTPRoute + cert-manager。Gateway API のドキュメントは CKS では参照できない
理解度チェック(○×形式・全 9 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
default-deny-allが適用されていれば、許可ポリシーの粒度が粗くても最小権限の設計として十分であるfromのリスト要素を-で分けて 2 つ書くと、どちらかに一致すれば通る(OR)namespaceSelectorだけを書いて許可すると、その Namespace の全 Pod からの通信を許可することになるpodSelector単独の指定は、そのポリシーがある Namespace の Pod にのみ一致する- egress を default-deny にしても、CoreDNS への 53 番ポートは自動的に許可される
ipBlockのexceptに書く CIDR は、同じipBlockのcidrに含まれている必要がある- 外向きの egress を一切許可していない Pod にも、メタデータ IP 遮断のための
exceptルールを追加する必要がある - CKS 試験の TLS Ingress は、標準
Ingressのspec.tlsとkubernetes.io/tlsタイプの Secret で解答する - 本回で扱った NetworkPolicy では、HTTP メソッドやパス単位の制御ができる
解答と解説
1=×/2=○/3=○/4=○/5=×/6=○/7=×/8=○/9=×
問1 が本回の主題そのものです。default-deny は「明示的に許可したものだけを通す」状態を作るだけで、許可の粒度までは保証しません。本回で締めた 2 件は、default-deny がある状態でも Namespace 全体を開けていました。「default-deny が入っているか」ではなく「開けた穴が最小か」を問う——これが CKS の視点です。
問2 と問4 はセットで押さえてください。問2 は正しく、- で分けたリスト要素は OR で評価されます。逆に同一リスト要素の中に namespaceSelector と podSelector を並べれば AND です。- を 1 つ付けるかどうかで、絞ったつもりの設定が広い許可のまま残ります。問4 も正しく、podSelector 単独はポリシーが置かれている Namespace の中でしか一致しません。別 Namespace の Pod を指定したいなら namespaceSelector との AND が必要です。この 2 問が本回の最頻出の混乱点で、試験でも時間を失いやすい箇所です。
問3 は正しく、namespaceSelector は Namespace の集合を指すセレクタであり、Pod を絞りません。monitoring の 17 Pod のうち、backend への通信が必要なのは prometheus 1 つだけでした。問5 は誤りで、自動的な例外はありません。CoreDNS への UDP/TCP 53 を明示的に許可します。TCP を忘れると、応答が大きい場合や再送時に名前解決が断続的に失敗します。
問6 は正しく、except は cidr の内側を差し引く指定なので、範囲外の値を書くと API Server が拒否します。問7 は誤りで、必要ありません。むしろ except を書くために 0.0.0.0/0 を許可すると、塞ぎたかった穴以外の外向き通信をすべて開けてしまいます。fanclub の db / logcollector は egress 許可を持たないため、既にメタデータへも到達できません。
問9 は誤りで、標準の networking.k8s.io/v1 NetworkPolicy は L3/L4(Pod・Namespace・IP・ポート)までです。HTTP メソッドやパス単位の制御は CiliumNetworkPolicy の領域で、第11回(D4)で扱います。問8 は TLS Ingress のセクションを読み返してください。
次回予告
第4回「RBAC 監査 + ServiceAccount トークン管理」では、D2(Cluster Hardening)に入ります。本回は「どこから、どこへ、何が通るか」を絞りました。次回は「誰が、何をできるか」を絞ります。kubectl auth can-i と rbac-tool で過剰な権限を洗い出し、最小権限へ寄せます。あわせて automountServiceAccountToken: false と期限付きトークン(TokenRequest)で、ServiceAccount トークンの露出を減らします。
本回で使った「できないはずのことを試して確かめる」という検証の型は、次回の RBAC でもそのまま使えます。ポリシーを書いた後に「効いているか」を測る癖を、ここで身につけておいてください。
