Skip to content

필사 모드: Ciliumネットワークポリシー実践 — L3からL7、DNSまでゼロトラストの実装

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

はじめに

Kubernetesクラスタのデフォルト状態は「すべてのPodがすべてのPodと通信可能」です。決済サービスのPodが社内WikiのPodにアクセスでき、侵害されたフロントエンドPodがデータベースに直接つながります。ゼロトラストの出発点は、このデフォルトを覆すこと、つまり「明示的に許可された通信のみ可能」にすることです。

標準のNetworkPolicyでもある程度は可能ですが、実務ではすぐに壁にぶつかります。HTTPパス単位の制御ができず、外部APIをドメイン名で許可できず、拒否されたトラフィックを見る方法がありません。CiliumはCiliumNetworkPolicy(CNP)とCiliumClusterwideNetworkPolicy(CCNP)でこのギャップを埋めます。本記事では、ポリシーモデルの原理からL3/L4/L7/DNSポリシーのYAML、デフォルト拒否への移行戦略、Hubbleベースの作成ワークフロー、よくある落とし穴まで、実務の順序で扱います。

標準NetworkPolicyの限界とCNPの拡張

| 能力 | k8s NetworkPolicy | CiliumNetworkPolicy |

| --- | --- | --- |

| L3/L4(Podセレクタ、ポート) | 可能 | 可能 |

| L7 HTTP(メソッド、パス) | 不可 | 可能 |

| Kafkaトピック、gRPCメソッド | 不可 | 可能 |

| DNS名ベースのegress | 不可 | 可能(toFQDNs) |

| 明示的な拒否(deny)ルール | 不可(許可リストのみ) | 可能(ingressDeny/egressDeny) |

| クラスタ全域ポリシー | 不可(名前空間単位) | 可能(CCNP) |

| ホスト(ノード)ポリシー | 不可 | 可能(nodeSelector) |

| 拒否トラフィックの可視性 | 実装依存 | Hubbleで即時確認 |

| エンティティ概念(world、hostなど) | 不可 | 可能 |

重要な前提: CNPも標準NetworkPolicyも、どちらもidentityベースで同じeBPFデータパス上で評価されます。2種類を混用すると「どちらか一方でも許可すれば許可」と合算されるため、チームとしてどのリソースを標準にするか決めておくことが運用の混乱を減らします。

ポリシーモデル — identityと方向

Ciliumポリシー評価の思考モデルは次のとおりです。

ingressポリシー egressポリシー

「誰が私に来られるか」 「私はどこへ行けるか」

[src identity] ----> (エンドポイント) ----> [dst identity/CIDR/FQDN]

| |

| エンドポイント別ポリシーマップでO(1)判定

| key: (identity, port, proto, 方向)

v

判定: ALLOW / DENY / (ポリシーなしなら) デフォルト許可*

* ただし、ある方向にポリシーが1つでもselectされると

その方向はデフォルト拒否に切り替わる (重要!)

最後の行が、実務で最も多く事故を起こすルールです。あるPodにingressポリシーを1つでも適用した瞬間、そのPodのingressはホワイトリストモードになります。egressはegressポリシーが適用されるまで依然としてすべて許可です。方向ごとに独立して切り替わる点を覚えておく必要があります。

L3/L4ポリシー実践YAML

名前空間の隔離(同じ名前空間のみ許可)

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: ns-isolation

namespace: payments

spec:

endpointSelector: {} # 名前空間内のすべてのPodに適用

ingress:

- fromEndpoints:

- {} # 同じ名前空間のすべてのPodを許可

空のendpointSelectorは「この名前空間のすべてのエンドポイント」を、ingressの空のfromEndpoints項目は「同じ名前空間のすべてのエンドポイント」を意味します。このポリシー1つで名前空間の外から入るトラフィックはすべて遮断されます。

特定サービス間の許可(フロントエンド → バックエンド 8080)

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: allow-frontend-to-backend

namespace: shop

spec:

endpointSelector:

matchLabels:

app: backend

ingress:

- fromEndpoints:

- matchLabels:

app: frontend

toPorts:

- ports:

- port: "8080"

protocol: TCP

別の名前空間のPodを許可

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: allow-from-monitoring

namespace: shop

spec:

endpointSelector:

matchLabels:

app: backend

ingress:

- fromEndpoints:

- matchLabels:

