インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

Cilium透過暗号化とL7制御【CKS第11回】

公開

新卒インフラエンジニア向け「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
目次
  1. 第11回のスコープ・今ここマップ
    1. 既習範囲 / 本回で上書きする観点
  2. この回のゴール
    1. 本回の起点
    2. 本回で行き来するホスト
  3. CNI を入れ替えるとは何をすることか
    1. 本ラボの Calico はどう配線されているか
    2. Calico と Cilium は何が違うのか
    3. 移行で変わってしまうもの
  4. 移行方式の設計 —— 公式の無停止手順がこのラボで破綻する理由
    1. 公式が用意している per-node 移行
    2. 破綻その 1 —— 二次モードで入れただけで Calico が全断した
    3. 破綻その 2 —— 1 台だけ移行すると、その Pod は孤島になる
    4. 結論 —— CNI 移行の 3 つの選択肢
  5. 移行の準備
    1. Cilium のバージョンと Kubernetes v1.36 の扱い
    2. whitelist に追加する 1 行
    3. Cilium CLI の導入
    4. Pod CIDR を 10.244.0.0/16 のまま維持する
  6. やってみよう①:Calico → Cilium 一括移行
    1. ステップ0:admission webhook を退避する
    2. ステップ1:firewalld に Cilium 用のポートを開ける
    3. ステップ2:Calico を削除する
    4. ステップ3:Cilium を導入する
    5. ステップ4:全ノードを順に再起動する
    6. ステップ5:残った Pod を確認する
    7. ステップ6:移行を確認する
    8. ステップ7:Gatekeeper の webhook を戻す
    9. ステップ8:動いていることを確認する
  7. 移行で踏む 4 つの落とし穴
    1. 落とし穴 3 の診断が本回でいちばん学びが大きい
    2. 移行後に残る Calico の痕跡
  8. 透過暗号化は何を守り、何を守らないのか
    1. 透過暗号化とは
    2. 暗号化されないもの
    3. トンネルモードとの併用は二重カプセル化になる
  9. やってみよう②:WireGuard 透過暗号化と「外は暗号・中は平文」の実証
    1. ステップ1:暗号化前の姿を記録する
    2. ステップ2:WireGuard を有効化する
    3. ステップ3:クラスタが止まる —— 51871/udp を開ける
    4. ステップ4:暗号化されていることを確認する
    5. ステップ5:「外は暗号・中は平文」を tcpdump で対比する
    6. ステップ6:後片付け
  10. mutual authentication は暗号化しない —— CKS で問われる区別
    1. chart のコメントが答えを書いている
    2. 記法(暗記対象)
    3. 本書ラボでは動きません —— そしてその理由は第6回にあります
    4. 試験対策として不足しないのか
  11. Istio の PeerAuthentication STRICT(記法のみ)
  12. Cilium L7 NetworkPolicy —— L3/L4 の限界とその先
    1. 第3回で書いたポリシーは何を許していたか
    2. CiliumNetworkPolicy が足すもの
    3. 本ラボの実トポロジ
    4. 設計する L7 ポリシー
  13. やってみよう③:L7 NetworkPolicy の適用と HTTP 426 の解決
    1. ステップ1:そのまま適用しても L7 は効かない
    2. ステップ2:既存の L3/L4 ポリシーを外す
    3. ステップ3:しかしブラウザからは 426 になる
    4. ステップ4:アプリ側を直す
  14. Hubble —— 拒否を「見る」
  15. やってみよう④:Hubble で L7 拒否を観測する
  16. toFQDNs(DNS ベースの egress 制御)
  17. 暗記必須コマンド(Cilium / L7 / Hubble)
    1. IPsec を選ぶ場合の鍵
  18. まとめ
  19. 理解度チェック(○×形式・全 9 問)
  20. 次回予告

第11回のスコープ・今ここマップ

本回も CKS ドメイン D4: Minimize Microservice Vulnerabilities(配点 20%) です。本回で D4 の 4 コンピテンシーがすべて埋まります。

公式コンピテンシー(D4 Minimize Microservice Vulnerabilities・20%)担当
Use appropriate Pod Security Standards第8回
Manage Kubernetes secrets第9回
Understand and implement isolation techniques (multi-tenancy, sandboxed containers)第10回
Implement Pod-to-Pod encryption using Cilium, Istio本回(第11回)

