Kubernetes + Cilium の AI エージェント実行基盤を Control Plane / Runner 分離と mTLS で改良した


1. 背景

以前、Kubernetes + Cilium を利用して、AI エージェント用 Pod の外部通信を制御する構成を作成した。

前回の構成では、以下を実装した。

  • namespace 全体の default deny
  • Cilium の toFQDNs を利用した FQDN ベースの Egress 制御
  • DNS rule による名前解決先の制限
  • npm / PyPI / Go module の mirror / approver 経由化
  • infra-codex Pod の隔離
  • Kubernetes API / Docker socket / hostPath への到達禁止
  • kubeconform / kube-score / kube-linter による静的検証
  • verify-network.sh による許可通信 / 拒否通信の確認

一方で、前回の構成には以下の課題が残っていた。

  • todo-scheduler 自身がアプリケーションと AI 実行責務を持っていた
  • AI ジョブをどの Runner が実行するかという明確な管理モデルがなかった
  • Runner の登録、認証、lease、停止、再構築の境界が曖昧だった
  • Scheduler の外部公開が平文 HTTP を前提としていた
  • 内部 Git サーバーの公開範囲が広かった
  • mirror / approver Pod の securityContext が十分ではなかった
  • Hubble による通信可視化が未対応だった
  • AI 実行ログやネットワーク制御の検証項目が限定的だった

そこで今回は、単に Pod の NetworkPolicy を追加するだけではなく、以下の境界を明確にする方向で構成を改良した。

Control Plane
Execution Cell
Credential
Repository
Network
Public Entry Point

2. 主な改善点

前回からの主な変更は以下。

項目前回今回
AI 実行責務Scheduler と作業 Pod の境界が曖昧Scheduler と Runner を分離
Runner 管理固定 Pod を個別に管理Runner API v1 と登録・bindingを導入
ジョブ排他明確な仕組みなしjob lease と heartbeat を導入
Credential用途ごとの分離が弱いoperator / Runner / job lease の3種類に分離
Scheduler公開HTTP / LoadBalancerClusterIP + mTLS reverse proxy
Operator API公開面と同居localhost限定port-forward
Git外部から到達可能な構成が残るClusterIP化し、許可Podとhostだけに制限
Pod hardening主にAI PodのみScheduler、Runner、mirror、approver、Gitまで拡大
package scan脆弱性scan中心tarball構造、lifecycle script、危険コードも確認
version管理floating versionが一部存在CLI、runtime、container imageを固定
Network観測未対応Hubble / Hubble Relay / Hubble UIを有効化
Network検証基本的な許可・拒否Scheduler、Runner、mirror、Git、Kubernetes APIを検証

3. SchedulerをControl Planeへ変更した

今回の構成では、todo-scheduler を AI コード実行環境ではなく、Control Plane として扱う。

todo-scheduler の責務は以下。

  • logical workspace の管理
  • Runner の登録
  • workspace と Runner のbinding
  • foreground / background job のqueue管理
  • job lease の発行と更新
  • 実行結果、進捗、添付ファイルの保存
  • UI / HTTP API の提供
  • Runnerの状態とcapacityの管理

Codex自体の実行は、Schedulerでは行わない。

AI実行は以下のRunnerへ分離した。

Runner役割
app-runnerアプリケーションソースの編集と検証
infra-codexKubernetes manifestやinfraソースの編集、静的検証

概念的には以下の構成になる。

flowchart LR
    User["User / Browser"]
    Proxy["mTLS Proxy"]
    Scheduler["todo-scheduler<br/>Control Plane"]

    subgraph Cells["Execution Cells"]
        AppRunner["app-runner<br/>Application Cell"]
        InfraRunner["infra-codex<br/>Infrastructure Cell"]
    end

    User -->|HTTPS + Client Cert| Proxy
    Proxy --> Scheduler

    Scheduler -->|Runner API v1| AppRunner
    Scheduler -->|Runner API v1| InfraRunner

    AppRunner -->|progress / result| Scheduler
    InfraRunner -->|progress / result| Scheduler

この分離により、Schedulerへの侵害と、AIが実行したコードによる侵害を別の境界として扱える。


4. Runner API v1

RunnerとSchedulerの通信には、version付きのRunner APIを利用する。

Runner側では以下のbase URLを設定する。

RUNNER_API_BASE_URL=http://todo-scheduler:3000/api/runner/v1

主なAPIは以下。

