新卒インフラエンジニア向け「Kubernetes 実践教科書 ③ CKS セキュリティ・ハードニング編」(全19回)の第11回です。前回はテナントの境界を扱いました。今回は境界の内側を流れているものを扱います。Pod と Pod のあいだを流れるパケットは、いま誰にでも読める状態にあります。
本回の主題は 2 つです。第2巻から使ってきた Calico を Cilium へ入れ替え、Pod 間通信を WireGuard で暗号化すること。そして HTTP のメソッドとパス単位で通信を絞る L7 NetworkPolicy を導入することです。
ただし本回は、手順どおりに進めても何度も止まります。CNI の入れ替えは公式が用意した無停止手順が本書ラボでは成立せず、暗号化を有効にした瞬間にクラスタ全体の通信が落ち、L7 ポリシーを効かせるとアプリが 426 を返し始めます。そのすべてに理由があり、理由のほうが本回の中身です。
- ラボ Kubernetes v1.36.3
- CKS 試験環境 v1.35
- Cilium v1.19.6(Helm chart 1.19.6)
- Cilium CLI v0.19.6
- Calico v3.32.0(移行元)
- containerd v2.2.6
- AlmaLinux 10.2(kernel 6.12.0-211.22.1.el10_2)
- Helm v4.1.4
- ArgoCD v3.4 系
- 確認日 2026-07-29
目次
- 第11回のスコープ・今ここマップ
- この回のゴール
- CNI を入れ替えるとは何をすることか
- 移行方式の設計 —— 公式の無停止手順がこのラボで破綻する理由
- 移行の準備
- やってみよう①:Calico → Cilium 一括移行
- 移行で踏む 4 つの落とし穴
- 透過暗号化は何を守り、何を守らないのか
- やってみよう②:WireGuard 透過暗号化と「外は暗号・中は平文」の実証
- mutual authentication は暗号化しない —— CKS で問われる区別
- Istio の PeerAuthentication STRICT(記法のみ)
- Cilium L7 NetworkPolicy —— L3/L4 の限界とその先
- やってみよう③:L7 NetworkPolicy の適用と HTTP 426 の解決
- Hubble —— 拒否を「見る」
- やってみよう④:Hubble で L7 拒否を観測する
- toFQDNs(DNS ベースの egress 制御)
- 暗記必須コマンド(Cilium / L7 / Hubble)
- まとめ
- 理解度チェック(○×形式・全 9 問)
- 次回予告
第11回のスコープ・今ここマップ
本回も CKS ドメイン D4: Minimize Microservice Vulnerabilities(配点 20%) です。本回で D4 の 4 コンピテンシーがすべて埋まります。
コンピテンシーの文言に Cilium, Istio と 2 つが併記されている点に注意してください。本書ラボは Cilium を採用しますが、Istio のサイドカー mTLS も試験範囲です。記法は本文で提示します。
既習範囲 / 本回で上書きする観点
| 既習 | どこで | 本回で上書きする観点 |
|---|---|---|
| Calico と Cilium の比較 | 第2巻第10回(「eBPF 内部・L7 ポリシー・Hubble・mTLS の詳細は第3巻の領域」と予告済み) | その予告の回収です。 比較ではなく実際に入れ替えます |
| NetworkPolicy(default-deny + 最小許可 9 本) | 第3回 | L3/L4 の限界を実測し、L7 へ進みます。既存ポリシーが L7 を無効化するという順序の問題も扱います |
SELinux Enforcing・containerd の enable_selinux = true | 第6回 | そのハードニングが mutual authentication(SPIRE)を止めます。 AVC を読んで原因を確定させます |
| ホスト OS の攻撃面最小化 | 第7回 | 検証のためにノードへツールを入れるという逆向きの行為を扱い、入れたら戻すことの難しさまで見ます |
| OPA Gatekeeper(admission webhook) | 第8回 | CNI が無い瞬間、webhook がクラスタをロックします。 第8回の成果物が移行を妨げます |
| gVisor と SELinux の正面衝突 | 第10回 | 同じ構図が本回で 2 度目になります。「防御は後から入れる機能を止める」という一般則として扱います |
GitOps 経由の変更(base/ を編集して push) | 第9回・第10回 | 本回は chart そのものを変更します(nginx の設定を ConfigMap 化)。アプリ側を直さないとセキュリティ設定が入らない、という事例です |
先に 3 つ、本回で覆る前提を予告します。
① 「CNI の移行は Cilium 公式の手順に従えば無停止でできる」——本書ラボでは成立しません。② 「mTLS は暗号化と相互認証をまとめて提供する」——Cilium の mutual authentication は暗号化しません。③ 「L7 ポリシーを足せば許可外の HTTP が遮断される」——既存の L3/L4 ポリシーが残っていると一切効きません。いずれも本文で実測を示します。
第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)
第3部 OS / ノード堅牢化(D3)
第6回: カーネル層の強制アクセス制御(SELinux/seccomp/capabilities/AppArmor)(D3)
第7回: ホスト OS の攻撃面最小化と権限管理(D3)
第4部 ワークロード防御(D4)
第8回: Pod Security Standards + Admission(D4)
第9回: Secret 管理(etcd 暗号化 / SealedSecrets / ExternalSecrets)(D4)
第10回: マルチテナンシー分離 + サンドボックス(gVisor/RuntimeClass)(D4)
★ 第11回: Cilium 透過暗号化 + L7 NetworkPolicy(Calico → Cilium 移行)(D4) ← 今ここ
第5部 サプライチェーンセキュリティ(D5)
第12回: 最小イメージ + 静的解析(Kubesec + KubeLinter)(D5)
この回のゴール
本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。
- 稼働中クラスタの CNI を入れ替える手順と、そこで何が止まるかを説明できる
- 透過暗号化(WireGuard / IPsec)と mutual authentication(SPIFFE/SPIRE)を区別できる
- WireGuard による暗号化を
wg showとtcpdumpで証明できる CiliumNetworkPolicyの L7 ルール(HTTP メソッド / パス)を設計できる- 既存の L3/L4 ポリシーが L7 を無効化する理由を説明できる
- Hubble で許可と拒否を観測できる
本回から、試験対応の書き方を変えます。
第6回から第10回までは、各演習の見出しに「所要 20 分(うち試験相当の粒度は ステップ2〜4 の 5 分)」という形で所要時間と試験相当の範囲を併記していました。第11回からは、節ごとに「試験ではこう問われる。」という枠を置く形にします。
理由は、ここから先は 1 つの演習の中で「試験に出る部分」と「本書ラボの事情」が何度も入れ替わるためです。演習の頭に 1 行で書くより、該当する話題の直後に置くほうが対応が取れます。 試験に出る粒度と所要時間は、その枠の中で引き続き明示します。
本回の起点
起点は第10回の完了状態です。第2巻から引き継いだ 9 VM 構成が稼働している前提で進めます。
| 項目 | 値 |
|---|---|
| ノード | 5/5 Ready・Kubernetes v1.36.3・containerd v2.2.6・AlmaLinux 10.2 |
| Pod | 103 個・異常 0・14 namespace |
| CNI | Calico v3.32.0(kubectl apply -f calico.yaml で導入・CRD 22 個) |
| Calico のデータプレーン | IPIP Always + BGP(vxlanMode: Never・blockSize: 26) |
| NetworkPolicy | fanclub に 9 本(第3回)・argocd に 6 本 |
| fanclub | https://fanclub.local/ が 200・/api/members が会員 3 名の JSON |
| Longhorn | 4 ボリューム attached healthy |
| ArgoCD | fanclub-api-prod が Synced / Healthy |
| Gateway | fanclub-gateway が traefik / 192.168.1.200 / PROGRAMMED True |
| alma-proxy の whitelist | 42 行(本回で 1 行だけ足します) |
containerd の enable_selinux | Workload Node 2 台のみ true(Control Plane Node 3 台は false)(第6回) |
| RuntimeClass | 0 個(第10回の gVisor は撤去済み) |
本回は本書ラボで最も破壊的な回です。
演習①で Calico を削除した時点から先は不可逆になります。kubectl apply -f calico.yaml で戻しても、Cilium が書き換えた CNI 設定ファイル・Pod の IP 割当・ノードのルーティングは噛み合いません。戻す手段は VM スナップショットからの復元だけです。 演習を始める前に、全 VM のスナップショットを停止状態で取得してください。
本回で行き来するホスト
| ホスト | 本回での役割 |
|---|---|
| alma-proxy | whitelist に helm.cilium.io を 1 行追加 |
| k8s-ops | Cilium CLI の導入・helm / kubectl の実行・疎通確認 |
| k8s-cp-01〜03 / k8s-wl-01〜02 | firewalld のポート開放(全 5 台)・再起動(全 5 台) |
| k8s-wl-01 | tcpdump / wireguard-tools の導入とパケットキャプチャ |
CNI を入れ替えるとは何をすることか
移行の手順に入る前に、いま動いている Calico が何をしているのかを実機で確認します。ここを飛ばすと、後で出てくる障害の原因が読めません。
本ラボの Calico はどう配線されているか
実行コマンド(k8s-ops・developer):
$ kubectl get ippools.crd.projectcalico.org default-ipv4-ippool -o yaml
実行結果(抜粋):
spec:
allowedUses:
- Workload
- Tunnel
assignmentMode: Automatic
blockSize: 26
cidr: 10.244.0.0/16
ipipMode: Always
natOutgoing: true
nodeSelector: all()
vxlanMode: Never
ipipMode: Always / vxlanMode: Never。本書ラボの Calico は IPIP トンネルで Pod トラフィックを運び、経路は BGP で配っています。ノード上では tunl0 というデバイスと、proto bird というルートとして見えます。
実行コマンド(k8s-wl-01・developer):
$ ip route | grep -E 'tunl0|bird'
実行結果:
10.244.83.0/26 via 10.0.10.127 dev tunl0 proto bird metric 1024 onlink
10.244.119.128/26 via 10.0.10.129 dev tunl0 proto bird metric 1024 onlink
10.244.184.64/26 via 10.0.10.125 dev tunl0 proto bird metric 1024 onlink
blackhole 10.244.215.192/26 proto bird
10.244.217.192/26 via 10.0.10.126 dev tunl0 proto bird metric 1024 onlink
この 5 行を覚えておいてください。 他ノードの Pod ブロック(/26)ごとに 1 行ずつ、10.0.10.x 経由の経路が並んでいます。本回の最初の障害では、この行がすべて消えます。(10.244.83.0/26 等のブロックは Calico の IPAM が動的に割り当てるため、読者の環境では別の値になります。見るべきは値ではなく「tunl0 経由で 10.0.10.x を向いた行が 5 本ある」という形です)
ノードには NIC が 2 枚あります。
eth0 が 192.168.1.x(既定ゲートウェイのある管理系)、eth1 が 10.0.10.x(クラスタ内部通信用)です。上の経路が 10.0.10.x を向いているのは、Calico が eth1 を自動検出して選んだためです。この「どちらの NIC を使うか」が、本回の落とし穴の源になります。
Calico と Cilium は何が違うのか
| 項目 | Calico(第2巻〜第10回) | Cilium(第11回〜) |
|---|---|---|
| カプセル化 | IPIP(tunl0) | VXLAN(cilium_vxlan・8472/udp) |
| 経路の配り方 | BGP(BIRD が proto bird のルートを配る) | VXLAN の overlay(agent 同士が直接同期) |
| IPAM | 独自ブロック /26 単位 | cluster-pool /24 単位 |
| ノード IP の選び方 | IP=autodetect(first-found) | Node の InternalIP |
| ノード間で使う NIC | eth1(10.0.10.x)を自動検出 | eth0(192.168.1.x)(InternalIP がそちらのため) |
| ポリシー | K8s NetworkPolicy(L3/L4) | K8s NetworkPolicy + CiliumNetworkPolicy(L7 まで) |
| 実装 | iptables / nftables + BIRD | eBPF(L7 は Envoy) |
| CRD | 22 個 | 10 個(chart に同梱されず operator が実行時に作る) |
この表で本回に効いてくるのは 4 行目と 5 行目です。 第2巻では firewalld の trusted ゾーンに 10.0.10.0/24 を入れることで、Calico のノード間通信をまとめて許可していました。Cilium は eth0 を使うので、その許可の恩恵を受けられません。

