Cilium + Kubernetes で AI エージェント用 Pod の外部通信を制御する構成を作った


1. 背景

以前、Docker コンテナの外向き通信制御について調査し、最終的には DOCKER-USER + ipset による IP allowlist 制御へ到達した。

この方式では、Docker の NAT / MASQUERADE は Docker に任せ、自前の外向き通信制御は DOCKER-USER chain と ipset で行う。

一方で、以下の課題が残っていた。

  • 許可対象は本来ドメインで管理したい
  • DNS 解決結果の IP を管理する必要がある
  • CDN や共有 IP を利用するサービスとの相性が悪い
  • IP ベース制御では、許可した IP 上の別ドメインへ到達できる可能性がある
  • AI エージェントに作業させる場合、外部通信先や内部リソースへの到達範囲をより明示的に制御したい

AI エージェントを開発環境内で動かす場合、以下のような動作が発生する。

  • 外部 API へ通信する
  • Git リポジトリを読む
  • npm / PyPI / Go module などの package registry へアクセスする
  • ソースコードを書き換える
  • テストや静的解析を実行する

そのため、

どの Pod が
どの内部サービスへ
どの外部ドメインへ
通信できるのか

を明示的に管理する必要がある。

今回は、Docker 単体の DOCKER-USER + ipset 方式から、Kubernetes + Cilium を利用した構成へ変更した。

本記事は、現時点で作成した構成と、その構成がこれまでの課題にどう対応しているかを整理するものである。


2. やりたいこと

今回の構成で扱う対象は以下。

  • AI / Codex 用の作業 Pod を Kubernetes 上に用意する
  • Pod から Kubernetes API や Docker socket へ直接触れないようにする
  • 外部通信は原則拒否する
  • 必要な OpenAI / Codex 系ドメインだけ FQDN ベースで許可する
  • npm / PyPI / Go module は直接 public registry へ行かせず、mirror / approver 経由にする
  • 内部 Git サーバーや mirror など、必要な Service だけ通信を許可する
  • NetworkPolicy / CiliumNetworkPolicy を Infrastructure as Code として管理する
  • 許可通信 / 拒否通信を検証スクリプトで確認できるようにする

3. これまでの課題と今回の対応

以前の DOCKER-USER + ipset 構成では、許可したいドメインを DNS 解決し、その結果の IP を ipset に登録して制御していた。

今回の構成では、以下のように対応した。

課題今回の対応
ドメイン単位で制御したいCilium の toFQDNs を利用
未許可通信を拒否したいnamespace 全体に default deny を適用
任意の DNS 解決を許可したくないCilium DNS rule で許可 DNS 名を制限
package registry へ直接アクセスさせたくないnpm / PyPI / Go module を mirror / approver 経由に変更
AI / Codex 用 Pod から Kubernetes API を触らせたくないServiceAccount token を mount しない
Docker socket や hostPath を触らせたくないPod に Docker socket / hostPath / kubeconfig を渡さない
設定ミスを早めに検知したいkubeconform / kube-score / kube-linter を利用
許可 / 拒否通信を確認したいverify-network.sh で検証

4. Cilium FQDN Policy

Cilium には FQDN ベースの Egress 制御機能がある。

今回の構成では、OpenAI / Codex 系の通信先を toFQDNs で許可している。

例:

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: todo-scheduler-egress
  namespace: default
spec:
  endpointSelector:
    matchLabels:
      app: todo-scheduler
  egress:
    - toFQDNs:
        - matchName: api.openai.com
        - matchName: auth.openai.com
        - matchName: chatgpt.com
        - matchName: ab.chatgpt.com
        - matchName: platform.openai.com
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP

この設定により、対象 Pod から指定 FQDN の TCP 443 への Egress を許可する。


5. 現在の構成

現時点の構成は以下。