POST /status
POST /claim
POST /jobs/{job-id}/heartbeat
POST /jobs/{job-id}/progress
POST /jobs/{job-id}/complete
POST /jobs/{job-id}/fail
POST /jobs/{job-id}/wait
POST /jobs/{job-id}/comments
POST /jobs/{job-id}/attachments
GET  /jobs/{job-id}/attachments/{attachment-id}

Runnerは定期的にstatusを報告し、以下の情報をSchedulerへ送る。

  • Runner ID
  • protocol version
  • capabilities
  • state
  • capacity
  • active job count
  • repository identity

Runnerがjobをclaimすると、Schedulerはjob単位のleaseを発行する。

queued
  ↓ claim
running
  ↓ heartbeat
lease renewed

complete / fail / cancelled / expired

heartbeatが停止した場合や、leaseが期限切れになった場合は、古いRunnerが処理を継続できないようにする。

これにより、Runnerが停止した場合やネットワークから切断された場合に、同じjobが複数のRunnerで継続実行されることを防ぐ。


5. Credentialを用途別に分離した

今回の構成では、credentialを以下の3種類に分けた。

Credential保持者用途
Operator tokenhost operator / SchedulerRunner登録、更新、無効化、credential rotation
Runner credential各RunnerのSecretstatus報告、job claim
Job lease tokenjobをclaimしたRunnerheartbeat、progress、complete、fail、attachment

Operator tokenはRunnerへ渡さない。

Runner credentialもRunnerごとに分離する。

Job lease tokenは一つのjobにのみ使用し、worktreeや永続PVCには保存しない。

flowchart TB
    Operator["Host Operator"]
    Scheduler["Scheduler"]
    Runner["Runner"]
    Job["Active Job"]

    Operator -->|Operator Token| Scheduler
    Runner -->|Runner Credential| Scheduler
    Scheduler -->|Job Lease Token| Job
    Job -->|Heartbeat / Progress / Result| Scheduler

mTLSは外部クライアントの端末認証に使用するが、Runner credentialやoperator tokenの代替にはしない。

用途の異なるcredentialを分けることで、一つのcredentialが漏えいした場合の影響範囲を小さくする。


6. RunnerとRepositoryのbinding

各Runnerには、処理可能なrepository identityを設定する。

例:

app-runner   → app.git
infra-codex  → infra.git

workspaceにもrepository名を保持し、Runnerとのbinding時およびjob claim時に一致を確認する。

workspace.repository_name
          ==
runner.repository_name

一致しないRunnerにはjobを渡さない。

これにより、app-runner が誤ってinfra用jobを実行したり、infra-codex がapp用jobをclaimしたりすることを防ぐ。

Runner API上のrepository identityは、job routingとworkspace分離のために利用する。


7. Runner lifecycleを明示した

Runnerの作成と削除は、Schedulerやplatformのdeployとは分離した。

./scripts/provision-runner.sh app
./scripts/provision-runner.sh infra

./scripts/deprovision-runner.sh app
./scripts/deprovision-runner.sh infra

以下のdeploy commandはRunnerを自動作成しない。

./scripts/deploy.sh all
./scripts/deploy.sh platform
./scripts/deploy.sh scheduler

Runnerを独立したexecution cellとして扱い、以下をRunnerごとに持たせる。

  • repository identity
  • source / workspace PVC
  • Codex Home PVC
  • Runner credential Secret
  • container image
  • CiliumNetworkPolicy
  • application固有のvalidation command

これにより、Control PlaneのdeployとAI実行環境のlifecycleを分離した。

Runnerに問題が発生した場合は、Scheduler全体を再deployせず、対象Runnerだけを停止・再作成できる。


8. Schedulerの外部公開をmTLS化した

以前はtodo-schedulerをLoadBalancer Serviceとして直接公開していた。

今回は、Scheduler本体をClusterIPに変更した。

apiVersion: v1
kind: Service
metadata:
  name: todo-scheduler
spec:
  type: ClusterIP

外部公開用に、nginxを利用したtodo-scheduler-proxyを追加した。

External Client
  ↓ HTTPS + Client Certificate
todo-scheduler-public

todo-scheduler-proxy

todo-scheduler ClusterIP

proxyでは以下を設定している。

  • TLS 1.2 / TLS 1.3
  • client certificate必須
  • private client CAによる検証
  • CRLによる失効確認
  • HSTS
  • request body size制限
  • header / body / upstream timeout
  • read-only root filesystem
  • non-root実行
  • access logからquery stringやauthorization headerを除外

また、公開proxyからoperator APIへアクセスした場合は404を返す。