移行で変わってしまうもの
- 全 Pod の IP が変わります(Calico の
/26ブロックから Cilium の/24割当へ)。第10回までの記事に載っている Pod IP は再現しません。IP をハードコードした設定があれば壊れます(本書ラボはすべて Service 名で解決しているため影響しませんが、現場では移行前に洗い出す対象になります) /etc/cni/net.d/10-calico.conflistが10-calico.conflist.cilium_bakにリネームされます(Cilium のcni-exclusiveの働き)- Calico の CRD 22 個が消えます
kube-proxyは残します(kubeProxyReplacement: "false")。Cilium による kube-proxy の置き換えは本回のスコープ外です
移行方式の設計 —— 公式の無停止手順がこのラボで破綻する理由
本回で最も設計的な節です。 結論から言うと、本書ラボでは公式が用意した無停止移行を採りません。なぜ採れないのかを、実測とともに説明します。
公式が用意している per-node 移行
Cilium 公式は「既存 CNI を残したまま Cilium を二次モードで入れ、ノードを 1 台ずつ移していく」手順を用意しています。
二次モードとは。
Cilium の agent を全ノードに常駐させながら、CNI の設定ファイル(/etc/cni/net.d/)を書かず、Pod のネットワークを一切担当しない状態を指します。kubelet は引き続き既存 CNI(Calico)を使い、Cilium は overlay を張って待機します。移行の準備だけを先に済ませておく形です。
要点は 3 つです。
- Cilium を CNI 設定を書かないモード(
cni.customConf: true)で入れる - 既存とは別の Pod CIDR(公式例は
10.245.0.0/16)を割り当てる CiliumNodeConfigにラベルで紐づけたノードだけを、cordon → drain → 再起動して移す
ただし公式自身が「テストしていない構成」として次を挙げています。
- BGP ベースのルーティング
- IP ファミリーの変更(IPv4 → IPv6)
- Cilium chained モードからの移行
- 既存の NetworkPolicy プロバイダ
本書ラボの Calico は BGP + IPIP です。 最初から「未検証」に該当しています。それでも試したところ、2 段階で破綻しました。
破綻その 1 —— 二次モードで入れただけで Calico が全断した
Cilium を公式どおり二次モードで入れた直後、次のことが同時に起きました。
calico-nodeが 5 台すべて0/1(Not Ready)になった- ノードのルーティングテーブルから
proto birdのクロスノード経路が全消滅した - クロスノードの Pod 間通信が全滅した(同一ノード内は正常)
fanclub-backendが DB に接続できず1/2 Runningへ後退し、https://fanclub.local/api/membersがGateway Timeoutを返した
原因は calico-node のログに出ていました。
実行コマンド(k8s-ops・developer):
$ NPOD=$(kubectl get pods -n kube-system -l k8s-app=calico-node --field-selector spec.nodeName=k8s-wl-01 -o jsonpath='{.items[0].metadata.name}')
$ kubectl logs -n kube-system $NPOD | grep -i autodetect | tail -1
実行結果:
[INFO][81] node-services/autodetection_methods.go 103: Using autodetected IPv4 address on interface cilium_host: 10.245.1.166/32
Calico の IP 自動検出が、Cilium の作ったインターフェースを拾いました。
本書ラボの calico-node は環境変数に IP=autodetect だけを持ち、IP_AUTODETECTION_METHOD は設定されていません。既定は first-found——「最初に見つかった有効なインターフェース」を選ぶ方式です。Cilium が cilium_host を作ると、それが候補に入ってしまいます。
その結果、Calico のノード IP が 10.0.10.128(eth1)から 10.245.1.166(cilium_host)に変わり、BGP ピアの接続先が全ノードで存在しないアドレスになってセッションが切れました。
回避策は 1 行です。Calico に「eth1 を使え」と明示します。
実行コマンド(k8s-ops・developer):
$ kubectl set env ds/calico-node -n kube-system IP_AUTODETECTION_METHOD=interface=eth1
実行結果:
daemonset.apps/calico-node env updated
90 秒で calico-node 5 台すべてが 1/1 に戻り、proto bird の経路も 5 本すべて復活しました。 https://fanclub.local/api/members も HTTP 200 に戻ります(0.06〜0.59 秒)。ログの表示も変わります。
[INFO][80] node-services/autodetection_methods.go 117: Using autodetected IPv4 address 10.0.10.128/24 on matching interface eth1
「CNI を 2 つ同居させる」という行為が、片方の設定を通じてもう片方を壊しました。 公式の移行ガイドはこの点に一切触れていません。本書が採る一括移行では Calico を先に消すため、この事象は起きませんが、共存させる方式を試すなら必ず踏みます。
破綻その 2 —— 1 台だけ移行すると、その Pod は孤島になる
回避策を入れたうえで、k8s-cp-03 を 1 台だけ公式手順どおりに移行しました。移行自体は成功します。
/etc/cni/net.d/10-calico.conflistが10-calico.conflist.cilium_bakにリネームされ、05-cilium.conflistが書かれた- そのノードに立った新しい Pod の IP は
10.245.4.18(Cilium の cluster-pool から払い出し) cilium statusはCluster Pods: 2/69 managed by Ciliumと表示
ところが、その Pod は誰とも通信できませんでした。
| テスト | 結果 |
|---|---|
Cilium 側 Pod → DNS(Service 10.96.0.10) | timeout |
| Cilium 側 Pod → Calico 側 Pod(Pod IP 直接) | No route to host |
| Calico 側 Pod → Cilium 側 Pod | timeout |
Cilium 側 Pod → apiserver(10.96.0.1・hostNetwork) | HTTP 401 = 到達する |
cilium-dbg status | Cluster health: 1/5 reachable |
Calico(IPIP・BGP)と Cilium(VXLAN)は、互いのプレフィックスへの経路を持ちません。 Calico の経路は BIRD が BGP で配るため Cilium 側の CIDR は伝播せず、Cilium も Calico 側の CIDR を知りません。公式手順にある bpf.hostLegacyRouting: true はホストのルーティングスタックを経由させるだけで、経路そのものは生えません。
実際にこの状態で、cp-03 に乗った fluent-bit は Loki へログを送れなくなりました。移行済みノードに乗ったワークロードは、全ノードを移し終えるまで機能しません。
結論 —— CNI 移行の 3 つの選択肢
| 方式 | 内容 | 適する条件 | 本ラボでの成否 |
|---|---|---|---|
| 公式 per-node(無停止) | 二次モードで入れ、ノードを 1 台ずつ移す | 既存 CNI がトンネルを使わないか、両者の経路が相互に伝播する構成 | 成立しない。移行途中のノードが孤島になり、全ノードを移すまで通信が回復しない |
| 一括移行(ダウンタイムあり) | 既存 CNI を消してから Cilium を入れ、全ノードを再起動して Pod を作り直す | メンテナンス時間を確保できる・ノード数が数十台まで | 成立する(本回で採用)。ただし Calico を消した瞬間にクラスタ全体のネットワークが止まる |
| 新クラスタへ切り替え | Cilium で新クラスタを建て、GitOps でワークロードを再構築してから DNS / LB を切り替える | ダウンタイムを許容できない・ノード数が多い・戻せることを最重視する | 本書ラボでは VM 資源の制約で採らない |
「無停止でやろうとすると、かえって長く止まる」——本回はこれを前提に、メンテナンス作業として設計します。
現場での読み替え。
本番クラスタで無停止移行を試みる前に、既存 CNI のデータプレーン方式(トンネルか BGP か native routing か)を確認してください。 公式の未検証リストに自分の構成が入っていないかを見ることが、最初にやるべき調査です。この判断は CKS の出題範囲ではありませんが、CKS 保持者が現場で必ず問われます。
移行の準備
Cilium のバージョンと Kubernetes v1.36 の扱い
| 項目 | 値 |
|---|---|
| Cilium 最新安定版 | v1.19.6(2026-07-16 公開)。chart version = app version |
| Cilium CLI | v0.19.6(2026-07-14 公開) |
| 公式の Kubernetes 互換表 | 1.32 / 1.33 / 1.34 / 1.35 のみ記載。本ラボの v1.36 は未記載 |
Cilium の公式互換表に載っているのは Kubernetes 1.35 までです。
本書ラボの v1.36 は記載がありません。公式は「記載のないバージョンは Kubernetes 自身の前方・後方互換性に依存する」としています。本書では v1.36.3 で全演習が通ることを実機で確認していますが、本番環境では互換表に載っている組み合わせを選ぶのが原則です。
whitelist に追加する 1 行
第2巻から使っている Squid の whitelist(42 行)には .quay.io・.ghcr.io・github.com 系が既に入っています。Cilium のイメージは quay.io/cilium/、CLI は GitHub Releases なので、追加が必要なのは Helm リポジトリの 1 行だけです。
実行コマンド(alma-proxy・root):
# cp /etc/squid/whitelist.txt /etc/squid/whitelist.txt.bak-11
# echo 'helm.cilium.io' >> /etc/squid/whitelist.txt
# systemctl reload squid
# wc -l /etc/squid/whitelist.txt
実行結果:
43 /etc/squid/whitelist.txt
Cilium CLI の導入
/usr/local/bin ではなく /usr/bin に置きます。 sudo の secure_path に /usr/local/bin が含まれないためです(第1回の kube-bench・第10回の runsc と同じ理由)。
実行コマンド(k8s-ops・developer):
$ CLI_VER=v0.19.6
$ curl -sSL --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CLI_VER}/cilium-linux-amd64.tar.gz{,.sha256sum}
$ sha256sum --check cilium-linux-amd64.tar.gz.sha256sum
$ sudo tar xzvfC cilium-linux-amd64.tar.gz /usr/bin
$ rm -f cilium-linux-amd64.tar.gz cilium-linux-amd64.tar.gz.sha256sum
$ cilium version --client
実行結果:
cilium-linux-amd64.tar.gz: 完了
cilium
cilium-cli: v0.19.6 compiled with go1.26.4 on linux/amd64
cilium image (default): v1.19.5
cilium image (stable): v1.19.6
CLI が既定で使うイメージは最新安定版より 1 パッチ古いことがあります。
上の出力では cilium image (default): v1.19.5 に対して (stable): v1.19.6 です。Helm の --version 1.19.6 によるピン留めが必須である理由がこれです。第2巻から一貫している「パッチバージョンまで明示する」方針をここでも守ります。
Pod CIDR を 10.244.0.0/16 のまま維持する
Cilium chart の Pod CIDR の既定値は 10.0.0.0/8 です。 これをそのまま使うと、本書ラボの既存設定 4 つと衝突します。
| 衝突先 | 値 | 問題 |
|---|---|---|
| Service CIDR | 10.96.0.0/12 | 10.0.0.0/8 に内包される |
| ノード間ネットワーク | 10.0.10.0/24 | 同上 |
firewalld の trusted ゾーン | 10.244.0.0/16 / 10.96.0.0/12 / 10.0.10.0/24 | 別 CIDR にすると許可から外れる |
/etc/profile.d/proxy.sh の no_proxy | 10.244.0.0/16 | 別 CIDR にするとプロキシ経由になる |
公式移行ガイドの例(10.245.0.0/16)を採ると、firewalld と no_proxy と第3回以降の記述をすべて直すことになります。本書は 10.244.0.0/16 を維持します。 Calico を完全に削除してから Cilium に割り当てさせるので、重複は起きません。
やってみよう①:Calico → Cilium 一括移行
開始前に必ず読んでください。
ステップ 2 で Calico を削除した時点から先は不可逆です。 kubectl apply -f calico.yaml で Calico を戻しても、Cilium が書き換えた CNI 設定ファイル・Pod の IP 割当・ノードのルーティングは噛み合わず、健全な状態には戻りません。戻す手段は VM スナップショットからの復元だけです。 演習前に全 VM のスナップショットを停止状態で取得してください。
所要時間の目安: 本書ラボ(5 ノード・111 Pod)で Calico 削除から https://fanclub.local が HTTP 200 に復帰するまで 14 分 31 秒でした(07:52:41 → 08:07:12)。この間、クラスタ上のすべてのサービスが停止します。 大半は全ノードの再起動に費やされます。
ステップ0:admission webhook を退避する
Calico を消すと Pod ネットワークが止まり、その瞬間に Gatekeeper の webhook が応答しなくなります。 apiserver は webhook を呼べずにあらゆるリソース更新を拒否するため、Cilium の導入コマンドすら通らなくなります。
実際に出るエラー:
Error: UPGRADE FAILED: Internal error occurred: failed calling webhook "check-ignore-label.gatekeeper.sh": failed to call webhook: Post "https://gatekeeper-webhook-service.gatekeeper-system.svc:443/v1/admitlabel?timeout=3s": context deadline exceeded
第8回で入れた Gatekeeper が、移行そのものを妨げています。Calico を消す前に webhook 設定をバックアップして外し、移行完了後に戻します。
実行コマンド(k8s-ops・developer):
$ kubectl get validatingwebhookconfigurations gatekeeper-validating-webhook-configuration -o yaml > ~/gatekeeper-vwc.bak-11.yaml
$ kubectl get mutatingwebhookconfigurations gatekeeper-mutating-webhook-configuration -o yaml > ~/gatekeeper-mwc.bak-11.yaml
$ kubectl delete validatingwebhookconfigurations gatekeeper-validating-webhook-configuration
$ kubectl delete mutatingwebhookconfigurations gatekeeper-mutating-webhook-configuration
この間、Gatekeeper の検証は一切効きません。
第8回で作った Constraint(PSS 違反の拒否など)が素通りします。メンテナンス時間中はクラスタへの変更を凍結するという運用上の約束とセットで行う操作です。移行が終わったらステップ 7 で必ず戻してください。
他の webhook はなぜ外さないのか。
本書ラボには cert-manager / Longhorn / MetalLB / kube-prometheus-stack の webhook もあります。外すのは Gatekeeper だけで足りました。理由は 移行中に触るリソースの種類にあります——Cilium の導入で作られるのは kube-system の DaemonSet・Deployment・ConfigMap・CRD であり、それらは他の webhook の対象(Certificate / Volume / IPAddressPool / ServiceMonitor 等)に当たりません。Gatekeeper だけが「すべての namespace のラベル変更」を検証対象にしていたため引っかかりました。
自分の環境で移行するときは、kubectl get validatingwebhookconfigurations -o wide で rules の対象リソースと failurePolicy を確認してください。failurePolicy: Fail かつ移行中に触るリソースを対象にしている webhookが、退避すべきものです。
ステップ1:firewalld に Cilium 用のポートを開ける
Calico を消す前に開けておきます。 後から開けると、通信が落ちた状態で原因を探すことになります。
実行コマンド(k8s-cp-01〜03 / k8s-wl-01〜02 の 5 台すべて・root):
# firewall-cmd --permanent --add-port=8472/udp --add-port=4240/tcp
# firewall-cmd --reload
# firewall-cmd --list-ports
実行結果(k8s-wl-01 の例):
4240/tcp 10250/tcp 30000-32767/tcp 8472/udp
| ポート | 用途 | いつ必要になるか |
|---|---|---|
| 8472/udp | VXLAN(Pod 間トラフィックのカプセル化) | 本ステップ |
| 4240/tcp | cilium health(ノード間の到達性チェック) | 本ステップ |
| 51871/udp | WireGuard | やってみよう② |
| 4250/tcp | mutual authentication | 本ラボでは使いません |
なぜ trusted ゾーンに 192.168.1.0/24 を丸ごと入れないのか。
第2巻が 10.0.10.0/24 を trusted に入れたのは、Calico がその NIC を使っていたからです。同じ発想で 192.168.1.0/24 を入れれば Cilium も動きますが、セグメント全体を信頼することになり、最小権限の原則に反します。 Cilium が必要とするのは 2 ポートだけなので、ポート単位で開けます。CKS が問うのはこの判断そのものです。
ステップ2:Calico を削除する
実行コマンド(k8s-ops・developer):
$ kubectl delete -f ~/calico.yaml
実行結果(末尾):
clusterrolebinding.rbac.authorization.k8s.io "calico-tier-getter" deleted
daemonset.apps "calico-node" deleted from kube-system namespace
deployment.apps "calico-kube-controllers" deleted from kube-system namespace
9 秒で完了します。そしてこの瞬間からクラスタのネットワークは止まります。 BIRD が止まり、全ノードから proto bird の経路が消えます。確認してみてください。
実行コマンド(k8s-cp-01・developer):
$ ip route | head -8
実行結果:
default via 192.168.1.1 dev eth0 proto static metric 100
10.0.10.0/24 dev eth1 proto kernel scope link src 10.0.10.125 metric 101
10.244.184.103 dev cali239d1527046 scope link metric 1024
10.244.184.104 dev cali6fbbd6b200d scope link metric 1024
10.244.184.105 dev cali47b43f2bea9 scope link metric 1024
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.125 metric 100
ローカル Pod(cali* デバイス)への経路だけが残り、他ノードへの経路が消えています。 既存の Pod は動き続けていますが、ノードをまたぐ通信はできません。
Calico の CRD も消えたことを確認します。
実行コマンド:
$ kubectl get crd --no-headers | grep -c projectcalico
実行結果:
0
ステップ3:Cilium を導入する
values ファイルを作ります。ファイル名は values-cilium.yaml とし、やってみよう② でも同じファイルに追記します。
ipam:
mode: "cluster-pool"
operator:
# chart の既定は 10.0.0.0/8。Service CIDR(10.96.0.0/12)と
# ノード間ネットワーク(10.0.10.0/24)を内包してしまうため必ず上書きする
clusterPoolIPv4PodCIDRList: ["10.244.0.0/16"]
clusterPoolIPv4MaskSize: 24
routingMode: tunnel
tunnelProtocol: vxlan
# kube-proxy は残す(Cilium による置き換えは本回のスコープ外)
kubeProxyReplacement: "false"
cni:
# 既存の CNI 設定ファイルをリネームして無効化する(chart の既定値)
exclusive: true
policyEnforcementMode: "default"
operator:
replicas: 2
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
実行コマンド(k8s-ops・developer):
$ helm repo add cilium https://helm.cilium.io/
$ helm repo update cilium
$ helm search repo cilium/cilium --versions | head -3
実行結果:
NAME CHART VERSION APP VERSION DESCRIPTION
cilium/cilium 1.19.6 1.19.6 eBPF-based Networking, Security, and Observability
cilium/cilium 1.19.5 1.19.5 eBPF-based Networking, Security, and Observability
実行コマンド:
$ helm install cilium cilium/cilium --version 1.19.6 --namespace kube-system --values values-cilium.yaml
実行結果(抜粋・LAST DEPLOYED は実行時の日時が入ります):
NAME: cilium
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
NOTES:
You have successfully installed Cilium with Hubble Relay and Hubble UI.
Your release version is 1.19.6.
helm install 自体は 2 秒で終わります。
Pod ネットワークが止まっている最中ですが、ステップ 0 で webhook を外してあるので apiserver は受け付けます。 ここで webhook を外し忘れていると context deadline exceeded で失敗し、移行が進まなくなります。
ステップ4:全ノードを順に再起動する
Calico が残した cali* インターフェース・tunl0・iptables ルールを消すため、全ノードを再起動します。 順序は Workload(k8s-wl-01 → k8s-wl-02)→ Control Plane(k8s-cp-01 → 02 → 03 を 1 台ずつ)です。
実行コマンド(各ノード・root):
# systemctl reboot
この順序には理由があります。
Control Plane を 1 台ずつにするのは etcd のクォーラムを保つためです。3 台構成では 2 台が生きていれば書き込みが継続します。2 台同時に落とすとクラスタ全体が読み取り専用になります。Workload を先にするのは、退避先の無い状態で Control Plane を落とすと Pod の再配置が滞るためです。第2巻第6回の kubeadm アップグレード演習と同じ順序です。
再起動後、Calico の痕跡が消えたことを確認します。
実行コマンド(k8s-wl-01・developer):
$ ip -brief link show | grep -c -E 'cali|tunl0'
$ sudo ls /etc/cni/net.d/
実行結果:
0
05-cilium.conflist
10-calico.conflist.cilium_bak
calico-kubeconfig
cali* と tunl0 が 0 個になり、10-calico.conflist が .cilium_bak にリネームされて 05-cilium.conflist が書かれています。 リネームは Cilium の cni-exclusive の働きです。ファイル名の先頭の数字が小さいほど優先されるため、05- が選ばれます。
ステップ5:残った Pod を確認する
実行コマンド(k8s-ops・developer):
$ kubectl get pods -A --no-headers | grep -v Running | grep -v Completed
実行結果:
(出力なし)
ステップ 1 で firewalld を先に開けてあれば、このステップは何もすることがありません。
本書ラボの通し検証では、全ノードの再起動が終わった時点で 111 Pod すべてが Running / Completed でした。
ポートを開けずに移行を進めた場合は、ここで大量の Unknown と CrashLoopBackOff が残ります。 その状態でポートを開けても Pod は自力では戻らないため、該当する Pod を削除して作り直す必要があります(Deployment / DaemonSet / StatefulSet が管理しているので、削除すれば再作成されます)。「順番を守れば手戻りが無い」ことが、この 1 行の出力で確認できます。
ステップ6:移行を確認する
実行コマンド(k8s-ops・developer):
$ cilium status
実行結果:
/¯¯\
/¯¯\__/¯¯\ Cilium: OK
\__/¯¯\__/ Operator: OK
/¯¯\__/¯¯\ Envoy DaemonSet: OK
\__/¯¯\__/ Hubble Relay: OK
\__/ ClusterMesh: disabled
DaemonSet cilium Desired: 5, Ready: 5/5, Available: 5/5
DaemonSet cilium-envoy Desired: 5, Ready: 5/5, Available: 5/5
Deployment cilium-operator Desired: 2, Ready: 2/2, Available: 2/2
Cluster Pods: 69/69 managed by Cilium
Helm chart version: 1.19.6
Cluster Pods: 69/69 managed by Cilium——すべての Pod が Cilium の管理下に入りました(hostNetwork の Pod はこの数に含まれません)。
Pod CIDR の割当も確認します。
実行コマンド:
$ kubectl get ciliumnodes.cilium.io -o custom-columns='NAME:.metadata.name,CIDR:.spec.ipam.podCIDRs'
実行結果:
NAME CIDR
k8s-cp-01 [10.244.1.0/24]
k8s-cp-02 [10.244.3.0/24]
k8s-cp-03 [10.244.4.0/24]
k8s-wl-01 [10.244.0.0/24]
k8s-wl-02 [10.244.2.0/24]
Calico の /26 ブロックと違い、/24 単位でノードに割り当てられています。 CIDR は指定どおり 10.244.0.0/16 の中に収まっています。
ステップ7:Gatekeeper の webhook を戻す
実行コマンド(k8s-ops・developer):
$ kubectl apply -f ~/gatekeeper-vwc.bak-11.yaml
$ kubectl apply -f ~/gatekeeper-mwc.bak-11.yaml
ステップ8:動いていることを確認する
実行コマンド(k8s-ops・developer):
$ curl -k -s --noproxy '*' -o /dev/null -w 'HTTP %{http_code}\n' https://fanclub.local/api/members
$ kubectl get pods -A --no-headers | grep -v Running | grep -v Completed | wc -l
$ kubectl get networkpolicy -n fanclub --no-headers | wc -l
$ kubectl get gateway -A --no-headers
実行結果:
HTTP 200
0
9
fanclub fanclub-gateway traefik 192.168.1.200 True 3d5h
| 確認項目 | 期待値 |
|---|---|
| 異常 Pod | 0(総数は 111。103 − Calico 6 + Cilium 14) |
cilium-dbg status の Cluster health | 5/5 reachable |
https://fanclub.local/api/members | HTTP 200 + 会員 3 名の JSON |
| Gateway | fanclub-gateway が PROGRAMMED True |
| Longhorn | 4 ボリューム attached healthy |
| ArgoCD | fanclub-api-prod が Synced / Healthy |
| NetworkPolicy | 第3回の 9 本がそのまま有効 |
第3回で書いた NetworkPolicy は 1 文字も変えずに Cilium で機能します。
Cilium は Kubernetes 標準の NetworkPolicy をそのままエンフォースするためです。CNI を替えても L3/L4 のポリシーを書き直す必要はありません。本回で追加する L7 制御は、この上に載る別のレイヤーです。
ArgoCD は移行中に何もしません。
selfHeal: true で動いているので心配になりますが、ArgoCD が見ているのは Git のマニフェストとクラスタ上のリソースの差分であり、CNI の入れ替えはその対象外です。移行前後で fanclub-api-prod は Synced / Healthy のままでした。Pod の IP が変わっても ArgoCD は差分と見なしません——IP はマニフェストに書かれていないからです。
試験ではこう問われる。
CNI の移行手順そのものは CKS では問われません。 試験環境の CNI は最初から用意されています。本演習で試験に直結するのは cilium status でクラスタの健全性を読めることと、cilium-dbg status --verbose で「どのノードと通信できていないか」を切り分けられることの 2 点です。
移行で踏む 4 つの落とし穴
演習①で「なぜその手順なのか」を、実際に踏んだ順に整理します。別の環境で移行するときに必要になる知識です。
| # | 落とし穴 | 症状 | 対処 |
|---|---|---|---|
| 1 | Calico の IP 自動検出が cilium_host を拾う | calico-node が全台 0/1・proto bird の経路が全消滅・クロスノード通信が全滅 | IP_AUTODETECTION_METHOD=interface=eth1(共存させる場合のみ。一括移行では Calico を先に消すので起きない) |
| 2 | CNI 不在の間、admission webhook がクラスタをロックする | helm の操作が context deadline exceeded で失敗 | 移行前に webhook を退避する |
| 3 | firewalld が VXLAN と health を塞ぐ | Cluster health: 1/5・4240: connect: no route to host・新規 Pod から DNS が引けない | 8472/udp + 4240/tcp を開ける |
| 4 | 設定を変えても agent が再起動しない | ConfigMap は更新されるのに挙動が変わらない | kubectl rollout restart ds/cilium -n kube-system |
落とし穴 3 の診断が本回でいちばん学びが大きい
firewalld を開けずに移行を進めると、Pod は起動するのに新しく作った Pod から DNS が引けません。原因を cilium-dbg で追います。
実行コマンド(k8s-ops・developer):
$ CPOD=$(kubectl get pods -n kube-system -l k8s-app=cilium --field-selector spec.nodeName=k8s-wl-01 -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec -n kube-system $CPOD -c cilium-agent -- cilium-dbg status --verbose | grep -A 30 'Cluster health'
実行結果(抜粋):
Cluster health: 1/5 reachable
k8s-cp-01:
Host connectivity to 192.168.1.125:
ICMP to stack: OK, RTT=411.866µs
HTTP to agent: Get "http://192.168.1.125:4240/hello": dial tcp 192.168.1.125:4240: connect: no route to host
Endpoint connectivity to 10.244.1.107:
ICMP to stack: Connection timed out
ICMP は通るのに TCP だけ no route to host。
これは firewalld の REJECT(icmp-host-prohibited)の典型的な見え方です。ping が通ることを理由に「ネットワークは問題ない」と判断すると、原因にたどり着けません。 経路(route)の問題ではなくフィルタの問題である、という切り分けがここで要求されます。
ポートを開けると、プローブの周期(約 2 分 24 秒)ごとに 1/5 → 3/5 → 5/5 reachable と回復します。新規 Pod からの名前解決も通るようになります。
cilium と cilium-dbg の違い。
cilium は k8s-ops に入れた CLI で、apiserver 経由でクラスタ全体を見ます。cilium-dbg は agent Pod の中にあるコマンドで、そのノードの datapath の状態を見ます。名前は似ていますが見ている対象が違います。ノード単位の切り分けには cilium-dbg を使います。
第3回で NetworkPolicy のトラブルシュートを扱ったときと同じで、「疎通しているように見えて特定のポートだけ落ちている」という切り分けの型です。第2巻第3回で HAProxy が 6443 に繋げなかったとき(SELinux の haproxy_connect_any)も構造は同じでした。
移行後に残る Calico の痕跡
| 残るもの | 場所 |
|---|---|
10-calico.conflist.cilium_bak | 全ノードの /etc/cni/net.d/ |
calico-kubeconfig | 同上 |
calico / calico-ipam バイナリ | 全ノードの /opt/cni/bin/ |
本書ラボでは残します(切り戻しの痕跡として、また「CNI を替えても古い CNI プラグインのバイナリは残る」ことを見せるため)。本番環境なら削除する判断になります——使われないバイナリがノード上に残ることは攻撃面の増加であり、第7回で扱った原則に反します。
透過暗号化は何を守り、何を守らないのか
ここから本回の主題である暗号化に入ります。まず「何を守るのか」を先に確定させます。
透過暗号化とは
アプリケーションを一切変更せずに、CNI がノード間の Pod トラフィックを暗号化する方式です。アプリは自分が暗号化されていることを知りません。Cilium は WireGuard と IPsec をサポートします。
| 項目 | 値 |
|---|---|
| Helm 値 | encryption.enabled=true / encryption.type=wireguard |
| chart の既定 | ipsec(WireGuard を使うなら明示指定が必要) |
| カーネル要件 | Linux 5.6 以降 + CONFIG_WIREGUARD(AlmaLinux 10.2 のカーネル 6.12 に同梱済み) |
| ポート | 51871/udp |
| インターフェース | cilium_wg0 |
カーネル側の準備は要りません。確認だけしておきます。
実行コマンド(k8s-wl-01・developer):
$ modinfo wireguard | head -4
実行結果:
filename: /lib/modules/6.12.0-211.22.1.el10_2.x86_64/kernel/drivers/net/wireguard/wireguard.ko.xz
alias: net-pf-16-proto-16-family-wireguard
alias: rtnl-link-wireguard
version: 1.0.0
暗号化されないもの
公式ドキュメントが明記している除外があります。「暗号化を有効にした = すべての通信が暗号化された」ではありません。
- 同一ノード内の Pod 間通信(そもそもノード外へ出ないため)
encryption.nodeEncryption=trueを有効にしても Control Plane Node 間のトラフィックは自動的に除外される- XDP 有効時の LoadBalancer / NodePort(Direct Server Return)
この 3 つは CKS で問われうる論点です。 「Pod 間通信を暗号化した」と報告するとき、同一ノード内の通信は対象外である、という前提を共有できているかが実務では効いてきます。
トンネルモードとの併用は二重カプセル化になる
公式の記述は「pod to pod traffic is encapsulated twice」です。本書ラボは VXLAN(tunnel)モードなので、VXLAN で包んだものを WireGuard でさらに包みます。
[ Pod のパケット ]
↓ VXLAN でカプセル化
[ VXLAN ヘッダ | Pod のパケット ]
↓ WireGuard でカプセル化・暗号化
[ WireGuard ヘッダ | 暗号化された ( VXLAN ヘッダ | Pod のパケット ) ]
↓ eth0 から送出(192.168.1.x:51871 → 192.168.1.y:51871)
MTU とスループットには影響します。本書ラボの規模では差が測れませんが、本番環境では MTU の設計が必要になる点を覚えておいてください。
やってみよう②:WireGuard 透過暗号化と「外は暗号・中は平文」の実証
暗号化の証拠を自分の目で取ることが、この演習の目的です。 「有効にしました」で終わらせません。
ステップ1:暗号化前の姿を記録する
ノードに tcpdump が入っていないので導入します。
実行コマンド(k8s-wl-01・root):
# dnf install -y tcpdump wireguard-tools
実行結果(末尾):
インストール済み:
libibverbs-61.0-1.el10.x86_64
libpcap-14:1.10.4-7.el10.x86_64
systemd-resolved-257-23.el10_2.2.alma.1.x86_64
tcpdump-14:4.99.4-10.el10.x86_64
wireguard-tools-1.0.20250521-1.el10.x86_64
完了しました!
「完了しました!」と和文で出るのはロケールのせいです。
本書ラボの VM は LANG=ja_JP.UTF-8 なので、dnf の出力が和文になります。英語ロケールでは Installed: / Complete! です。本回はこのあと rpm -q の「パッケージ tcpdump はインストールされていません」(英語では package tcpdump is not installed)も出てきます。第2回・第5回と同じ話で、成功/失敗を出力文字列で判定するスクリプトはロケールが変わると壊れます。判定は終了コードで行ってください。
依存で 3 つのパッケージも入ります。
libpcap / libibverbs / systemd-resolved。第7回で扱った「ホスト OS の攻撃面最小化」の観点では、検証のためにノードへツールを入れること自体が攻撃面の増加です。演習の最後に必ず撤去します(ステップ 6)。dnf は不要になった依存パッケージも一緒に削除するので、3 つとも消えることを撤去時に確認します。
暗号化前に、ノード間を流れる VXLAN を覗きます。別のセッションから curl でトラフィックを発生させながら実行してください。
実行コマンド(k8s-wl-01・root):
# tcpdump -i eth0 -nn -c 8 'udp port 8472'
実行結果:
08:08:28.305088 IP 192.168.1.125.58922 > 192.168.1.128.8472: OTV, flags [I] (0x08), overlay 0, instance 6
IP 10.244.0.92.36372 > 10.244.3.225.8443: Flags [P.], seq 1885170421:1885170688, ack 2212232438, win 8072, options [nop,nop,TS val 3149035007 ecr 932886961], length 267
08:08:28.305220 IP 192.168.1.128.46391 > 192.168.1.125.8472: OTV, flags [I] (0x08), overlay 0, instance 9838
IP 10.244.3.225.8443 > 10.244.0.92.36372: Flags [.], ack 2373, win 503, options [nop,nop,TS val 932886990 ecr 3149035007], length 0
出力を 2 行 1 組で読んでください。
上段が 外側(192.168.1.125 → 192.168.1.128 の 8472/udp)、下段が その中身(10.244.0.92 → 10.244.3.225:8443)です。tcpdump が VXLAN のカプセル化を剥がして、内側の Pod IP・ポート・シーケンス番号まで表示しています。 ノード間の経路を覗ける立場にいる者は、どの Pod がどの Pod と何を話しているかを読めます。
中身は HTTP まで見えます。ペイロードを表示して数えます。
実行コマンド(k8s-wl-01・root):
# tcpdump -i eth0 -nn -A -s 0 'udp port 8472' > /tmp/precap.txt
# grep -c -E 'HTTP/1\.|GET /api|members|hanako' /tmp/precap.txt
# grep -E 'HTTP/1\.|GET /api' /tmp/precap.txt | head -6
実行結果:
52
Zqf..?.BPOST /api/prom/push HTTP/1.1
.?."Zqf.HTTP/1.1 204 No Content
POST /api/prom/push HTTP/1.1
..b..:5rHTTP/1.1 204 No Content
=..e.>.:GET /hello HTTP/1.1
.>.:=..eHTTP/1.1 200 OK
内側が 8080 のフローを取り出すと、HTTP のメソッドとパスがそのまま読めます。
08:09:29.565115 IP 192.168.1.128.45619 > 192.168.1.129.8472: OTV, flags [I] (0x08), overlay 0, instance 59516
IP 10.244.3.87.44400 > 10.244.4.66.8080: Flags [P.], seq 115780946:115781311, ack 607075723, win 661, options [nop,nop,TS val 867461634 ecr 483782904], length 365: HTTP: GET /metrics HTTP/1.1
これが暗号化前の姿です。 平文の HTTP が 52 件見えました。この数字を覚えておいてください。暗号化後は 0 件になります。
ステップ2:WireGuard を有効化する
values-cilium.yaml に encryption: を足します。変更後の全量は次のとおりです。
ipam:
mode: "cluster-pool"
operator:
clusterPoolIPv4PodCIDRList: ["10.244.0.0/16"]
clusterPoolIPv4MaskSize: 24
routingMode: tunnel
tunnelProtocol: vxlan
kubeProxyReplacement: "false"
cni:
exclusive: true
policyEnforcementMode: "default"
operator:
replicas: 2
hubble:
enabled: true
relay:
enabled: true
ui:
enabled: true
encryption:
enabled: true
# chart の既定は ipsec なので明示する
type: wireguard
実行コマンド(k8s-ops・developer):
$ helm upgrade cilium cilium/cilium --version 1.19.6 --namespace kube-system --values values-cilium.yaml
$ CPOD=$(kubectl get pods -n kube-system -l k8s-app=cilium --field-selector spec.nodeName=k8s-wl-01 -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec -n kube-system $CPOD -c cilium-agent -- cilium-dbg status | grep Encryption
実行結果:
Encryption: Disabled
helm upgrade は成功したのに Encryption: Disabled のままです。
ConfigMap には enable-wireguard: "true" が書き込まれています。しかし DaemonSet の spec が変わらないため、agent Pod が再起動しません。 Cilium の設定の多くは ConfigMap 経由で渡されるので、値を変えたら agent を明示的に再起動する必要があります。
ConfigMap を確認してから再起動します。
実行コマンド:
$ kubectl get cm -n kube-system cilium-config -o jsonpath='{.data}' | tr ',' '\n' | grep -i wireguard
$ kubectl rollout restart ds/cilium -n kube-system
$ kubectl rollout status ds/cilium -n kube-system
実行結果:
"enable-wireguard":"true"
"wireguard-persistent-keepalive":"0s"
daemonset.apps/cilium restarted
Waiting for daemon set "cilium" rollout to finish: 4 of 5 updated pods are available...
daemon set "cilium" successfully rolled out
ステップ3:クラスタが止まる —— 51871/udp を開ける
agent の再起動が終わると、暗号化は有効になります。
実行コマンド(k8s-ops・developer):
$ CPOD=$(kubectl get pods -n kube-system -l k8s-app=cilium --field-selector spec.nodeName=k8s-wl-01 -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec -n kube-system $CPOD -c cilium-agent -- cilium-dbg status | grep Encryption
実行結果:
Encryption: Wireguard [NodeEncryption: Disabled, cilium_wg0 (Pubkey: GsOw5gvmK6QDIaQjmbxwNZCx0N7/peHjkzc6l0Utl3g=, Port: 51871, Peers: 4)]
ところが、この瞬間にクラスタの通信が落ちます。
実行コマンド:
$ curl -k -s --noproxy '*' -m 10 -o /dev/null -w 'HTTP %{http_code}\n' https://fanclub.local/api/members
実行結果:
HTTP 000
移行のときと同じ落とし穴です。今度は 51871/udp が塞がっています。
Pod 間通信は WireGuard トンネル(51871/udp)を通るようになったのに、firewalld がそのポートを許可していません。暗号化方式を変えるたびに新しいポートが必要になる——この一般則を、2 回踏むことで体得してください。
実行コマンド(k8s-cp-01〜03 / k8s-wl-01〜02 の 5 台すべて・root):
# firewall-cmd --permanent --add-port=51871/udp
# firewall-cmd --reload
# firewall-cmd --list-ports
実行結果(k8s-wl-01 の例):
4240/tcp 10250/tcp 30000-32767/tcp 8472/udp 51871/udp
30 秒以内に HTTP 200 に復帰します(実測 0.023〜0.032 秒の応答)。VXLAN のときと違って agent の再起動は不要です。ファイアウォールが開いた瞬間から WireGuard のハンドシェイクが成立します。
ステップ4:暗号化されていることを確認する
実行コマンド(k8s-ops・developer):
$ kubectl exec -n kube-system $CPOD -c cilium-agent -- cilium-dbg encrypt status
実行結果:
Encryption: Wireguard
Interface: cilium_wg0
Public key: GsOw5gvmK6QDIaQjmbxwNZCx0N7/peHjkzc6l0Utl3g=
Number of peers: 4
公開鍵は環境ごとに、そして agent を作り直すたびに変わります。
上の値と一致する必要はありません。鍵は Cilium が起動時に自動生成します——WireGuard を使うのに鍵の管理作業は発生しません(IPsec では鍵の Secret を自分で作ります。後述)。
ホスト側からも見ます。
実行コマンド(k8s-wl-01・root):
# wg show
実行結果(抜粋):
interface: cilium_wg0
public key: GsOw5gvmK6QDIaQjmbxwNZCx0N7/peHjkzc6l0Utl3g=
private key: (hidden)
listening port: 51871
fwmark: 0xe00
peer: Fa8bqxHQOyaQUx92mV90uomPQsPeSCvGLFAFSOTtK0g=
endpoint: 192.168.1.126:51871
allowed ips: 192.168.1.126/32
latest handshake: 57 seconds ago
transfer: 386.54 KiB received, 179.05 KiB sent
peer: 5JH+u1MYudcpiW8WHTBset9KeGlx5LpgvztSWeRQszs=
endpoint: 192.168.1.129:51871
allowed ips: 192.168.1.129/32
latest handshake: 57 seconds ago
transfer: 21.33 MiB received, 2.13 GiB sent
peer: V9skN3O5f2qqMV/Tw7gRZ7/41PEPvLbmnka3rVgvv3U=
endpoint: 192.168.1.125:51871
allowed ips: 192.168.1.125/32
latest handshake: 59 seconds ago
transfer: 5.26 MiB received, 1.67 MiB sent
peer: ayoYBoPjnp2NbXUI5nbOplTWkIkVqUDwiSprAIlrTUo=
endpoint: 192.168.1.127:51871
allowed ips: 192.168.1.127/32
latest handshake: 1 minute ago
transfer: 148.04 KiB received, 77.12 KiB sent
allowed ips を読んでください。
Pod の CIDR ではなく ノード IP の /32 になっています。Cilium の WireGuard は「VXLAN の外側(ノード間の UDP)をまるごと暗号化する」方式であり、Pod ごとに鍵を持つわけではありません。ノード間に 1 本ずつトンネルが張られ、その中を全 Pod のトラフィックが流れます。ノードが 5 台なので、各ノードから見た peer は 4 つです。
ステップ5:「外は暗号・中は平文」を tcpdump で対比する
この演習の成果物です。 別セッションで curl を繰り返しながら、2 か所を覗いて対比します。
実行コマンド(k8s-wl-01・root・その 1):
# tcpdump -i eth0 -nn -c 8 'udp port 51871'
実行結果:
08:12:47.904451 IP 192.168.1.125.51871 > 192.168.1.128.51871: UDP, length 416
08:12:47.904453 IP 192.168.1.125.51871 > 192.168.1.128.51871: UDP, length 1440
08:12:47.904454 IP 192.168.1.125.51871 > 192.168.1.128.51871: UDP, length 944
08:12:47.905003 IP 192.168.1.128.51871 > 192.168.1.125.51871: UDP, length 144
08:12:47.905010 IP 192.168.1.128.51871 > 192.168.1.125.51871: UDP, length 496
08:12:47.934179 IP 192.168.1.125.51871 > 192.168.1.128.51871: UDP, length 416
08:12:47.934210 IP 192.168.1.125.51871 > 192.168.1.128.51871: UDP, length 1440
08:12:47.934211 IP 192.168.1.125.51871 > 192.168.1.128.51871: UDP, length 944
実行コマンド(その 2・VXLAN はまだ見えるか):
# timeout 15 tcpdump -i eth0 -nn -c 8 'udp port 8472' ; echo "exit=$?"
実行結果:
0 packets captured
exit=124
VXLAN は 1 件も捕まりません。 WireGuard の内側に隠れたためです。
実行コマンド(その 3・ペイロードに平文が無いか):
# tcpdump -i eth0 -nn -A -c 4 'udp port 51871' > /tmp/cap-a.txt
# grep -c -E 'HTTP/1|GET /|members|hanako' /tmp/cap-a.txt
実行結果:
0
ダンプの中身はランダムなバイト列になります。
08:13:08.248992 IP 192.168.1.128.51871 > 192.168.1.129.51871: UDP, length 144
E...|...@.y.........................f.......
!......rqM..] .>..".;..!qJ....n2~..G.....[.'.......4>.....t.^WRi.......d_*x#..;h.....Q.n.v..#.....;.....Yn_/:1.mZ.J....MC...q..
08:13:08.275049 IP 192.168.1.125.51871 > 192.168.1.128.51871: UDP, length 416
E....!..@. ....}..........Z.....]:.=.$......(n.....}5..)<1...8...e.%...DW...V.ZV$...........8.U........5...t....Aa.o..lT.3.A ....x....(..f.I.....L._k.<&.`Z.l..)...EV.RHc.E.j<?3....=..p.#..6..q.-..J....../;...0V.jX. #M..p.........o.8.l...Z0n.".@>5....R9.../.wX]..jB.^.U.E.l.i[.....Y......u.8.r.]'...Y2..z..MGG...Nf...........{M.:}.4.........D.m..%.!C.....$.^T1.h}.. .;..O.
実行コマンド(その 4・暗号化の内側を見る):
# tcpdump -i cilium_vxlan -nn -c 5 'tcp port 8080'
実行結果:
08:14:29.565583 IP 10.244.3.87.35888 > 10.244.4.66.8080: Flags [P.], seq 1558303605:1558303970, ack 2859453381, win 632, options [nop,nop,TS val 867761634 ecr 484082903], length 365: HTTP: GET /metrics HTTP/1.1
08:14:29.568276 IP 10.244.4.66.8080 > 10.244.3.87.35888: Flags [P.], seq 1:5146, ack 365, win 502, options [nop,nop,TS val 484097899 ecr 867761634], length 5145: HTTP: HTTP/1.1 200 OK
08:14:29.568330 IP 10.244.3.87.35888 > 10.244.4.66.8080: Flags [.], ack 5146, win 632, options [nop,nop,TS val 867761637 ecr 484097899], length 0
暗号化前に見えていたのと同じ通信です(10.244.3.87 → 10.244.4.66:8080 の GET /metrics)。外側では読めなくなったものが、内側では変わらず読めます。
| 観測点 | フィルタ | 暗号化【前】 | 暗号化【後】 |
|---|---|---|---|
eth0 | udp port 8472(VXLAN) | 見える(内側の Pod IP・ポートまで) | 0 件 |
eth0 | udp port 51871(WireGuard) | 0 件 | 見える(length 144 / 416 / 944 / 1440) |
eth0 | -A で平文 HTTP を数える | 52 件(POST /api/prom/push HTTP/1.1 等) | 0 件(ランダムなバイト列) |
cilium_vxlan | tcp port 8080 | (暗号化の内側なので同じ) | GET /metrics HTTP/1.1 が見える |
この 4 行の対比が、暗号化の証明です。
「eth0 では読めないが、cilium_vxlan(暗号化の内側)では読める」——2 点観測することで、暗号化がどの層で効いているのかが具体的に分かります。 ノードに侵入されて cilium_vxlan を覗かれれば中身は読めます。透過暗号化が守るのは「ノードとノードのあいだ」だけです。
ステップ6:後片付け
実行コマンド(k8s-wl-01・root):
# dnf remove -y tcpdump wireguard-tools
# rpm -q tcpdump wireguard-tools systemd-resolved libpcap
実行結果:
パッケージ tcpdump はインストールされていません
パッケージ wireguard-tools はインストールされていません
パッケージ systemd-resolved はインストールされていません
パッケージ libpcap はインストールされていません
依存で入った 3 つも一緒に消えました。 dnf は「他に必要としているパッケージが無くなった依存」を自動的に削除します。導入前の状態に戻せたことを、rpm -q で確認してから次へ進んでください。
wg show は使えなくなりますが、cilium-dbg encrypt status は agent Pod の中で常に使えます。 運用ではノードにツールを常駐させない、という判断です。
試験ではこう問われる。
「クラスタの Pod 間通信を WireGuard で暗号化せよ」という粒度で出ます。手が覚えているべきは encryption.enabled=true / encryption.type=wireguard の 2 行、設定後に agent を再起動する必要があること、確認手段が cilium-dbg encrypt status であることの 3 点です。tcpdump による証明は試験では求められませんが、「暗号化できているか」を自分で確かめる手段を持っているかどうかが実務では分かれ目になります。
mutual authentication は暗号化しない —— CKS で問われる区別
Cilium には mutual authentication という機能があります。SPIFFE / SPIRE を使って Pod にアイデンティティを配り、通信の両端で相互に身元を確認する仕組みです。これを「mTLS」と呼ぶ資料は多いのですが、一般に言う mTLS とは指す範囲が違います。
chart のコメントが答えを書いている
実行コマンド(k8s-ops・developer):
$ helm show values cilium/cilium --version 1.19.6 | grep -B 2 -A 1 '^ mutual:'
実行結果(該当部分):
# Configuration for Cilium's service-to-service mutual authentication using TLS handshakes.
# Note that this is not full mTLS support without also enabling encryption of some form.
# Current encryption options are WireGuard or IPsec, configured in encryption block above.
mutual:
探しているコメントは authentication: の真下ではなく、その 11 行下の mutual: の直前にあります。 grep -A 4 '^authentication:' では届きません——そこに出るのは enabled: false までです。values.yaml を読むときは、コメントがどのキーに掛かっているかを見てから範囲を決めます。
「暗号化を別途有効にしない限り、これは完全な mTLS ではない」と chart 自身が書いています。公式ドキュメントも「users must enable encryption」と述べています。
| 方式 | 機密性(盗聴を防ぐ) | 相互認証(相手が誰かを保証する) |
|---|---|---|
| 透過暗号化(WireGuard / IPsec) | あり | なし |
| Cilium mutual authentication(SPIFFE/SPIRE) | なし | あり |
| 両方を併用 | あり | あり |
「mTLS」という言葉は一般に「暗号化 + 相互認証」を指しますが、Cilium の mutual authentication が担うのは認証だけです。
暗号化は encryption ブロックの担当で、両者は独立して有効化します。この 2 つを 1 つのものと思い込んでいると、設問の選択肢を読み分けられません。

記法(暗記対象)
CiliumNetworkPolicy の ingress / egress ブロックの中に authentication: を書きます。
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: mutual-auth-example
namespace: fanclub
spec:
endpointSelector:
matchLabels:
app: fanclub-backend
ingress:
- fromEndpoints:
- matchLabels:
app: fanclub-frontend
authentication:
mode: "required"
有効化に必要な Helm 値は 3 つすべてです。
authentication:
# これを忘れると helm template がバリデーションで落ちる
enabled: true
mutual:
spire:
enabled: true
install:
enabled: true
| 項目 | 値 |
|---|---|
| ステータス | Beta(v1.19.6 時点でも “This is a beta feature”) |
| namespace | cilium-spire |
| コンポーネント | spire-server(StatefulSet・PVC 1Gi RWO)+ spire-agent(DaemonSet) |
| イメージ | ghcr.io/spiffe/spire-server:1.9.6 / spire-agent:1.9.6 |
| agent 間ポート | 4250/tcp |
| trust domain | spiffe.cilium |
| SPIFFE ID | ワークロード = spiffe://spiffe.cilium/identity/$IDENTITY_ID / agent = spiffe://spiffe.cilium/cilium-agent |
本書ラボでは動きません —— そしてその理由は第6回にあります
実際に有効化すると、spire-server が CrashLoopBackOff になります。
実行コマンド(k8s-ops・developer):
$ kubectl logs -n cilium-spire spire-server-0 -c spire-server --tail=3
実行結果:
time="..." level=error msg="Fatal run error" error="listen unix /tmp/spire-server/private/api.sock: bind: permission denied"
time="..." level=error msg="Server crashed" error="listen unix /tmp/spire-server/private/api.sock: bind: permission denied"
permission denied ですが、プロセスは root で動いています。第6回で習得した読み方で AVC を確認します。
実行コマンド(k8s-wl-02・root):
# grep -i spire /var/log/audit/audit.log | tail -2
実行結果:
type=AVC msg=audit(1785252460.067:4207): avc: denied { write } for pid=42086 comm="spire-server" name="sockets" dev="tmpfs" ino=10957 scontext=system_u:system_r:container_t:s0:c698,c876 tcontext=system_u:object_r:container_var_run_t:s0 tclass=dir permissive=0
type=SYSCALL msg=audit(1785252460.067:4207): arch=c000003e syscall=49 success=no exit=-13 ... comm="spire-server" exe="/opt/spire/bin/spire-server" subj=system_u:system_r:container_t:s0:c698,c876 ... SYSCALL=bind AUID="unset" UID="root"
読み方は第6回と同じです。
| フィールド | 値 | 意味 |
|---|---|---|
scontext | container_t:s0:c698,c876 | MCS カテゴリを持つコンテナ(主体) |
tcontext | container_var_run_t:s0 | カテゴリを持たないディレクトリ(客体) |
tclass / 操作 | dir / write | ディレクトリへの書き込み |
SYSCALL | bind(49) | UNIX ドメインソケットの作成 |
exit | -13 | EACCES |
UID | root | パーミッションではなく SELinux が拒否している |
対象は spire-agent と共有する hostPath のソケットディレクトリです。hostPath は kubelet の自動リラベル対象外——これは第6回で確定させた事実です。コンテナ側には MCS カテゴリが付き、hostPath 側には付かないため、書き込みが拒否されます。
第10回と同じ構図が 2 度目です。
第10回では gVisor(runsc)が SELinux の MCS ラベルを拒否して Pod が起動しませんでした。今回は SPIRE が hostPath のソケットに bind できません。どちらも「第6回で入れたハードニングが、後から入れる機能を止めている」という同じ形をしています。
防御を足すことは、機能を減らすことでもあります。 セキュリティ設計とは「何を守り、その代わりに何を諦めるか」を決める作業である——第3巻を通じて繰り返し出てくる主題です。
本回では mutual authentication を有効化しません。 記法を暗記対象として提示し、実機での確認は第19回の重点復習(AppArmor・Kata と同じ枠)に送ります。
試験対策として不足しないのか
CKS の公式コンピテンシーは「Pod 間通信の暗号化を Cilium / Istio で実装する」であり、SPIFFE / SPIRE の構築そのものは問われません。本回で暗記対象とするのは次の 2 点です。
authentication: {mode: "required"}の記法(ingress/egressブロックの中に書く)- 「認証と暗号化は別物」という区別——これは選択肢の読み分けで直接効きます
実機で SPIRE を動かせないことは、試験対応上の欠落にはなりません。むしろ 「ハードニングが後から入れる機能を止める」という現実を先に知っておくことのほうが、CKS 保持者として現場で問われます。
Istio の PeerAuthentication STRICT(記法のみ)
CKS のコンピテンシーは Cilium, Istio と 2 つを併記しています。「どちらかを知っていればよい」のか「両方」なのかは公式に明示がありません。 本書は実装は Cilium で行い、Istio は記法を押さえる方針を採ります。判断の根拠は 2 つです。
- Istio の導入はサイドカー注入を伴い、クラスタ全体の構成が変わる。試験の 120 分でゼロから構築させる設問は現実的でない
- 設問が「暗号化を有効にせよ」の形なら、CNI 層で完結する Cilium のほうが手数が少ない。逆に「既に Istio が入っている環境で mTLS を STRICT にせよ」なら記法だけで足りる
つまり Cilium は手を動かせるように、Istio は記法を書けるように——という配分です。
Istio はサイドカー(Envoy)を各 Pod に注入し、サイドカー間で mTLS を張ります。Cilium が「CNI 層で暗号化する」のに対し、Istio は「アプリの隣で TLS を終端する」——働く層が違います。
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: default
namespace: fanclub
spec:
mtls:
mode: STRICT
| モード | 意味 |
|---|---|
STRICT | mTLS 必須。平文を拒否する |
PERMISSIVE | mTLS と平文の両方を受け付ける(移行期に使う) |
DISABLE | mTLS を使わない |
Istio の mTLS は「暗号化 + 相互認証」の両方を提供します。
Cilium が 2 つの機能に分かれているのに対し、Istio は 1 つの設定で両方が有効になります。「mTLS」という同じ言葉が製品によって指す範囲が違う——この点を押さえておくと、設問で迷いません。
Cilium L7 NetworkPolicy —— L3/L4 の限界とその先
第3回で書いたポリシーは何を許していたか
実行コマンド(k8s-ops・developer):
$ kubectl get networkpolicy -n fanclub allow-backend-from-frontend -o yaml | sed -n '/spec:/,$p'
実行結果:
spec:
ingress:
- from:
- podSelector:
matchLabels:
app: fanclub-frontend
ports:
- port: 8080
protocol: TCP
podSelector:
matchLabels:
app: fanclub-backend
policyTypes:
- Ingress
これは「frontend から backend の 8080 への TCP」を許しています。その TCP の上で何を喋るかは一切問いません。 frontend が侵害されれば、攻撃者は backend の任意のエンドポイントを叩けます。
CiliumNetworkPolicy が足すもの
| 項目 | 内容 |
|---|---|
| 記法 | toPorts[].rules.http[] に method / path / host / headers |
| マッチング | path・method・host はいずれも POSIX 正規表現。省略時はすべて許可 |
| 拒否時の挙動 | パケットドロップではなく HTTP 403 access denied を返す |
| 実装 | ノードローカルの Envoy(本ラボでは cilium-envoy DaemonSet が 5 台稼働) |
| 制約 | ポートレンジ非対応(DNS を除く) |
L3/L4 の拒否は「タイムアウト」、L7 の拒否は「403」。
この違いは切り分けの手がかりになります。接続そのものは成立していて HTTP レイヤーで断られているのか、それとも接続すらできていないのか——返ってくるものが違えば、見るべき設定も違います。
本ラボの実トポロジ
ポリシーを書く前に、誰が誰に通信しているのかを確定させます。第3回で設計した経路そのものです。
ブラウザ
→ Gateway(Traefik・192.168.1.200:443)
→ HTTPRoute(/ をすべて frontend へ)
→ fanclub-frontend(nginx)
├─ / 静的ファイル
└─ /api/ proxy_pass → fanclub-backend:8080
→ fanclub-backend(Payara)
→ fanclub-db(PostgreSQL 5432)
Prometheus(monitoring)→ fanclub-backend:8080/metrics
nginx の設定を実際に見ると、/api/ を backend へ中継していることが確認できます。
実行コマンド(k8s-ops・developer):
$ FPOD=$(kubectl get pods -n fanclub -l app=fanclub-frontend -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec -n fanclub $FPOD -- grep -A 5 'location /api/' /etc/nginx/conf.d/default.conf
実行結果:
location /api/ {
proxy_pass http://fanclub-backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
Traefik から backend への直接の経路はありません。 L7 ポリシーの起点は frontend と Prometheus の 2 つです。

設計する L7 ポリシー
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: fanclub-backend-l7
namespace: fanclub
spec:
endpointSelector:
matchLabels:
app: fanclub-backend
ingress:
- fromEndpoints:
- matchLabels:
app: fanclub-frontend
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: "GET"
path: "/api/members$"
- method: "GET"
path: "/api/members/[0-9]+$"
- method: "POST"
path: "/api/members$"
- method: "PUT"
path: "/api/members/[0-9]+$"
- method: "DELETE"
path: "/api/members/[0-9]+$"
- fromEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: monitoring
app.kubernetes.io/name: prometheus
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: "GET"
path: "/metrics$"
別 Namespace の Endpoint は k8s:io.kubernetes.pod.namespace ラベルで指定します。
Kubernetes 標準の NetworkPolicy が namespaceSelector を使うのに対し、Cilium はラベルの名前空間として k8s: プレフィックスを付けた特殊なキーで表現します。暗記対象です。
やってみよう③:L7 NetworkPolicy の適用と HTTP 426 の解決
この演習では意図的に 2 回つまずきます。
1 回目は「L7 ポリシーを書いたのに効かない」、2 回目は「効いた途端にアプリが壊れる」。どちらも設定ミスではなく、L7 制御に踏み込むと必ず出会う構造的な問題です。手順どおりに進めて、両方を自分の目で確認してください。
ステップ1:そのまま適用しても L7 は効かない
上のポリシーを cnp-l7.yaml として保存し、適用します。
実行コマンド(k8s-ops・developer):
$ kubectl apply -f cnp-l7.yaml
$ kubectl get cnp -n fanclub
実行結果:
ciliumnetworkpolicy.cilium.io/fanclub-backend-l7 created
NAME AGE VALID
fanclub-backend-l7 12s True
frontend Pod から backend を叩いて確認します。
実行コマンド:
$ kubectl exec -n fanclub $FPOD -- wget -S -q -O /dev/null http://fanclub-backend:8080/api/members
$ kubectl exec -n fanclub $FPOD -- wget -S -q -O /dev/null http://fanclub-backend:8080/api/admin
実行結果:
HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
wget: server returned error: HTTP/1.1 404 Not Found
/api/admin が 404 です。403 ではありません。
この 404 は backend(Payara)が返しています——つまりリクエストは backend まで届いており、Cilium は何も遮断していません。 L7 ポリシーは適用済みで VALID: True なのに、効いていないのです。
原因は第3回で書いた既存のポリシーです。allow-backend-from-frontend が 8080/TCP をまるごと許可しています。
Cilium のポリシーは加算的で、より広い許可が勝ちます。 「L7 で絞ったつもり」でも、同じ経路を L3/L4 で全許可するルールが並んでいれば、そちらが通り道になります。
ステップ2:既存の L3/L4 ポリシーを外す
実行コマンド(k8s-ops・developer):
$ kubectl get networkpolicy -n fanclub allow-backend-from-frontend -o yaml > ~/np-backend-from-frontend.bak-11.yaml
$ kubectl get networkpolicy -n fanclub allow-backend-from-monitoring -o yaml > ~/np-backend-from-monitoring.bak-11.yaml
$ kubectl delete networkpolicy -n fanclub allow-backend-from-frontend allow-backend-from-monitoring
15 秒ほど待ってから、5 通りの組み合わせを試します。
実行コマンド:
$ PPOD=$(kubectl get pods -n monitoring -l app.kubernetes.io/name=prometheus -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec -n fanclub $FPOD -- wget -S -q -O /dev/null http://fanclub-backend:8080/api/members
$ kubectl exec -n fanclub $FPOD -- wget -S -q -O /dev/null http://fanclub-backend:8080/api/admin
$ kubectl exec -n fanclub $FPOD -- wget -S -q -O /dev/null http://fanclub-backend:8080/metrics
$ kubectl exec -n monitoring $PPOD -c prometheus -- wget -S -q -O /dev/null http://fanclub-backend.fanclub:8080/metrics
$ kubectl exec -n monitoring $PPOD -c prometheus -- wget -S -q -O /dev/null http://fanclub-backend.fanclub:8080/api/members
実行結果:
| 起点 | リクエスト | 結果 |
|---|---|---|
| frontend | GET /api/members | 200 OK |
| frontend | GET /api/admin | 403 Forbidden |
| frontend | GET /metrics | 403 Forbidden |
| Prometheus | GET /metrics | 200 OK |
| Prometheus | GET /api/members | 403 Forbidden |
ここで試したのは GET だけです。
ポリシーには POST / PUT / DELETE の許可も書いてありますが、busybox の wget では送れないため本演習では検証しません。ブラウザから会員を追加・削除して CRUD が通ることは、ステップ 4 の後に確認できます。
同じ backend に対して、起点ごとに許可する HTTP パスが変わっています。
frontend は /api/* だけ、Prometheus は /metrics だけ。これが L7 制御の本体です。 L3/L4 では「8080 に繋げるか繋げないか」しか表現できませんでした。
3 行目に注目してください。 frontend は backend への接続を許可されているのに、/metrics だけは 403 です。「どの Pod から来たか」だけでなく「何を要求したか」で判定していることが、この 1 行で分かります。
重要な設計原則: L7 ポリシーを効かせるには、同じ相手・同じポートを許可する L3/L4 ルールを取り除く必要があります。「L7 を足す」のではなく「L3/L4 を L7 に置き換える」という理解が正しいものです。
ステップ3:しかしブラウザからは 426 になる
Pod 間では意図どおり動きました。ところが外形の確認をすると壊れています。
実行コマンド(k8s-ops・developer):
$ curl -k -s --noproxy '*' -m 10 -D - -o /dev/null https://fanclub.local/api/members
実行結果:
HTTP/2 426
content-type: text/plain
date: ...
server: nginx/1.27.5
content-length: 16
HTTP 426 Upgrade Required。 切り分けの材料を並べます。
| 観測 | 結果 |
|---|---|
| frontend Pod から wget(HTTP/1.1) | 200 |
| Prometheus から(HTTP/1.1) | 200 |
ブラウザ経由(nginx の proxy_pass) | 426 |
frontend の nginx 設定に proxy_http_version | 0 件(既定は HTTP/1.0) |
| Hubble | nginx 経由のリクエストは http-request イベントにすら現れない |
実行コマンド(設定の確認):
$ kubectl exec -n fanclub $FPOD -- grep -c 'proxy_http_version' /etc/nginx/conf.d/default.conf
実行結果:
0
原因は nginx の proxy_pass が既定で HTTP/1.0 を使うことです。
Cilium の L7 ポリシーは通信を Envoy に通しますが、その Envoy は HTTP/1.0 のリクエストを受け付けず、HTTP パースの前段で 426 Upgrade Required を返します。 Cilium 側に HTTP/1.0 を許容する設定はありません。
L7 制御の導入は、アプリが黙って使っていた HTTP バージョンの前提を露呈させます。 L3/L4 のポリシーは「TCP が通るか」しか見ないのでこの問題は起きません。L7 に踏み込むと、プロトコルの細部がそのままセキュリティ設定の成否になります。
ステップ4:アプリ側を直す
default.conf はフロントエンドのイメージに同梱されています(ConfigMap fanclub-frontend-config が持っているのは config.js だけです)。イメージを作り直さずに設定を差し替えるため、既存の ConfigMap に default.conf を足して、同じ名前でマウントします。
変更は GitOps 経由で行います。 第9回・第10回と同じく、リポジトリの base/ を編集して push し、ArgoCD に同期させます。
4-1. base/configmap.yaml に default.conf を追加する
fanclub-frontend-config の data に default.conf を足します(足すのは proxy_http_version 1.1; の 1 行だけで、あとはイメージ同梱の内容と同じです)。
apiVersion: v1
kind: ConfigMap
metadata:
name: fanclub-frontend-config
data:
config.js: |
window.API_BASE_URL = "/api";
# Cilium の L7 NetworkPolicy を有効にすると、nginx の proxy_pass が
# 既定で使う HTTP/1.0 を Cilium の Envoy が拒否して 426 Upgrade Required を返す。
# proxy_http_version 1.1 を明示してイメージ同梱の default.conf を上書きする。
default.conf: |
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location /api/ {
proxy_pass http://fanclub-backend:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
try_files $uri $uri/ =404;
}
location /css/ {
expires 7d;
access_log off;
}
location /js/ {
expires 7d;
access_log off;
}
}
4-2. base/frontend-deployment.yaml にマウントを追加する
volume は既存の config をそのまま使い、volumeMounts に 1 つ足すだけです(同じ ConfigMap の別のキーを別の場所へマウントします)。
volumeMounts:
- name: config
mountPath: /usr/share/nginx/html/config.js
subPath: config.js
# L7 NetworkPolicy 適用時の HTTP 426 を避けるため
# イメージ同梱の default.conf を ConfigMap の内容で上書きする
- name: config
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
- name: nginx-cache
mountPath: /var/cache/nginx
- name: nginx-run
mountPath: /var/run
4-3. commit して push する
実行コマンド(k8s-ops・developer):
$ cd ~/gitops/gitops-fanclub
$ git add -A
$ git commit -m 'frontend: force HTTP/1.1 on proxy_pass for Cilium L7 policy'
$ export no_proxy="$no_proxy,k8s-registry"
$ git push
実行結果:
remote: Processed 1 references in total
To http://k8s-registry:3000/developer/gitops-fanclub.git
e84a4e9..06d8c88 main -> main
git push の前に no_proxy に k8s-registry を足しています。
足さないと Squid 経由になって push が失敗します。第2巻から引き継いでいる本書ラボ固有の作法です。
4-4. ArgoCD の同期を待つ
実行コマンド:
$ kubectl get app fanclub-api-prod -n argocd -o jsonpath='{.status.sync.status} {.status.health.status} {.status.sync.revision}'
実行結果:
Synced Healthy 06d8c88...
実測では push から 30 秒以内に同期が完了しました(syncPolicy は automated: {prune: true, selfHeal: true})。frontend Pod が作り直され、設定が反映されます。
実行コマンド:
$ FPOD=$(kubectl get pods -n fanclub -l app=fanclub-frontend -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec -n fanclub $FPOD -- grep -c 'proxy_http_version' /etc/nginx/conf.d/default.conf
実行結果:
1
適用後の確認。
実行コマンド(k8s-ops・developer):
$ curl -k -s --noproxy '*' -m 10 -o /dev/null -w 'members: HTTP %{http_code}\n' https://fanclub.local/api/members
$ curl -k -s --noproxy '*' -m 10 -o /dev/null -w 'admin: HTTP %{http_code}\n' https://fanclub.local/api/admin
実行結果:
members: HTTP 200
admin: HTTP 403
アプリは復旧し、L7 の制限は効いたままです。 セキュリティ機能を入れるためにアプリケーション側を直す——この順番は現場でしばしば逆転します。 「セキュリティ要件が先にあり、アプリがそれに合わせる」という進め方ができるかどうかが、実装の成否を分けます。
試験ではこう問われる。
「namespace X の Pod Y に対し、Z からの GET /api/* のみを許可する CiliumNetworkPolicy を作成せよ」という粒度で出ます。手が覚えているべきは toPorts[].rules.http[] の 4 階層のネストです。
ingress → fromEndpoints → toPorts → ports / rules → http → method / path
kubectl create のサブコマンドは存在しません(RuntimeClass と同じく手書きになります)。path が正規表現であること、拒否が 403 で返ることも設問の読み分けに効きます。既存の L3/L4 ポリシーが同じ経路を許可していないかの確認も忘れないでください。
Hubble —— 拒否を「見る」
Cilium には Hubble という観測機能が組み込まれています。演習①で hubble.enabled: true を指定したので、すでに動いています。
| 項目 | 値 |
|---|---|
| CLI | cilium agent Pod に /usr/bin/hubble(v1.19.6)が同梱。別途インストール不要 |
| Service | hubble-peer(443)/ hubble-relay(80)/ hubble-ui(80) |
| フローバッファ | Current/Max Flows: 4,095/4,095(既定 4,095 件のリングバッファ) |
Hubble は「保存する」ものではなく「今流れているものを見る」ものです。
既定では 4,095 件のリングバッファで、古いものから消えていきます。監査ログ(第16回)とは役割が違います——後から遡って調べる用途には向きません。第17回で「Networks 面の観測手段」として再登場します。
やってみよう④:Hubble で L7 拒否を観測する
実行コマンド(k8s-ops・developer):
$ kubectl exec -n kube-system $CPOD -c cilium-agent -- hubble status
実行結果:
Healthcheck (via unix:///var/run/cilium/hubble.sock): Ok
Current/Max Flows: 4,095/4,095 (100.00%)
Flows/s: 93.05
観測を開始し、別のセッションで許可されるリクエストと拒否されるリクエストを流します。
実行コマンド:
$ kubectl exec -n kube-system $CPOD -c cilium-agent -- hubble observe --namespace fanclub --follow
実行結果(抜粋):
23:19:47.680: fanclub/fanclub-frontend-59b469cf6-5gcjl:41776 (ID:8580) -> fanclub/fanclub-backend-67dc44fc95-jtv7j:8080 (ID:16266) to-proxy FORWARDED (TCP Flags: SYN)
23:19:47.681: fanclub/fanclub-frontend-59b469cf6-5gcjl:41776 (ID:8580) -> fanclub/fanclub-backend-67dc44fc95-jtv7j:8080 (ID:16266) http-request FORWARDED (HTTP/1.1 GET http://fanclub-backend:8080/api/members)
23:19:47.688: fanclub/fanclub-frontend-59b469cf6-5gcjl:41776 (ID:8580) <- fanclub/fanclub-backend-67dc44fc95-jtv7j:8080 (ID:16266) http-response FORWARDED (HTTP/1.1 200 7ms (GET http://fanclub-backend:8080/api/members))
23:19:48.190: fanclub/fanclub-frontend-59b469cf6-5gcjl:41812 (ID:8580) -> fanclub/fanclub-backend-67dc44fc95-jtv7j:8080 (ID:16266) http-request DROPPED (HTTP/1.1 GET http://fanclub-backend:8080/api/admin)
23:19:48.190: fanclub/fanclub-frontend-59b469cf6-5gcjl:41812 (ID:8580) <- fanclub/fanclub-backend-67dc44fc95-jtv7j:8080 (ID:16266) http-response FORWARDED (HTTP/1.1 403 0ms (GET http://fanclub-backend:8080/api/admin))
Prometheus からの /metrics も同じ画面に出ます。
23:19:44.566: monitoring/prometheus-kube-prometheus-stack-prometheus-0:38910 (ID:59516) -> fanclub/fanclub-backend-67dc44fc95-4vwbz:8080 (ID:16266) http-request FORWARDED (HTTP/1.1 GET http://10.244.4.66:8080/metrics)
23:19:44.569: monitoring/prometheus-kube-prometheus-stack-prometheus-0:38910 (ID:59516) <- fanclub/fanclub-backend-67dc44fc95-4vwbz:8080 (ID:16266) http-response FORWARDED (HTTP/1.1 200 3ms (GET http://10.244.4.66:8080/metrics))
1 行ずつ読みます。
| 表示 | 意味 |
|---|---|
http-request DROPPED | L7 で拒否された。URL がそのまま出る |
http-response FORWARDED (HTTP/1.1 403 ...) | 拒否の応答として 403 が返っている(パケットのドロップではない) |
to-proxy | Envoy を経由した = L7 ポリシーの対象 |
to-overlay / to-endpoint | Envoy を通らない = L3/L4 のみ |
policy-verdict:none ... ALLOWED | ポリシー判定の結果 |
(ID:8580) / (ID:16266) | Cilium のセキュリティアイデンティティ(ラベルの組から導出される番号。backend の 3 Pod はすべて同じ ID——同じラベルを持つ Pod は 1 つのアイデンティティとして扱われます) |
to-proxy と to-overlay の差で「この通信が L7 検査を受けたか」が分かります。
ポリシーを書いたのに to-overlay しか出ないなら、その通信は Envoy を通っていない——つまり L7 ルールの対象外です。ステップ 1 で L7 が効かなかった状態は、この表示で見分けられます。
よく使うフィルタです。
$ hubble observe --namespace fanclub --protocol http
$ hubble observe --verdict DROPPED
$ hubble observe --pod fanclub-backend --last 50
試験ではこう問われる。
Hubble そのものが設問になることは少ないのですが、「ポリシーを書いたが通信できない。原因を調べて直せ」という設問で hubble observe --verdict DROPPED が最短の診断手段になります。kubectl logs では分からない「誰から誰への通信がどの層で落ちたか」が 1 行で出ます。試験時間 120 分のうち診断に使える時間は限られますので、この 1 コマンドを手に入れておく価値は大きいものです。
toFQDNs(DNS ベースの egress 制御)
第3回で扱った egress 制御は IP と CIDR が単位でした。Cilium は FQDN で外部接続先を絞ることができます。Cilium の DNS proxy が DNS 応答を監視して IP を学習する仕組みです。
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: egress-fqdn-example
namespace: fanclub
spec:
endpointSelector:
matchLabels:
app: fanclub-batch
egress:
- toFQDNs:
- matchName: "api.example.com"
- matchPattern: "*.example.com"
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
rules:
dns:
- matchPattern: "*"
toFQDNs を使うときは、DNS への egress を rules.dns 付きで許可する必要があります。
Cilium が DNS 応答を覗けないと、FQDN と IP の対応を学習できないためです。この 1 ブロックを書き忘れるのが定番の失敗で、「ポリシーを書いたのに名前解決すらできない」という症状になります。
本書ラボでも動きます。default namespace に検証用の Pod を立てて確かめます。
実行コマンド(k8s-ops・developer):
$ kubectl run fqdn-probe --image=busybox:1.37 --restart=Never -- sleep 3600
$ kubectl wait --for=condition=Ready pod/fqdn-probe --timeout=90s
$ kubectl exec fqdn-probe -- nslookup registry.k8s.io
実行結果(抜粋):
Address: 34.96.108.209
Non-authoritative answer:
Name: registry.k8s.io
Address: 2600:1901:0:bbc4::
Pod は自分で外部ドメインを解決できています。 これが toFQDNs の前提です。検証用のポリシーを作ります——先ほどの例から namespace と endpointSelector の 2 箇所を変え、許可するのは registry.k8s.io だけにします。
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: fqdn-probe-egress
namespace: default
spec:
endpointSelector:
matchLabels:
run: fqdn-probe
egress:
- toFQDNs:
- matchName: "registry.k8s.io"
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
rules:
dns:
- matchPattern: "*"
実行コマンド:
$ kubectl apply -f cnp-fqdn.yaml
$ sleep 15
$ kubectl exec fqdn-probe -- wget -q -O /dev/null --timeout=8 https://registry.k8s.io/v2/ ; echo "exit=$?"
$ kubectl exec fqdn-probe -- wget -q -O /dev/null --timeout=8 https://github.com/ ; echo "exit=$?"
実行結果:
wget: note: TLS certificate validation not implemented
exit=0
wget: download timed out
exit=1
許可した registry.k8s.io は通り、許可していない github.com は遮断されました。 名前解決自体はどちらも成功する(DNS を許可しているため)のに、接続だけが FQDN 単位で選別されている点に注目してください。
Cilium が学習した対応表も見られます。
実行コマンド:
$ kubectl exec -n kube-system $CPOD -c cilium-agent -- cilium-dbg fqdn cache list
実行結果:
Endpoint Source FQDN TTL ExpirationTime IPs
1629 lookup registry.k8s.io. 15 2026-07-28T23:21:50.289Z 2600:1901:0:bbc4::
1629 lookup registry.k8s.io. 15 2026-07-28T23:21:50.289Z 34.96.108.209
1629 lookup github.com. 18 2026-07-28T23:21:53.597Z 20.27.177.113
cilium-dbg fqdn cache list が toFQDNs の唯一の診断手段です。
github.com もキャッシュには載っています(DNS proxy が応答を見ているため)が、ポリシーで許可していないので接続は落ちます。「学習しているのに通らない」のか「そもそも学習していない」のかを、このコマンドで切り分けます。TTL が短い(実測 15〜18 秒)ことにも注意してください。
確認が済んだら片付けます。
実行コマンド:
$ kubectl delete cnp -n default fqdn-probe-egress
$ kubectl delete pod fqdn-probe
暗記必須コマンド(Cilium / L7 / Hubble)
Cilium のドキュメントは CKS の Resources Allowed に含まれますが、許可されるのは https://docs.cilium.io/en/stable 配下のみです(第1回で確定済み)。試験時間内に検索している余裕もありません。構造は手に覚えさせてください。
Istio のドキュメント(istio.io/latest/docs/)は Resources Allowed に含まれます。
ただし許可されるのはこのサブパス配下のみで、CKA / CKAD では許可されていません——CKS 固有の追加です。参照はできますが、PeerAuthentication は本書ラボで手を動かしていない領域なので、mtls.mode の 3 つの値(STRICT / PERMISSIVE / DISABLE)は開かずに書けるようにしておいてください。
cilium CLI は試験環境にあるとは限りません。 本書ラボでは k8s-ops に入れましたが、試験環境にあるのは kubectl と、タスクごとに指定される SSH ホストのツール群です。確実なのは agent Pod の中にある cilium-dbg と hubble——これらは Cilium が動いていれば必ず存在します。
$ kubectl -n kube-system get pods -l k8s-app=cilium -o wide
$ CPOD=$(kubectl get pods -n kube-system -l k8s-app=cilium --field-selector spec.nodeName=<node> -o jsonpath='{.items[0].metadata.name}')
$ kubectl exec -n kube-system $CPOD -c cilium-agent -- cilium-dbg status
| 用途 | コマンド / 記法 | 補足 |
|---|---|---|
| CRD の確認 | kubectl api-resources | grep cilium | CiliumNetworkPolicy は cilium.io/v2(v2alpha1 の CRD と混同しない)。短縮名は cnp |
| 状態確認 | cilium status | Cluster Pods の x/y が移行の進捗。CLI が無い環境では cilium-dbg を使う |
| 詳細診断 | kubectl exec -n kube-system $CPOD -c cilium-agent -- cilium-dbg status --verbose | Cluster health の no route to host はファイアウォール |
| 暗号化確認 | cilium-dbg encrypt status | Interface: cilium_wg0 / Number of peers |
| 暗号化(WireGuard) | encryption.enabled=true / encryption.type=wireguard | chart の既定は ipsec。rollout restart を忘れない |
| 暗号化(IPsec) | encryption.type=ipsec + Secret cilium-ipsec-keys(kube-system・キー名 keys) | 鍵は自分で作る(下記)。KEYID は 1〜15 で循環 |
| L7 ポリシー | toPorts[].rules.http[] の method / path | path は正規表現・拒否は 403 |
| クロス NS の起点 | k8s:io.kubernetes.pod.namespace: <ns> | Cilium 固有の記法 |
| FQDN egress | toFQDNs: [{matchName: ...}] + rules.dns | DNS 許可とセット |
| 相互認証 | authentication: {mode: "required"} | 暗号化はしない。authentication.enabled=true も必要 |
| Istio | PeerAuthentication の mtls.mode: STRICT | 記法のみ |
| 可視化 | hubble observe --namespace <ns> / --verdict DROPPED | agent Pod に同梱 |
| FQDN の学習内容 | cilium-dbg fqdn cache list | toFQDNs の唯一の診断手段 |
| ポート | 8472/udp(VXLAN)/ 4240/tcp(health)/ 51871/udp(WireGuard)/ 4250/tcp(mutual auth) | ファイアウォールのある環境で必須 |
IPsec を選ぶ場合の鍵
WireGuard は鍵を Cilium が自動生成しますが、IPsec では鍵の Secret を自分で作ります。 コンピテンシーの文言は暗号化方式を限定していないため、どちらを問われても答えられるようにしておきます。
kubectl create -n kube-system secret generic cilium-ipsec-keys \
--from-literal=keys="3+ rfc4106(gcm(aes)) $(dd if=/dev/urandom count=20 bs=1 2> /dev/null | xxd -p -c 64) 128"
| 要素 | 意味 |
|---|---|
3 | KEYID(1〜15)。鍵ローテーションのたびに増やし、15 の次は 1 に戻る |
+ | トンネルごとの鍵派生を強制する(公式が推奨) |
rfc4106(gcm(aes)) | アルゴリズム |
$(dd ...) | PSK(16 進) |
128 | 鍵長(ビット) |
Secret は kube-system に置きます(Cilium と同じ namespace であることが必要)。ローテーション中は新旧の鍵が共存し、エンドポイントごとに使用中の鍵を追跡するため無停止で更新できます。
まとめ
- 稼働中クラスタの CNI 入れ替えは、公式の per-node 無停止手順が本書ラボ(Calico BGP + IPIP)では成立しません。一括移行を選び、ダウンタイムを前提に設計します
- Cilium を入れるだけで Calico の IP 自動検出が壊れます。共存させるなら
IP_AUTODETECTION_METHODの固定が必要です - CNI が無い瞬間、admission webhook がクラスタをロックします。移行前に退避します
- firewalld は VXLAN(8472/udp)・health(4240/tcp)・WireGuard(51871/udp)を個別に開けます。
pingが通ることは根拠になりません - 透過暗号化は機密性だけを守ります。相互認証は mutual authentication の役目で、Cilium では両者が独立しています
wg showと 2 点のtcpdumpで「外は暗号・中は平文」を証明できます- L7 ポリシーは既存の L3/L4 許可があると効きません。置き換えとして設計します
- L7 制御はアプリの HTTP バージョンの前提を露呈させます(nginx の
proxy_passは既定 HTTP/1.0) - Hubble の
to-proxy/http-request DROPPEDで L7 の効きを確認できます - 第6回の SELinux ハードニングは mutual authentication(SPIRE)も止めます。第10回の gVisor に続く 2 例目です
第19回で復習する項目。
本回で手を動かせなかった 4 つは、第19回の重点復習に送ります——mutual authentication(authentication.mode)・Istio(PeerAuthentication)・IPsec(cilium-ipsec-keys)・toFQDNs。第6回の AppArmor、第10回の Kata と同じ枠です。記法を書けることが目標で、Killer.sh などで補完してください。
理解度チェック(○×形式・全 9 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
- 第3回で作った標準 NetworkPolicy は Cilium 移行後もそのまま有効である
- Cilium 公式の per-node 移行手順に従えば、どんなクラスタでも無停止で移行できる
- 透過暗号化(WireGuard)は通信相手の相互認証まで行う
- Cilium の mutual authentication を有効にすれば、通信は暗号化される
- CiliumNetworkPolicy の L7 ルールで拒否されたリクエストは、パケットが破棄されるのではなく HTTP 403 が返る
- 別 Namespace の Endpoint は
k8s:io.kubernetes.pod.namespaceラベルで指定する - L7 ポリシーを追加すれば、同じ相手・同じポートを許可する既存の L3/L4 ポリシーが残っていても L7 の制限が効く
toFQDNsを使うときは、DNS への egress をrules.dns付きで許可する必要があるcilium-dbg status --verboseでno route to hostが出るときは、経路ではなくファイアウォールを疑うべきである
解答と解説
1=○/2=×/3=×/4=×/5=○/6=○/7=×/8=○/9=○
- 問2: 公式自身が「BGP ベースのルーティングは未検証」と明記しています。本書ラボでは移行済みノードの Pod が孤島になり、DNS も引けなくなりました
- 問3・問4: 透過暗号化 = 機密性のみ / mutual authentication = 認証のみです。chart のコメントが「暗号化を別途有効にしない限り完全な mTLS ではない」と明記しています
- 問7: Cilium のポリシーは加算的で、より広い許可が勝ちます。L7 は「足す」のではなく L3/L4 を「置き換える」ものです
- 問9: ICMP は通るのに TCP だけ
no route to hostになるのは、firewalld の REJECT の典型的な見え方です
次回予告
第12回「最小イメージ + 静的解析(Kubesec + KubeLinter)」から D5(サプライチェーンセキュリティ) に入ります。distroless / multi-stage build でベースイメージを最小化し、Kubesec でマニフェストをスコアリング、KubeLinter で CI 段階のベストプラクティス違反を検出します。「動くイメージ」から「攻撃面の小さいイメージ」へ進めます。