コンピテンシーの文言に 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 showtcpdump証明できる
  • CiliumNetworkPolicyL7 ルール(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
Pod103 個・異常 0・14 namespace
CNICalico v3.32.0kubectl apply -f calico.yaml で導入・CRD 22 個
Calico のデータプレーンIPIP Always + BGPvxlanMode: NeverblockSize: 26
NetworkPolicyfanclub に 9 本第3回)・argocd に 6 本
fanclubhttps://fanclub.local/200/api/members が会員 3 名の JSON
Longhorn4 ボリューム attached healthy
ArgoCDfanclub-api-prodSynced / Healthy
Gatewayfanclub-gatewaytraefik / 192.168.1.200 / PROGRAMMED True
alma-proxy の whitelist42 行本回で 1 行だけ足します
containerd の enable_selinuxWorkload Node 2 台のみ true(Control Plane Node 3 台は false)(第6回
RuntimeClass0 個第10回の gVisor は撤去済み)

本回は本書ラボで最も破壊的な回です。

演習①で Calico を削除した時点から先は不可逆になります。kubectl apply -f calico.yaml で戻しても、Cilium が書き換えた CNI 設定ファイル・Pod の IP 割当・ノードのルーティングは噛み合いません。戻す手段は VM スナップショットからの復元だけです。 演習を始める前に、全 VM のスナップショットを停止状態で取得してください。

本回で行き来するホスト

ホスト本回での役割
alma-proxywhitelist に helm.cilium.io を 1 行追加
k8s-opsCilium CLI の導入・helm / kubectl の実行・疎通確認
k8s-cp-01〜03 / k8s-wl-01〜02firewalld のポート開放(全 5 台)・再起動(全 5 台)
k8s-wl-01tcpdump / 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 枚あります。

eth0192.168.1.x(既定ゲートウェイのある管理系)、eth110.0.10.x(クラスタ内部通信用)です。上の経路が 10.0.10.x を向いているのは、Calico が eth1 を自動検出して選んだためです。この「どちらの NIC を使うか」が、本回の落とし穴の源になります。

Calico と Cilium は何が違うのか

項目Calico(第2巻〜第10回)Cilium(第11回〜)
カプセル化IPIPtunl0VXLANcilium_vxlan8472/udp
経路の配り方BGP(BIRD が proto bird のルートを配る)VXLAN の overlay(agent 同士が直接同期)
IPAM独自ブロック /26 単位cluster-pool /24 単位
ノード IP の選び方IP=autodetect(first-found)Node の InternalIP
ノード間で使う NICeth1(10.0.10.x)を自動検出eth0(192.168.1.x)InternalIP がそちらのため)
ポリシーK8s NetworkPolicy(L3/L4)K8s NetworkPolicy + CiliumNetworkPolicy(L7 まで)
実装iptables / nftables + BIRDeBPF(L7 は Envoy
CRD22 個10 個(chart に同梱されず operator が実行時に作る)

この表で本回に効いてくるのは 4 行目と 5 行目です。 第2巻では firewalld の trusted ゾーンに 10.0.10.0/24 を入れることで、Calico のノード間通信をまとめて許可していました。Cilium は eth0 を使うので、その許可の恩恵を受けられません。

Calico と Cilium でノード間通信に使う NIC が入れ替わることを示す図。上段の Calico は Pod から tunl0(IPIP)を経て eth1(10.0.10.x・firewalld の trusted zone)へ抜け、ノード間を素通りする。下段の Cilium は Pod から cilium_vxlan・cilium_wg0 を経て eth0(192.168.1.x・public zone)へ抜けるため、8472/udp と 51871/udp を個別に開けないと全断する
図1:Calico と Cilium でノード間通信の NIC が入れ替わる

移行で変わってしまうもの

  • 全 Pod の IP が変わります(Calico の /26 ブロックから Cilium の /24 割当へ)。第10回までの記事に載っている Pod IP は再現しません。IP をハードコードした設定があれば壊れます(本書ラボはすべて Service 名で解決しているため影響しませんが、現場では移行前に洗い出す対象になります)
  • /etc/cni/net.d/10-calico.conflist10-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 つです。

  1. Cilium を CNI 設定を書かないモードcni.customConf: true)で入れる
  2. 既存とは別の Pod CIDR(公式例は 10.245.0.0/16)を割り当てる
  3. 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/membersGateway 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.128eth1)から 10.245.1.166cilium_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/membersHTTP 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.conflist10-calico.conflist.cilium_bak にリネームされ、05-cilium.conflist が書かれた
  • そのノードに立った新しい Pod の IP は 10.245.4.18(Cilium の cluster-pool から払い出し)
  • cilium statusCluster Pods: 2/69 managed by Cilium と表示