location ^~ /api/operator/ {
    return 404;
}

Operator APIはlocalhost限定のport-forwardを使用する。

kubectl port-forward \
  --address 127.0.0.1 \
  service/todo-scheduler \
  3002:3000

これにより、一般利用者向けの公開面と、Runner管理用のoperator APIを分離した。


9. Client certificateの発行と失効

Client certificateは端末ごとに発行する。

例:

SCHEDULER_PUBLIC_DNS=scheduler.example.test \
SCHEDULER_PUBLIC_IP=192.168.39.240 \
  ./scripts/manage-scheduler-pki.sh issue phone-2026-01

生成される.p12には以下が含まれる。

client certificate
private key
certificate chain

.p12とpasswordは別経路で転送し、端末へのimport完了後に一時ファイルを削除する。

同じclient certificateを複数端末で共有せず、一端末につき一つのclient certificateを発行する。

端末を紛失した場合はclient certificateを失効させる。

./scripts/manage-scheduler-pki.sh revoke phone-2026-01

失効時にはCRLを更新し、nginx proxyへ反映する。

mTLSにより、ID/passwordだけではなく、許可されたclient certificateを持つ端末だけがSchedulerの公開入口へ接続できる構成にした。


10. 内部GitサーバーをClusterIP化した

前回の構成では、git-bareの公開範囲が広くなる可能性があった。

今回はgit-bareをClusterIP Serviceへ変更した。

apiVersion: v1
kind: Service
metadata:
  name: git-bare
spec:
  type: ClusterIP

CiliumNetworkPolicyでは、以下からの通信だけを許可する。

  • Scheduler
  • app-runner
  • infra-codex
  • hostの一時的なport-forward

hostからGitへアクセスする場合は、127.0.0.1にbindした一時port-forwardを使う。

Host Git
  ↓ localhost port-forward
git-bare ClusterIP

固定の外部IPやworld ingressは使用しない。

これにより、内部GitをAI実行基盤の内部Serviceとして扱い、外部ネットワークから直接到達できない構成にした。


11. package mirror / approverの強化

npm / PyPI / Go moduleについては、引き続きpublic registryへの直接通信を禁止し、mirror / approverを経由する。

flowchart LR
    Runner["AI Runner"]

    subgraph Internal["Internal Supply Chain Services"]
        NpmMirror["npm-mirror"]
        NpmApprover["npm-approver"]
        PyPIMirror["pypi-mirror"]
        PyPIApprover["pypi-approver"]
        GoMirror["go-module-mirror"]
        GoApprover["gomod-approver"]
    end

    Public["Public Registries"]

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

    Runner -. denied .-> Public

    NpmApprover --> Public
    PyPIApprover --> Public
    GoApprover --> Public

npm mirrorへのpublishは、匿名ユーザーやAI Runnerには許可しない。

approverのみが認証付きでpublish可能な構成にした。

AI Runnerは、承認済みpackageをmirrorから読み取ることだけができる。

npm packageの検査では、以下を確認する。

  • OSV Scannerによる既知脆弱性
  • compressed tarball size
  • expanded size
  • archive内のfile数
  • absolute path
  • ../ path traversal
  • 複数のarchive root
  • symbolic link
  • unsupported file type
  • preinstall
  • install
  • postinstall
  • prepare
  • child_process
  • exec
  • spawn
  • fork
  • eval
  • new Function
  • 不審なnetwork access
  • 不審なfilesystem操作
  • credentialやtokenに関連する文字列

scannerは悪意あるpackageを完全に判定できるものではない。

そのため、以下を組み合わせたdefense-in-depthとして扱う。

NetworkPolicy
mirror
approver
static scan
known vulnerability scan
install script restriction
read-only filesystem
resource limits

12. Workload hardeningの対象を拡大した

以前は主にinfra-codexsecurityContextを強化していた。

今回は、以下にも同様のhardeningを適用した。

  • app-runner
  • infra-codex
  • todo-scheduler
  • todo-scheduler-proxy
  • npm-mirror
  • npm-approver
  • pypi-mirror
  • pypi-approver
  • go-module-mirror
  • gomod-approver
  • git-bare
  • mock-server

基本設定は以下。

spec:
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault

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

必要な書き込み先だけをemptyDirまたはPVCとしてmountする。

例:

/workspace
/data
/tmp
/home/node/.codex
/home/node/.cache
/home/node/.config
/home/node/.npm

また、CPU / memoryのrequestsとlimitsを設定し、不正packageやAI実行によるresource枯渇の影響を制限する。