k8s:io.kubernetes.pod.namespace: monitoring

app: prometheus

toPorts:

- ports:

- port: "9090"

protocol: TCP

名前空間をまたぐ選択には`k8s:io.kubernetes.pod.namespace`ラベルを使います。標準NetworkPolicyのnamespaceSelectorより表現が直接的です。

明示的な拒否 — egressDeny

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: deny-metadata-endpoint

namespace: shop

spec:

endpointSelector: {}

egressDeny:

- toCIDR:

- 169.254.169.254/32 # クラウドメタデータエンドポイントを遮断

denyルールは常にallowルールより優先されます。クラウドメタデータサーバの遮断のように「何があっても止めるべき」項目に適しています。

L7ポリシー — HTTP、Kafka、gRPC

動作原理: Envoy連携

L7ポリシーが付いたトラフィックの経路がどう変わるかを理解しなければ運用できません。

L4まで: クライアントPod --eBPF--> サーバPod (カーネル内処理)

L7ポリシー: クライアントPod --eBPF--> [Envoyプロキシ] --> サーバPod

^

cilium-agentに内蔵 (または専用Pod)

eBPFが該当フローのみプロキシへリダイレクト

HTTPをパースしルールにマッチ、違反時は403

eBPFはL7ルールが掛かったフローだけを選別的にEnvoyへ渡すため、L7ポリシーのないトラフィックは依然としてカーネル内のみで処理されます。L7検査対象のトラフィックにはプロキシ経由のコスト(追加レイテンシ、接続の終端)が生じる点を受け入れる必要があります。

HTTPメソッド/パスの制限

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: api-l7-allow

namespace: shop

spec:

endpointSelector:

matchLabels:

app: order-api

ingress:

- fromEndpoints:

- matchLabels:

app: frontend

toPorts:

- ports:

- port: "8080"

protocol: TCP

rules:

http:

- method: GET

path: /api/v1/orders.*

- method: POST

path: /api/v1/orders

- method: GET

path: /healthz

マッチしないリクエスト(例: DELETE、または/adminパス)は接続自体は成立するもののHTTP 403で拒否されます。L4遮断と異なり、アプリケーションログの観点では「接続はできるのに403」と見えるため、トラブルシューティング時にこの違いを知っておくべきです。

Kafkaトピックの制限

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: kafka-topic-policy

namespace: streaming

spec:

endpointSelector:

matchLabels:

app: kafka

ingress:

- fromEndpoints:

- matchLabels:

app: order-service

toPorts:

- ports:

- port: "9092"

protocol: TCP

rules:

kafka:

- role: produce

topic: orders

- fromEndpoints:

- matchLabels:

app: settlement-service

toPorts:

- ports:

- port: "9092"

protocol: TCP

rules:

kafka:

- role: consume

topic: orders

注文サービスはordersトピックへのproduceのみ、精算サービスはconsumeのみ可能にする例です。メッセージブローカーを共有するマルチテナント環境で、トピック単位の隔離をネットワーク層で強制できます。

gRPCメソッドの制限

gRPCはHTTP/2上で「POST /パッケージ.サービス/メソッド」という形で呼び出されるため、HTTPルールで表現します。

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: grpc-method-policy

namespace: shop

spec:

endpointSelector:

matchLabels:

app: inventory-grpc

ingress:

- fromEndpoints:

- matchLabels:

app: order-api

toPorts:

- ports:

- port: "50051"

protocol: TCP

rules:

http:

- method: POST

path: /inventory.InventoryService/CheckStock

- method: POST

path: /inventory.InventoryService/ReserveStock

DNSベースのegressポリシー — toFQDNs

外部SaaS APIをIPで許可するのは保守不可能です(IPは頻繁に変わります)。toFQDNsはドメイン名でegressを許可します。

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: allow-external-apis

namespace: payments

spec:

endpointSelector:

matchLabels:

app: pg-gateway

egress:

1) DNSクエリ自体を許可し、DNSプロキシで観察対象にする

- toEndpoints:

- matchLabels:

k8s:io.kubernetes.pod.namespace: kube-system

k8s-app: kube-dns

toPorts:

- ports:

- port: "53"

protocol: UDP

rules:

dns:

- matchPattern: "*"

2) 許可する外部ドメイン

- toFQDNs:

- matchName: api.stripe.com

- matchPattern: "*.tosspayments.com"

