Skip to content
Published on

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

Authors

はじめに

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

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

標準NetworkPolicyの限界とCNPの拡張

能力k8s NetworkPolicyCiliumNetworkPolicy
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
  1. 強制への切り替え: 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-apiserverAPIサーバ
healthCiliumヘルスチェックエンドポイント
# クラスタ内部通信 + 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を扱います。

参考資料