Podには以下を渡さない。

Docker socket
hostPath
host network
kubeconfig
不要なServiceAccount token
cluster administrator credential

13. ToolとContainer versionを固定した

以前は一部でlatestやversion未指定のglobal installを利用していた。

今回は主要toolとcontainer imageのversionを固定した。

例:

Codex CLI
Claude Code
Antigravity
Bun
Go
Node.js
Python
Playwright MCP
Verdaccio
OSV Scanner
kubeconform
kube-score
kube-linter
nginx

downloadしたbinaryやarchiveについては、可能な範囲でchecksumも確認する。

例:

SHA-256
SHA-512
container image version
CLI version
runtime version

これにより、同じDockerfileから異なるversionが突然導入される可能性を減らし、buildの再現性を高めた。

また、更新時にはversionを明示的に変更し、静的検証と動作確認を行う。


14. Hubbleによる通信可視化

前回は未対応だったHubbleを有効化した。

Ciliumのinstall時に以下を設定する。

hubble.enabled=true
hubble.relay.enabled=true
hubble.ui.enabled=true
hubble.metrics.enableOpenMetrics=true

収集するmetricsは以下。

dns
drop
tcp
flow
port-distribution
icmp
httpV2

これにより、以下を確認できるようになった。

  • どのPodが通信を開始したか
  • 宛先Pod / Service
  • DNS query
  • 許可された通信
  • 拒否された通信
  • 使用port
  • TCP flow
  • HTTP flow

例えば、以下のような確認ができる。

hubble observe --from-pod default/infra-codex
hubble observe --from-pod default/app-runner
hubble observe --verdict DROPPED
hubble observe --to-fqdn api.openai.com

Hubble UIを利用することで、PodやService間の通信関係も視覚的に確認できる。

HubbleはNetworkPolicyが意図どおり動作しているかを確認するための観測手段として利用する。


15. 検証スクリプトの拡張

verify-network.shでは、以下を検証する。

todo-scheduler cannot reach npm mirror
todo-scheduler cannot publish to npm mirror
todo-scheduler can reach mock-server
todo-scheduler cannot resolve OpenAI endpoints
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 internal mirrors / approvers
infra-codex can reach approved OpenAI endpoints
infra-codex cannot resolve public registry
infra-codex cannot reach Kubernetes API
app-runner can reach Scheduler
untrusted Pod cannot reach Scheduler directly
public Scheduler rejects clients without a certificate
app-runner can reach git-bare
app-runner can reach approved OpenAI endpoints
app-runner cannot reach Kubernetes API

ネットワーク制御では、許可される通信だけでなく、拒否されるべき通信が実際に失敗することを確認する。

例:

AI Runner → internal mirror: allowed
AI Runner → public registry: denied
AI Runner → approved OpenAI FQDN: allowed
AI Runner → arbitrary domain: denied
AI Runner → Kubernetes API: denied
AI Runner → internal Git: allowed
untrusted Pod → Scheduler: denied

静的検証では引き続き以下を実行する。

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

重要なのは、manifestが正しい形であることと、実際に通信が拒否されることは別である点である。

Static validation
  !=
Runtime enforcement verification

そのため、静的検証と実環境のnetwork testを分離している。


16. 現在の構成

現在の構成は以下。

flowchart TB
    Client["PC / Phone<br/>Client Certificate"]
    HostOperator["Host Operator"]

    subgraph Cluster["Kubernetes / minikube"]
        DefaultDeny["Namespace Default Deny"]

        Proxy["todo-scheduler-proxy<br/>nginx / mTLS"]
        Scheduler["todo-scheduler<br/>Control Plane"]

        subgraph Runners["Execution Cells"]
            AppRunner["app-runner<br/>app.git"]
            InfraRunner["infra-codex<br/>infra.git"]
        end

        Git["git-bare<br/>ClusterIP"]

        subgraph SupplyChain["Internal Package Supply Chain"]
            Npm["npm-mirror / approver"]
            PyPI["pypi-mirror / approver"]
            Go["go-module-mirror / approver"]
        end

        Mock["mock-server"]
        MCP["mcp-playwright"]
        Hubble["Cilium / Hubble"]
    end

    OpenAI["Approved OpenAI FQDNs"]
    Registries["Public Package Registries"]
    K8sAPI["Kubernetes API"]

    Client -->|HTTPS + mTLS| Proxy
    Proxy --> Scheduler

    HostOperator -->|localhost port-forward<br/>Operator Token| Scheduler

    AppRunner -->|Runner API<br/>Runner Credential / Lease| Scheduler
    InfraRunner -->|Runner API<br/>Runner Credential / Lease| Scheduler

    AppRunner --> Git
    InfraRunner --> Git
    Scheduler --> Git

    AppRunner --> SupplyChain
    InfraRunner --> SupplyChain

    AppRunner --> OpenAI
    InfraRunner --> OpenAI

    SupplyChain --> Registries

    AppRunner -. denied .-> Registries
    InfraRunner -. denied .-> Registries

    AppRunner -. denied .-> K8sAPI
    InfraRunner -. denied .-> K8sAPI

    Hubble -. observes .-> Scheduler
    Hubble -. observes .-> AppRunner
    Hubble -. observes .-> InfraRunner