toPorts:

- ports:

- port: "443"

protocol: TCP

DNSプロキシの動作

Pod --(DNSクエリ)--> [Cilium DNSプロキシ] --> CoreDNS/外部DNS

|

| 応答のA/AAAAレコードを傍受

| 「このPodは api.stripe.com = 54.187.x.x を知った」

v

ipcache/ポリシーマップに該当IPをtoFQDNs identityとして登録

|

Pod --(TCP 443 to 54.187.x.x)--> eBPFがIPベースで許可

核心: toFQDNsは「パケットのSNIを見る」のではなく、**そのPodがDNSで解決した名前と応答IPのペア**を記憶し、IPベースで許可する方式です。したがってDNSルール(上記YAMLの1番ブロック)が一緒にないとtoFQDNsは動作しません。これが最もよくある設定ミスです。

デフォルト拒否への移行戦略 — 4段階ロードマップ

稼働中のクラスタでデフォルト拒否を一度に有効化すると障害になります。検証済みの漸進的な適用順序は次のとおりです。

第1段階: 観察 第2段階: 主要経路の許可 第3段階: 監査モード拒否 第4段階: 強制

Hubbleで現行 観察結果をポリシー化、 デフォルト拒否を配備 auditを解除し

トラフィックを全数 → 拒否なしで適用 → + policy-audit-mode → 実際に遮断

観察 (2〜4週間) (サービス別PRレビュー) (違反はログのみ) (名前空間ごとに順次)

1. **観察**: Hubbleのメトリクスとフローログで、名前空間別の実際の通信マトリクスを収集します。バッチジョブ(cron)のようにまれに動くトラフィックを見逃さないよう、最低1か月の周期を推奨します。

2. **主要経路の許可ポリシー作成**: 拒否ポリシーなしでallowポリシーのみ先に配備します。この段階では何も遮断されないため安全です。

3. **監査モード**: エンドポイントをpolicy-audit-modeに切り替えると、デフォルト拒否ポリシーがあっても実際にはブロックせず、「ブロックされたはずのトラフィック」をverdictとして記録します。

エージェント全域の監査モード (helm: policyAuditMode=true)

または特定エンドポイントのみ

kubectl -n kube-system exec ds/cilium -- cilium endpoint config 1234 PolicyAuditMode=Enabled

監査判定の観察: 実際にブロックされたはずのトラフィックを探す

hubble observe --verdict AUDIT --namespace payments

4. **強制への切り替え**: auditで一定期間(例: 2週間)違反がなければ、名前空間単位でauditを解除します。クラスタ全体の一括切り替えは禁物です。

デフォルト拒否自体は次のように明示的に配備することを推奨します。

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: default-deny

namespace: payments

spec:

endpointSelector: {}

ingress:

- fromEndpoints: [] # 何にもマッチしない = 明示的なデフォルト拒否

egress:

- toEndpoints: []

ポリシー作成ワークフロー — Hubbleからポリシーへ

実務でポリシーは頭で組み立てるものではなく、観察から導出します。

1) 対象サービスが実際にやり取りするトラフィックを観察

hubble observe --namespace shop --pod shop/order-api --last 1000

2) どのidentityと通信しているかを要約

hubble observe --namespace shop --pod shop/order-api \

--output json | jq -r '.flow.destination.labels | join(",")' | sort | uniq -c

3) ポリシー適用後に拒否されるトラフィックがないか確認 (auditモードで)

hubble observe --verdict AUDIT --namespace shop

4) 強制後のドロップモニタリング

hubble observe --verdict DROPPED --namespace shop --since 1h

ドラフト作成にはネットワークポリシーエディタ(editor.networkpolicy.io)が便利です。Hubbleフローをアップロードすると、観察されたトラフィックに基づいてポリシードラフトを視覚的に生成してくれます。ただし、生成されたドラフトは必ず人がレビューすべきです。観察期間中に発生しなかった正常トラフィック(フェイルオーバー経路、月次バッチ)はドラフトから抜けているからです。

hostポリシーと外部エンティティ

エンティティ(entities)の概念

クラスタの外の世界を扱うための予約識別子です。

| エンティティ | 意味 |

| --- | --- |

| world | クラスタ外部のすべて(インターネットを含む) |

| cluster | クラスタ内のすべてのエンドポイント |

| host | ローカルノード自身 |

| remote-node | 他のノード |