ところが、その Pod は誰とも通信できませんでした。

テスト結果
Cilium 側 Pod → DNS(Service 10.96.0.10timeout
Cilium 側 Pod → Calico 側 Pod(Pod IP 直接)No route to host
Calico 側 Pod → Cilium 側 Podtimeout
Cilium 側 Pod → apiserver(10.96.0.1・hostNetwork)HTTP 401 = 到達する
cilium-dbg statusCluster 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 CLIv0.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.iogithub.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 に置きます。 sudosecure_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 CIDR10.96.0.0/1210.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.shno_proxy10.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 widerules の対象リソースと 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/udpVXLAN(Pod 間トラフィックのカプセル化)本ステップ
4240/tcpcilium health(ノード間の到達性チェック)本ステップ
51871/udpWireGuardやってみよう②
4250/tcpmutual authentication本ラボでは使いません

なぜ trusted ゾーンに 192.168.1.0/24 を丸ごと入れないのか。

第2巻が 10.0.10.0/24trusted に入れたのは、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 でした。

ポートを開けずに移行を進めた場合は、ここで大量の UnknownCrashLoopBackOff が残ります。 その状態でポートを開けても 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
確認項目期待値
異常 Pod0(総数は 111。103 − Calico 6 + Cilium 14)
cilium-dbg status の Cluster health5/5 reachable
https://fanclub.local/api/membersHTTP 200 + 会員 3 名の JSON
Gatewayfanclub-gatewayPROGRAMMED True
Longhorn4 ボリューム attached healthy
ArgoCDfanclub-api-prodSynced / Healthy
NetworkPolicy第3回の 9 本がそのまま有効

第3回で書いた NetworkPolicy は 1 文字も変えずに Cilium で機能します。

Cilium は Kubernetes 標準の NetworkPolicy をそのままエンフォースするためです。CNI を替えても L3/L4 のポリシーを書き直す必要はありません。本回で追加する L7 制御は、この上に載る別のレイヤーです。

ArgoCD は移行中に何もしません。

selfHeal: true で動いているので心配になりますが、ArgoCD が見ているのは Git のマニフェストとクラスタ上のリソースの差分であり、CNI の入れ替えはその対象外です。移行前後で fanclub-api-prodSynced / Healthy のままでした。Pod の IP が変わっても ArgoCD は差分と見なしません——IP はマニフェストに書かれていないからです。

試験ではこう問われる。

CNI の移行手順そのものは CKS では問われません。 試験環境の CNI は最初から用意されています。本演習で試験に直結するのは cilium status でクラスタの健全性を読めることと、cilium-dbg status --verbose で「どのノードと通信できていないか」を切り分けられることの 2 点です。

移行で踏む 4 つの落とし穴

演習①で「なぜその手順なのか」を、実際に踏んだ順に整理します。別の環境で移行するときに必要になる知識です。

#落とし穴症状対処
1Calico の IP 自動検出が cilium_host を拾うcalico-node が全台 0/1proto bird の経路が全消滅・クロスノード通信が全滅IP_AUTODETECTION_METHOD=interface=eth1共存させる場合のみ。一括移行では Calico を先に消すので起きない
2CNI 不在の間、admission webhook がクラスタをロックするhelm の操作が context deadline exceeded で失敗移行前に webhook を退避する
3firewalld が VXLAN と health を塞ぐCluster health: 1/54240: 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/53/55/5 reachable と回復します。新規 Pod からの名前解決も通るようになります。

ciliumcilium-dbg の違い。

cilium は k8s-ops に入れた CLI で、apiserver 経由でクラスタ全体を見ます。cilium-dbgagent 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 は WireGuardIPsec をサポートします。

項目
Helm 値encryption.enabled=true / encryption.type=wireguard
chart の既定ipsec(WireGuard を使うなら明示指定が必要)
カーネル要件Linux 5.6 以降 + CONFIG_WIREGUARDAlmaLinux 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.yamlencryption: を足します。変更後の全量は次のとおりです。

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:8080GET /metrics)。外側では読めなくなったものが、内側では変わらず読めます。