flowchart TB
    subgraph Host["Host / minikube"]
        subgraph K8s["Kubernetes Cluster"]
            Deny["Default Deny<br/>NetworkPolicy"]

            subgraph Policies["CiliumNetworkPolicy"]
                FQDN["FQDN Egress<br/>OpenAI / Codex domains"]
                DNS["DNS Rules<br/>allowed names only"]
                Internal["Internal Service Egress<br/>Git / mirrors / approvers"]
            end

            subgraph Workloads["Workloads"]
                App["todo-scheduler<br/>Application Pod"]
                Codex["infra-codex<br/>AI / Codex workspace Pod"]
            end

            subgraph InternalServices["Internal Services"]
                Git["git-bare<br/>internal Git"]
                Npm["npm-mirror<br/>npm-approver"]
                PyPI["pypi-mirror<br/>pypi-approver"]
                GoMod["go-module-mirror<br/>gomod-approver"]
                Mock["mock-server"]
                MCP["mcp-playwright"]
            end
        end

        Internet["Internet<br/>allowed FQDN only"]
        K8sAPI["Kubernetes API"]
        PublicRegistry["Public registries<br/>npm / PyPI / Go"]
    end

    Deny --> Policies

    App --> FQDN
    App --> DNS
    App --> Internal

    Codex --> FQDN
    Codex --> DNS
    Codex --> Internal

    Internal --> Git
    Internal --> Npm
    Internal --> PyPI
    Internal --> GoMod
    Internal --> Mock
    Internal --> MCP

    FQDN --> Internet

    Codex -. blocked .-> K8sAPI
    Codex -. blocked .-> PublicRegistry

    classDef allowed fill:#e6ffed,stroke:#008000,color:#000;
    classDef blocked fill:#ffe6e6,stroke:#cc0000,color:#000;
    classDef control fill:#e6f0ff,stroke:#005fcc,color:#000;

    class FQDN,DNS,Internal,Deny control;
    class Internet,Git,Npm,PyPI,GoMod,Mock,MCP allowed;
    class K8sAPI,PublicRegistry blocked;

主要な Pod / Service は以下。

名称役割
todo-schedulerアプリケーション本体
infra-codexインフラソース編集用の隔離ワークスペース
git-bareapp.git / infra.git を提供する内部 Git サーバー
npm-mirror / npm-approvernpm 依存関係取得と承認
pypi-mirror / pypi-approverPyPI 依存関係取得と承認
go-module-mirror / gomod-approverGo module 依存関係取得と承認
mock-serverアプリケーション検証用
mcp-playwrightPlaywright / MCP 関連の検証用

infra-codexinfra リポジトリを編集するための Pod として用意している。

infra-codex で可能な操作は以下。

  • /workspace/infra の編集
  • 静的検証
  • Git 操作
  • OpenAI / Codex 系エンドポイントへの通信
  • mirror / approver への通信
  • git-bare への通信

infra-codex で許可しない操作は以下。

  • Kubernetes API への到達
  • Docker socket の利用
  • hostPath へのアクセス
  • public package registry への直接通信
  • 任意の外部ドメイン解決

6. infra-codex Pod の隔離

infra-codex Pod では、ServiceAccount token を mount しない。

spec:
  automountServiceAccountToken: false

また、以下を持たせない。

ServiceAccount token
kubeconfig
kubectl
minikube
Helm
Docker socket
hostPath
application data
application source mount

Pod の securityContext は以下。

securityContext:
  runAsUser: 1000
  runAsGroup: 1000
  fsGroup: 1000
  runAsNonRoot: true
  seccompProfile:
    type: RuntimeDefault

container 側の securityContext は以下。

securityContext:
  allowPrivilegeEscalation: false
  capabilities:
    drop:
      - ALL
  readOnlyRootFilesystem: true

7. package registry への直接通信を避ける

今回の構成では、public registry へ直接行かせず、mirror / approver を経由させる。

flowchart LR
    Codex["infra-codex Pod"]

    subgraph Mirrors["Internal mirrors / approvers"]
        NpmMirror["npm-mirror"]
        NpmApprover["npm-approver"]
        PyPIMirror["pypi-mirror"]
        PyPIApprover["pypi-approver"]
        GoMirror["go-module-mirror"]
        GoApprover["gomod-approver"]
    end

    Public["Public package registries<br/>npm / PyPI / Go"]

    Codex --> NpmMirror
    Codex --> NpmApprover
    Codex --> PyPIMirror
    Codex --> PyPIApprover
    Codex --> GoMirror
    Codex --> GoApprover

    Codex -. blocked .-> Public

    classDef allowed fill:#e6ffed,stroke:#008000,color:#000;
    classDef blocked fill:#ffe6e6,stroke:#cc0000,color:#000;

    class NpmMirror,NpmApprover,PyPIMirror,PyPIApprover,GoMirror,GoApprover allowed;
    class Public blocked;

infra-codex Pod では、環境変数で mirror / approver を参照する。

env:
  - name: NPM_CONFIG_REGISTRY
    value: http://npm-mirror:4873
  - name: PIP_INDEX_URL
    value: http://pypi-mirror:3141/root/pypi/+simple/
  - name: PYPI_APPROVER_URL
    value: http://pypi-approver:8080
  - name: NPM_APPROVER_URL
    value: http://npm-approver:8080
  - name: GOPROXY
    value: http://go-module-mirror:3000
  - name: GOSUMDB
    value: "off"
  - name: GOMOD_APPROVER_URL
    value: http://gomod-approver:8080

