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-codexPod の隔離- 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 / LoadBalancer | ClusterIP + 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-codex | Kubernetes 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 token | host operator / Scheduler | Runner登録、更新、無効化、credential rotation |
| Runner credential | 各RunnerのSecret | status報告、job claim |
| Job lease token | jobをclaimしたRunner | heartbeat、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-runnerinfra-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
preinstallinstallpostinstallpreparechild_processexecspawnforkevalnew 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-codexのsecurityContextを強化していた。
今回は、以下にも同様のhardeningを適用した。
app-runnerinfra-codextodo-schedulertodo-scheduler-proxynpm-mirrornpm-approverpypi-mirrorpypi-approvergo-module-mirrorgomod-approvergit-baremock-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-scheduler | workspace、Runner、job、lease、実行結果を管理するControl Plane |
todo-scheduler-proxy | mTLSによる外部クライアント認証 |
app-runner | application repositoryを編集・検証するExecution Cell |
infra-codex | infrastructure repositoryを編集・検証するExecution Cell |
git-bare | app.git / infra.gitを提供する内部Git Service |
npm-mirror / npm-approver | npm packageの取得、scan、承認、配布 |
pypi-mirror / pypi-approver | Python packageの取得、承認、配布 |
go-module-mirror / gomod-approver | Go moduleの取得、承認、配布 |
mock-server | アプリケーション検証用の内部Service |
mcp-playwright | Playwright / MCP関連の処理 |
Cilium / Hubble | NetworkPolicy 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エージェント実行基盤として必要な境界を整理していきたい。