観測点フィルタ暗号化【前】暗号化【後】
eth0udp port 8472(VXLAN)見える(内側の Pod IP・ポートまで)0 件
eth0udp port 51871(WireGuard)0 件見える(length 144 / 416 / 944 / 1440)
eth0-A で平文 HTTP を数える52 件POST /api/prom/push HTTP/1.1 等)0 件(ランダムなバイト列)
cilium_vxlantcp 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 つのものと思い込んでいると、設問の選択肢を読み分けられません。

機密性と相互認証を 2 軸に取った 4 象限の図。左下は「どちらも無し」で盗聴もなりすましもできる移行直後の状態、右下は「透過暗号化のみ」で盗聴はできないがなりすませる本回のゴール、左上は「mutual authentication のみ」で相手は確かめられるが中身は読まれる、右上は両方を有効化した一般に mTLS と呼ばれる状態。本書ラボでは第6回の SELinux が SPIRE の起動を拒否するため上段には到達できない
図2:機密性と相互認証の 4 象限(透過暗号化はなりすましを防がない)

記法(暗記対象)

CiliumNetworkPolicyingress / 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”)
namespacecilium-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 domainspiffe.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回と同じです。

フィールド意味
scontextcontainer_t:s0:c698,c876MCS カテゴリを持つコンテナ(主体)
tcontextcontainer_var_run_t:s0カテゴリを持たないディレクトリ(客体)
tclass / 操作dir / writeディレクトリへの書き込み
SYSCALLbind(49)UNIX ドメインソケットの作成
exit-13EACCES
UIDrootパーミッションではなく 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 点です。

  1. authentication: {mode: "required"} の記法ingress / egress ブロックの中に書く)
  2. 「認証と暗号化は別物」という区別——これは選択肢の読み分けで直接効きます

実機で SPIRE を動かせないことは、試験対応上の欠落にはなりません。むしろ 「ハードニングが後から入れる機能を止める」という現実を先に知っておくことのほうが、CKS 保持者として現場で問われます。

Istio の PeerAuthentication STRICT(記法のみ)

CKS のコンピテンシーは Cilium, Istio と 2 つを併記しています。「どちらかを知っていればよい」のか「両方」なのかは公式に明示がありません。 本書は実装は Cilium で行い、Istio は記法を押さえる方針を採ります。判断の根拠は 2 つです。

  1. Istio の導入はサイドカー注入を伴い、クラスタ全体の構成が変わる。試験の 120 分でゼロから構築させる設問は現実的でない
  2. 設問が「暗号化を有効にせよ」の形なら、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
モード意味
STRICTmTLS 必須。平文を拒否する
PERMISSIVEmTLS と平文の両方を受け付ける(移行期に使う)
DISABLEmTLS を使わない

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
マッチングpathmethodhost はいずれも 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 ポリシーの起点は frontendPrometheus の 2 つです。

L7 NetworkPolicy が起点ごとに許可する HTTP パスを変える様子を示す図。fanclub-frontend からの GET /api/members と Prometheus からの GET /metrics は cilium-envoy を通って backend へ届き、逆の組み合わせ(frontend からの /metrics、Prometheus からの /api/members)は 403 Forbidden で返される。判定は CiliumNetworkPolicy の ingress から fromEndpoints、toPorts、rules、http の method と path へ至る 4 階層のネストで表現する
図3:L7 NetworkPolicy が起点ごとに許可するパスを変える

設計する 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/admin404 です。403 ではありません。

この 404backend(Payara)が返しています——つまりリクエストは backend まで届いており、Cilium は何も遮断していません。 L7 ポリシーは適用済みで VALID: True なのに、効いていないのです。

原因は第3回で書いた既存のポリシーです。allow-backend-from-frontend8080/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

実行結果:

起点リクエスト結果
frontendGET /api/members200 OK
frontendGET /api/admin403 Forbidden
frontendGET /metrics403 Forbidden
PrometheusGET /metrics200 OK
PrometheusGET /api/members403 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_pass426
frontend の nginx 設定に proxy_http_version0 件(既定は HTTP/1.0
Hubblenginx 経由のリクエストは 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-configdatadefault.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_proxyk8s-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 秒以内に同期が完了しましたsyncPolicyautomated: {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 階層のネストです。

ingressfromEndpointstoPortsports / ruleshttpmethod / path

kubectl create のサブコマンドは存在しませんRuntimeClass と同じく手書きになります)。path が正規表現であること、拒否が 403 で返ることも設問の読み分けに効きます。既存の L3/L4 ポリシーが同じ経路を許可していないかの確認も忘れないでください。

Hubble —— 拒否を「見る」

Cilium には Hubble という観測機能が組み込まれています。演習①で hubble.enabled: true を指定したので、すでに動いています。

項目
CLIcilium agent Pod に /usr/bin/hubble(v1.19.6)が同梱。別途インストール不要
Servicehubble-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 DROPPEDL7 で拒否された。URL がそのまま出る
http-response FORWARDED (HTTP/1.1 403 ...)拒否の応答として 403 が返っている(パケットのドロップではない)
to-proxyEnvoy を経由した = L7 ポリシーの対象
to-overlay / to-endpointEnvoy を通らない = L3/L4 のみ
policy-verdict:none ... ALLOWEDポリシー判定の結果
(ID:8580) / (ID:16266)Cilium のセキュリティアイデンティティ(ラベルの組から導出される番号。backend の 3 Pod はすべて同じ ID——同じラベルを持つ Pod は 1 つのアイデンティティとして扱われます)

to-proxyto-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 の前提です。検証用のポリシーを作ります——先ほどの例から namespaceendpointSelector の 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 listtoFQDNs の唯一の診断手段です。

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-dbghubble——これらは 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 ciliumCiliumNetworkPolicycilium.io/v2v2alpha1 の CRD と混同しない)。短縮名は cnp
状態確認cilium statusCluster Pods の x/y が移行の進捗。CLI が無い環境では cilium-dbg を使う
詳細診断kubectl exec -n kube-system $CPOD -c cilium-agent -- cilium-dbg status --verboseCluster health の no route to host はファイアウォール
暗号化確認cilium-dbg encrypt statusInterface: cilium_wg0 / Number of peers
暗号化(WireGuard)encryption.enabled=true / encryption.type=wireguardchart の既定は ipsecrollout restart を忘れない
暗号化(IPsec)encryption.type=ipsec + Secret cilium-ipsec-keyskube-system・キー名 keys鍵は自分で作る(下記)。KEYID は 1〜15 で循環
L7 ポリシーtoPorts[].rules.http[]method / pathpath は正規表現・拒否は 403
クロス NS の起点k8s:io.kubernetes.pod.namespace: <ns>Cilium 固有の記法
FQDN egresstoFQDNs: [{matchName: ...}] + rules.dnsDNS 許可とセット
相互認証authentication: {mode: "required"}暗号化はしないauthentication.enabled=true も必要
IstioPeerAuthenticationmtls.mode: STRICT記法のみ
可視化hubble observe --namespace <ns> / --verdict DROPPEDagent Pod に同梱
FQDN の学習内容cilium-dbg fqdn cache listtoFQDNs の唯一の診断手段
ポート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"
要素意味
3KEYID(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.modeIstio(PeerAuthenticationIPsec(cilium-ipsec-keystoFQDNs。第6回の AppArmor、第10回の Kata と同じ枠です。記法を書けることが目標で、Killer.sh などで補完してください。

理解度チェック(○×形式・全 9 問)

次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。

  1. 第3回で作った標準 NetworkPolicy は Cilium 移行後もそのまま有効である
  2. Cilium 公式の per-node 移行手順に従えば、どんなクラスタでも無停止で移行できる
  3. 透過暗号化(WireGuard)は通信相手の相互認証まで行う
  4. Cilium の mutual authentication を有効にすれば、通信は暗号化される
  5. CiliumNetworkPolicy の L7 ルールで拒否されたリクエストは、パケットが破棄されるのではなく HTTP 403 が返る
  6. 別 Namespace の Endpoint は k8s:io.kubernetes.pod.namespace ラベルで指定する
  7. L7 ポリシーを追加すれば、同じ相手・同じポートを許可する既存の L3/L4 ポリシーが残っていても L7 の制限が効く
  8. toFQDNs を使うときは、DNS への egress を rules.dns 付きで許可する必要がある
  9. cilium-dbg status --verboseno 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 段階のベストプラクティス違反を検出します。「動くイメージ」から「攻撃面の小さいイメージ」へ進めます。

→ 詳しくは第12回 最小イメージ + 静的解析(Kubesec + KubeLinter)

前の記事
次の記事