各コンポーネントの役割は以下。

名称役割
todo-schedulerworkspace、Runner、job、lease、実行結果を管理するControl Plane
todo-scheduler-proxymTLSによる外部クライアント認証
app-runnerapplication repositoryを編集・検証するExecution Cell
infra-codexinfrastructure repositoryを編集・検証するExecution Cell
git-bareapp.git / infra.gitを提供する内部Git Service
npm-mirror / npm-approvernpm packageの取得、scan、承認、配布
pypi-mirror / pypi-approverPython packageの取得、承認、配布
go-module-mirror / gomod-approverGo moduleの取得、承認、配布
mock-serverアプリケーション検証用の内部Service
mcp-playwrightPlaywright / MCP関連の処理
Cilium / HubbleNetworkPolicy enforcementと通信可視化

17. 前回からできるようになったこと

今回の改良により、以下が実現できた。

  • SchedulerとAI実行環境の責務分離
  • Runner API v1による実行制御
  • Runner registration / binding
  • repository identityによるjob routing
  • Runner capacity管理
  • job lease / heartbeat / cancellation
  • operator / Runner / job credentialの分離
  • Runner単位のprovision / deprovision
  • Scheduler本体のClusterIP化
  • mTLS reverse proxyによる外部端末認証
  • Client certificateの端末単位での発行
  • CRLによるClient certificateの失効
  • Operator APIの公開面からの分離
  • git-bareのClusterIP化
  • mirror / approver / Gitを含むworkload hardening
  • npm mirrorのread-only consumer化
  • npm tarball構造の検査
  • npm lifecycle scriptの検査
  • 主要tool / imageのversion固定
  • Hubble / Hubble Relay / Hubble UIの有効化
  • Runtime network verificationの拡張
  • DNS queryと拒否flowの観測

前回は、

AI用PodをNetworkPolicyで囲む

ことが中心だった。

現在は、

Control Plane
  +
Execution Cell
  +
Credential Boundary
  +
Repository Boundary
  +
Network Boundary
  +
Device Authentication

を持つ実行基盤へ変わっている。


18. まとめ

前回は、Kubernetes + Ciliumを使って、AIエージェント用Podの外部通信を制限する構成を作成した。

今回の改良では、単純なNetworkPolicyの追加から一歩進み、以下を導入した。

  • SchedulerとRunnerの責務分離
  • Runner API v1
  • job leaseとheartbeat
  • credentialの用途別分離
  • repository identityとbinding
  • Runner単位のlifecycle
  • mTLSによる外部端末認証
  • Operator APIの分離
  • 内部GitのClusterIP化
  • supply chain workloadのhardening
  • npm tarball / static scanの強化
  • tool / image versionの固定
  • Hubbleによる通信可視化
  • network verificationの拡張

前回の構成は、

AI用Podを安全に動かすためのネットワーク制御

が中心だった。

現在は、

AI jobを
どのRunnerへ割り当て
どのcredentialを渡し
どのrepositoryを編集させ
どのnetworkへ到達させ
どの端末から操作できるか

を管理する、小規模なAIエージェント実行基盤へ変化している。

Docker単体のDOCKER-USER + ipsetによるIP allowlistから始めた調査は、Kubernetes + CiliumによるFQDN制御を経て、Control PlaneとExecution Cellを分離した構成まで進んだ。

今回の改良によって、ネットワーク制御だけではなく、Runnerのlifecycle、credential、repository、端末認証、package supply chainを個別の境界として扱えるようになった。

今後も、CiliumのNetworkPolicyだけに依存するのではなく、より強いruntime isolationやcredential管理、監査ログなどを組み合わせながら、AIエージェント実行基盤として必要な境界を整理していきたい。