| kube-apiserver | APIサーバ |

| health | Ciliumヘルスチェックエンドポイント |

クラスタ内部通信 + APIサーバのみ許可するegressの例

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: cluster-only-egress

namespace: internal-tools

spec:

endpointSelector: {}

egress:

- toEntities:

- cluster

- kube-apiserver

ホストポリシー(CCNP + nodeSelector)

ノード自体のトラフィックもポリシーの対象になります。SSHとkubeletポートのみ許可する例です。

apiVersion: cilium.io/v2

kind: CiliumClusterwideNetworkPolicy

metadata:

name: host-fw-control-plane

spec:

nodeSelector:

matchLabels:

node-role.kubernetes.io/control-plane: ""

ingress:

- fromEntities:

- cluster

- fromCIDR:

- 10.50.0.0/24 # 管理ネットワーク

toPorts:

- ports:

- port: "22"

protocol: TCP

- port: "6443"

protocol: TCP

ホストポリシーは誤ると**ノード全体をロックアウトし得る**ため、必ずpolicy-audit-modeで十分に検証してから強制すべきです。ホストファイアウォール機能(hostFirewall.enabled)もhelmで有効になっている必要があります。

ポリシーテストと検証の自動化

ポリシーもコードです。CIに組み込める検証手段:

1) スキーマ/構文検証 (CI段階)

kubectl apply --dry-run=server -f policies/

2) シミュレーション: 特定トラフィックが許可されるかを事前判定

kubectl -n kube-system exec ds/cilium -- \

cilium policy trace --src-k8s-pod shop:frontend-abc --dst-k8s-pod shop:backend-xyz --dport 8080

3) 実クラスタでの統合テスト (ステージング)

cilium connectivity test --test pod-to-pod,pod-to-world

4) 回帰テスト: 配備後のドロップカウント比較

hubble observe --verdict DROPPED --since 10m --output json | jq length

GitOps環境であれば、ポリシーディレクトリへのPRに上記1、2をパイプラインで強制し、マージ後にステージングで3、4を自動実行する構成を推奨します。

よくある落とし穴とアンチパターン

1. **DNSルールのないtoFQDNs**: 前述のとおり、DNSプロキシルールがないとtoFQDNsは永遠にマッチしません。症状は「DNSは通るのに接続が拒否される」またはその逆です。

2. **DNS TTLとIPの変動**: toFQDNsはDNS応答に基づくため、アプリケーションがDNSキャッシュを長く保持し、期限切れのIPに接続すると拒否され得ます。CDNのようにIPが速く回る対象はmatchPatternを広めに取り、エージェントのFQDN関連TTL設定(tofqdns-idle-connection-grace-periodなど)を点検してください。

3. **システム名前空間を一緒にロックする**: kube-system、モニタリング、Ingressコントローラの名前空間に性急にデフォルト拒否を適用すると、クラスタ機能自体が死にます。別トラックとして、最後に、最も慎重に進めるべきです。

4. **ヘルスチェック/プローブの遮断**: kubeletのliveness/readinessプローブはノード(host identity)から来ます。ingressポリシーでhostエンティティを忘れるとPodが無限再起動に陥ります。Ciliumはデフォルトでプローブトラフィックを自動許可しますが、ホストポリシーと組み合わせると壊れることがあります。

5. **新規接続だけ切れるミステリー**: ポリシー適用直後、既存接続はconntrackに残って動作し続け、新規接続だけ遮断される(またはその逆の)非対称が観察されることがあります。「今動いている」は「ポリシーが許可している」の証拠ではありません。

6. **ラベルのタイポと空セレクタの誤解**: matchLabelsのタイポは「何も選択されない」として静かに失敗します。適用後は必ず`cilium endpoint list`でenforce状態のエンドポイント数を確認してください。

7. **L7ポリシーを全トラフィックに乱用**: すべてのトラフィックをEnvoyに送るとレイテンシとCPUが同時に上がります。L7制御が本当に必要な境界(外部公開API、機微データへのアクセス)にのみ選別適用するのが定石です。

実践シナリオ — PCI DSSスタイルの隔離

カード決済データを扱うワークロード(CDE)をクラスタ内で隔離するパターンです。規制の表現は一般化した例であり、実際の審査要件はQSAと確認すべきです。

1) CDE名前空間: デフォルト拒否 + 明示許可のみ