8. 検証スクリプト

ネットワーク制御については、verify-network.sh で確認する。

確認項目は以下。

todo-scheduler can read npm mirror
todo-scheduler cannot publish anonymously
unlabelled workload is denied cluster egress
todo-scheduler cannot query arbitrary DNS names
npm mirror cannot reach the public internet
infra-codex can reach git-bare
infra-codex can reach mirrors/approvers
infra-codex cannot resolve public registry directly
infra-codex cannot reach Kubernetes API

9. 静的検証

Kubernetes manifest については、validate.sh で静的検証を実行する。

実行内容は以下。

Shell syntax check
kubeconform
kube-score
kube-linter
git diff --check

kubeconform では Kubernetes schema と Cilium CRD schema を使って検証する。

kube-score では運用上の注意点を確認する。

kube-linter ではセキュリティ寄りの静的解析を行う。

この検証は静的検証であり、以下は対象外としている。

Cilium enforcement
PVC permissions
probes
Service routing
rollout behavior
image execution

10. Docker 単体構成から変わったこと

以前の Docker 単体構成では、主に以下のような制御をしていた。

flowchart LR
    App["Docker container"]
    Bridge["Docker bridge"]
    DU["DOCKER-USER"]
    IPSet["ipset<br/>allowed IPs"]
    Internet["Internet"]

    App --> Bridge
    Bridge --> DU
    DU --> IPSet
    IPSet --> Internet

今回の構成では、以下のように制御する。

flowchart LR
    Pod["Pod"]
    NP["NetworkPolicy<br/>default deny"]
    CNP["CiliumNetworkPolicy"]
    FQDN["FQDN / Service / Label based policy"]
    Dest["Allowed destinations"]

    Pod --> NP
    NP --> CNP
    CNP --> FQDN
    FQDN --> Dest

変更点は以下。

  • IP ではなく FQDN ベースの許可をマニフェストで表現する
  • Pod label 単位で通信制御する
  • 内部 Service への通信と外部 FQDN への通信を分ける
  • namespace default deny を前提にする
  • Git / package mirror / approver などを Kubernetes Service として定義する
  • AI / Codex 用 workspace を Pod として分離する
  • 静的検証とネットワーク検証をスクリプト化する

11. 現時点でできていること

現時点でできていることは以下。

  • namespace 全体の default deny
  • CiliumNetworkPolicy による FQDN ベースの OpenAI / Codex 系通信許可
  • Cilium DNS rule による許可 DNS 名の制御
  • 内部 Service への明示的な Egress 許可
  • infra-codex Pod の ServiceAccount token 無効化
  • infra-codex Pod から Kubernetes API へ到達できないことの検証
  • Docker socket / kubectl / Helm / minikube を持たない作業 Pod
  • npm / PyPI / Go module の mirror / approver 経由化
  • public registry への直接 DNS 解決拒否
  • unlabelled workload の egress 拒否
  • kubeconform / kube-score / kube-linter による静的検証
  • 検証スクリプトによる許可通信 / 拒否通信の確認

12. 未対応項目

未対応の項目は以下。

項目状況
Hubble による通信可視化未対応
DNS TTL / キャッシュ / IP 変動時の挙動確認未対応
CDN / 共有 IP 利用サービスでの制御精度確認未対応
Kyverno / OPA による独自ポリシー強制未対応
Docker Sandboxes との詳細比較未対応
Kata Containers / gVisor / Firecracker との比較未対応
Credential Proxy未対応
rootless Docker / Podman / BuildKit を使った sandbox 内ビルド未対応
本番運用前提の監査ログ設計未対応

今後、Docker Sandboxes、Kata Containers、gVisor、Firecracker などの sandbox 構成との比較を行いたい。

また、Cilium と Calico の比較も未対応である。


13. まとめ

以前は、Docker 単体で DOCKER-USER + ipset を使い、外向き通信を IP allowlist で制御していた。

この方式では、ドメイン単位の制御、DNS 解決結果の管理、AI エージェント用 Pod の通信先制御に課題があった。

今回、Kubernetes + Cilium を使い、以下の構成を作成した。

  • namespace default deny
  • Cilium FQDN Policy
  • DNS rule による名前解決制御
  • mirror / approver 経由の package 取得
  • infra-codex Pod の隔離
  • Kubernetes API / Docker socket への到達禁止
  • 静的検証
  • ネットワーク検証スクリプト

現時点では、Hubble による可視化、DNS TTL / CDN 挙動、Kyverno / OPA によるポリシー強制、Docker Sandboxes や Kata Containers との比較、Credential Proxy などは未対応である。