apiVersion: cilium.io/v2

kind: CiliumNetworkPolicy

metadata:

name: cde-lockdown

namespace: cde-payments

spec:

endpointSelector: {}

ingress:

APIゲートウェイからの決済リクエストのみ、L7パス制限まで

- fromEndpoints:

- matchLabels:

k8s:io.kubernetes.pod.namespace: gateway

app: api-gateway

toPorts:

- ports:

- port: "8443"

protocol: TCP

rules:

http:

- method: POST

path: /v1/payments

- method: GET

path: /v1/payments/[0-9a-f-]+

egress:

DNS (プロキシ経由の観察)

- toEndpoints:

- matchLabels:

k8s:io.kubernetes.pod.namespace: kube-system

k8s-app: kube-dns

toPorts:

- ports:

- port: "53"

protocol: UDP

rules:

dns:

- matchPattern: "*.internal.example.com"

- matchName: api.pgprovider.com

社内元帳DB (専用名前空間)

- toEndpoints:

- matchLabels:

k8s:io.kubernetes.pod.namespace: cde-db

app: ledger-db

toPorts:

- ports:

- port: "5432"

protocol: TCP

外部決済代行APIのみ

- toFQDNs:

- matchName: api.pgprovider.com

toPorts:

- ports:

- port: "443"

protocol: TCP

ここにHubbleフローログの長期保管を加えて「隔離が実際に維持されていたこと」を監査証跡として提出する構成(次回扱います)を足せば、ネットワークセグメンテーション要件への技術的な回答が完成します。

導入チェックリスト

- [ ] 標準NetworkPolicyとCNPのどちらをチーム標準にするか決めたか

- [ ] 「ポリシーが1つでも付くとその方向はデフォルト拒否」を全員が理解しているか

- [ ] Hubble観察期間(最低2〜4週間、バッチ周期を含む)を確保したか

- [ ] toFQDNsポリシーごとにDNSプロキシルールがペアで存在するか

- [ ] policy-audit-mode検証段階が配備手順に含まれているか

- [ ] システム名前空間は別トラックに分離したか

- [ ] ポリシーPRにdry-runとpolicy traceがCIで強制されるか

- [ ] 強制後のDROPPED verdictアラートがモニタリングに接続されているか

- [ ] ホストポリシーはauditで検証した後にのみ強制しているか

- [ ] ポリシー変更履歴がGitOpsで追跡可能か

おわりに

Ciliumポリシーの力は表現力(L7、FQDN)だけでなく、**観察とポリシーが同じデータパスから生まれる**ことにあります。Hubbleが見せるフローとポリシーエンジンが判定するフローは同一なので、「観察 → ポリシー化 → 監査 → 強制」のループが推測なしに閉じます。ゼロトラストは一度のビッグバンではなく、このループを名前空間ごとに回し続ける運動に近いです。次回はこのループの観察軸を担うHubbleと、マルチクラスタへ拡張するClusterMeshを扱います。

参考資料

- Ciliumネットワークポリシー公式ドキュメント: https://docs.cilium.io/en/stable/security/policy/

- Cilium L7ポリシー(HTTP/Kafka)ドキュメント: https://docs.cilium.io/en/stable/security/policy/language/

- Cilium DNSベースポリシードキュメント: https://docs.cilium.io/en/stable/security/dns/

- Kubernetes NetworkPolicy公式ドキュメント: https://kubernetes.io/docs/concepts/services-networking/network-policies/

- ネットワークポリシーエディタ: https://editor.networkpolicy.io/

- NIST SP 800-207 ゼロトラストアーキテクチャ: https://csrc.nist.gov/pubs/sp/800/207/final

- PCI Security Standards Council: https://www.pcisecuritystandards.org/

- Envoyプロキシ公式ドキュメント: https://www.envoyproxy.io/docs

- Apache Kafka公式ドキュメント: https://kafka.apache.org/documentation/

- gRPC公式ドキュメント: https://grpc.io/docs/

- Hubble GitHubリポジトリ: https://github.com/cilium/hubble

현재 단락 (1/371)

Kubernetesクラスタのデフォルト状態は「すべてのPodがすべてのPodと通信可能」です。決済サービスのPodが社内WikiのPodにアクセスでき、侵害されたフロントエンドPodがデータベースに...

작성 글자: 0원문 글자: 11,873작성 단락: 0/371