Kubernetes MCPサーバーの権限設計 — 本番クラスタに読み取り専用で繋ぐ
本番クラスタにKubernetes MCPサーバーを繋ぐ前提で、専用ServiceAccountの作成からTOMLでのアクセス制御までを手順化します。
Kubernetes MCPサーバーを個人の開発クラスタで試したあと、本番クラスタに繋ぐ段になると事情が変わります。既定の接続方法は自分のkubeconfigをそのまま使うため、AIエージェントに管理者権限そのものを渡すことになるからです。専用のServiceAccountを読み取り専用のRBACロールに紐付け、read_onlyと組み合わせるのが最低ラインです。
なお、read_onlyの渡し方はMCPサーバーの版で異なります。npmで公開済みの0.0.67(2026年9月18日公開)は--read-onlyフラグを受け付けます。GitHubのmainブランチのドキュメントでは、実行時のCLIフラグが廃止されTOMLのread_only = trueに一本化されました。この記事の手順はどちらの版でも動くよう、TOML設定ファイルを--configで渡す形にそろえています。
なぜkubeconfigをそのまま渡してはいけないか
~/.kube/configには、多くの場合クラスタ管理者権限に近いユーザー認証情報が入っています。MCPサーバーにこのファイルを渡すと、ツール呼び出しの権限はあなたの権限そのものになります。プロンプトインジェクションで意図しない削除コマンドが実行されるリスクや、単純な指示ミスでNamespaceごとPodを消してしまうリスクは、この権限の大きさに比例します。
最小権限の原則に沿うなら、専用のID(ServiceAccount)を用意し、そのIDにだけ必要最小限の権限を与えます。Kubernetes MCPサーバー自体も、公式ドキュメントで本番構成として専用ServiceAccountの作成を推奨しています。
本番向けの防御は、効く場所の違う層を重ねて作ります。
重ねる4つの防御層
ID(RBAC)
専用のServiceAccountに
viewロールだけを与え、Kubernetes API側で権限を絞ります。ツールの公開範囲
read_onlyで、書き込み系のMCPツールをクライアントに見せません。リソース種別の拒否
denied_resourcesで、SecretやRBAC関連のリソースを種別ごと塞ぎます。事前検証
validation_enabledで、書き込みがAPIに届く前にスキーマとRBACを検査します。
読み取り専用のServiceAccountを作る
まずMCPサーバー専用のNamespaceとServiceAccountを作成します。
kubectl create namespace mcp
kubectl create serviceaccount mcp-viewer -n mcp権限の与え方は2通りあります。クラスタ全体を読み取り専用にするか、特定のNamespaceだけに絞るかです。
# クラスタ全体を読み取り専用にする(組み込みのviewロールを使う)
kubectl create clusterrolebinding mcp-viewer-crb \
--clusterrole=view \
--serviceaccount=mcp:mcp-viewer
# 特定Namespaceだけに絞る場合はこちら(例: production)
kubectl create rolebinding mcp-viewer-rb \
--clusterrole=view \
--serviceaccount=mcp:mcp-viewer \
-n productionviewはRoleではなくClusterRoleです。RoleBindingから参照するときも--clusterrole=viewを使い、権限が及ぶのは-nで指定したNamespaceに限られます。MCPサーバーのGetting Startedには--role=viewと書かれた例がありますが、viewという名前のRoleは標準では存在しないため、そのまま打つと参照先が見つかりません。ServiceAccountを置くmcpと、読ませたいNamespaceは別でかまいません。
権限が意図通りかはkubectl auth can-iで確認できます。
kubectl auth can-i list pods \
--as=system:serviceaccount:mcp:mcp-viewer --all-namespacesyesが返れば、クラスタ全体でPod一覧の読み取りが許可されている状態です。Kubernetesの公式ドキュメントでは、viewはRoleとRoleBindingも、Secretも閲覧できないと説明されています。SecretからServiceAccountの資格情報を読み出して権限昇格できてしまうためです。同じ理由で、editロールはSecretにアクセスできる点に注意が必要です。
専用トークンとkubeconfigを組み立てる
次に、このServiceAccount用の期限付きトークンを発行します。kubectl create tokenはKubernetes 1.24以降で使えるTokenRequest API経由のコマンドです。
TOKEN="$(kubectl create token mcp-viewer --duration=2h -n mcp)"このトークンを使い、管理者kubeconfigとは別ファイルの専用kubeconfigを作ります。
API_SERVER="$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')"
KUBECONFIG_FILE="$HOME/.kube/mcp-viewer.kubeconfig"
CA_FILE="$(kubectl config view --raw --minify \
-o jsonpath='{.clusters[0].cluster.certificate-authority}')"
if [ -z "$CA_FILE" ]; then
CA_FILE="$(mktemp)"
kubectl config view --raw --minify \
-o jsonpath='{.clusters[0].cluster.certificate-authority-data}' \
| base64 -d > "$CA_FILE"
fi
kubectl config --kubeconfig="$KUBECONFIG_FILE" set-cluster mcp-viewer-cluster \
--server="$API_SERVER" --certificate-authority="$CA_FILE" --embed-certs=true
kubectl config --kubeconfig="$KUBECONFIG_FILE" set-credentials mcp-viewer \
--token="$TOKEN"
kubectl config --kubeconfig="$KUBECONFIG_FILE" set-context mcp-viewer-context \
--cluster=mcp-viewer-cluster --user=mcp-viewer
kubectl config --kubeconfig="$KUBECONFIG_FILE" use-context mcp-viewer-context
chmod 600 "$KUBECONFIG_FILE"--embed-certs=trueを付けたset-clusterは--certificate-authorityを必須とするため、上のコマンドにCAの取得を含めています。kubeconfigにCAデータ(certificate-authority-data)しかない場合は、base64デコードして一時ファイルに書き出し、それを渡します。トークンは2時間で失効するため、長時間の運用にはトークンの自動更新か、有効期限を伸ばした運用ルールが必要です。失効後はkubectl create tokenを再実行し、set-credentialsでトークンだけ差し替えます。
Claude Codeに読み取り専用で接続する
専用kubeconfigができたら、MCPサーバーの設定ファイルを作ります。TOMLにはパスの展開機能がないため、kubeconfigには絶対パスを書きます。
mkdir -p "$HOME/.config/kubernetes-mcp-server"
cat > "$HOME/.config/kubernetes-mcp-server/config.toml" <<EOF
read_only = true
kubeconfig = "$HOME/.kube/mcp-viewer.kubeconfig"
EOFこの設定ファイルを--configで指定して、Claude Codeに追加します。
claude mcp add-json kubernetes-mcp-server \
'{"command":"npx","args":["-y","kubernetes-mcp-server@0.0.67","--config","'${HOME}'/.config/kubernetes-mcp-server/config.toml"]}' \
-s userバージョンを固定しているのは、起動方法の仕様が版をまたいで変わっている最中だからです。
read_onlyの渡し方は版で違う
0.0.67まで
kubernetes-mcp-server --read-onlyのように、起動時のフラグで指定できます。--portや--kubeconfigなどのフラグも同様です。TOMLのread_only = trueも効くため、フラグなしの--configだけの起動も成立します。
mainブランチ
実行時のフラグが廃止され、残るのは--version・--config・--config-dirの3つです。--read-onlyはread_onlyに、--portはportに置き換わります。環境変数K8S_MCP_CONFIG_PATHもMCP_CONFIG_PATHへ改名され、旧名は読まれません。
0.0.67のヘルプ(kubernetes-mcp-server --help)には--read-only、--port、--kubeconfig、--config、--config-dirが並びます。mainブランチの移行ガイドには、廃止済みのフラグをChartのextraArgsやコンテナのargsに残すと、cobraのunknown flagで失敗するとあります。
read_onlyは、readOnlyHint=trueが付いたツールだけを公開し、作成・更新・削除系の操作をサーバー側で遮断します。RBACによる権限制限とこの設定は独立した2層です。片方だけに頼らず両方を重ねておくと、RBAC設定に穴があってもMCPサーバー側で書き込みを止められます。
クラスタ内にMCPサーバー自体をデプロイする
ローカル端末で専用kubeconfigを保持する構成には、トークンファイルがローカルディスクに残るという弱点があります。MCPサーバーをクラスタ内のPodとして動かせば、この弱点を避けられます。
Podの中で起動したKubernetes MCPサーバーは、in-clusterの実行環境を自動検知し、そのPodに紐づくServiceAccountのトークンを自動的に使います。手元でトークンを発行してファイルに書き出す手順そのものが不要になり、トークンの取り扱いミスによる漏えいリスクも下がります。DeploymentのServiceAccountに、先ほど作ったmcp-viewerを指定するだけです。
apiVersion: v1
kind: ConfigMap
metadata:
name: kubernetes-mcp-server-config
namespace: mcp
data:
config.toml: |
port = "8080"
read_only = true
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kubernetes-mcp-server
namespace: mcp
spec:
selector:
matchLabels:
app: kubernetes-mcp-server
template:
metadata:
labels:
app: kubernetes-mcp-server
spec:
serviceAccountName: mcp-viewer
containers:
- name: kubernetes-mcp-server
image: <MCPサーバーのコンテナイメージ>
args: ["--config", "/etc/kubernetes-mcp-server/config.toml"]
volumeMounts:
- name: config
mountPath: /etc/kubernetes-mcp-server
volumes:
- name: config
configMap:
name: kubernetes-mcp-server-configimageの値は、使うKubernetes MCPサーバーのコンテナイメージに置き換えてください。Podで動かすならHTTPモードが前提です。portが空のstdioモードは、ローカルのクライアントがプロセスを起動してパイプで会話する形で、Podには向きません。設定がmainブランチの形式なので、--portや--read-onlyをコンテナのargsに並べるとunknown flagで起動しません。HTTPモードでは、kubeconfigの中身を返すconfiguration_viewツールが既定で無効になります。有効にするならenabled_toolsに明示し、公開したい他のツールもすべて並べる必要があります。
この構成が既定で見ているのは同じクラスタのAPIサーバーです。別クラスタを見に行かせたい場合は、kubeconfigにそのファイルのパスを書けば、in-cluster検知より優先されます。cluster_provider_strategy = "kubeconfig"だけを書いてkubeconfigを省くと、in-cluster検知が残るため失敗します。外部クラスタ用のkubeconfigはSecretとしてマウントし、kubeconfig = "/etc/kubernetes-mcp-server/kubeconfig"のようにパスを渡します。
TOML設定でさらに絞り込む
CLIフラグより細かい制御が要るなら、TOML設定ファイルを使います。特定リソース種別へのアクセスを丸ごと拒否するdenied_resourcesは、Secretのような機密性の高いリソースを追加でブロックするのに向いています。
read_only = true
toolsets = ["core", "config"]
[[denied_resources]]
group = ""
version = "v1"
kind = "Secret"
[[denied_resources]]
group = "rbac.authorization.k8s.io"
version = "v1"
kind = "ClusterRoleBinding"toolsetsの既定値は["core", "config"]なので、この例は既定と同じ指定です。なお、stdioモードではstdoutがMCPプロトコルに使われるため、ログを取るにはlog_fileの指定が必要です。
RBAC自体がviewロールでSecretの中身を隠していても、この設定を重ねておけばRBAC側の設定ミスをもう1段吸収できます。公式の例には、ConfigMapやRole、RoleBinding、ClusterRoleも拒否する書き方が載っています。書き込みは許しつつ破壊的操作だけを止めたいならdisable_destructive = trueも選べます。
TOMLのキー名の打ち間違いは、起動時に表面化します。mainブランチのドキュメントでは、スキーマにないキーを含むファイルは読み込みに失敗し、起動時もSIGHUP時もプロセスが終了するとあります。read_onlyをreadonlyと書いたまま保護が効いていない、という状態になりにくい作りです。
削除のような特定操作だけ確認を挟みたい場合はconfirmation_rulesが使えます。
confirmation_fallback = "deny"
[[confirmation_rules]]
verb = "delete"
namespace = "kube-system"
message = "kube-system内の削除操作です。"確認プロンプト(elicitation)に対応していないMCPクライアントでは、confirmation_fallbackが挙動を決めます。既定値はallowで、確認なしに警告ログだけを残して実行されます。denyにしておけば、確認できない環境では自動的に操作を止める安全側の動作になります。
確認ルールには2つの階層があります。ツール名やdestructive = trueで照合するツール単位のルールは、ハンドラの実行前に1回だけ働きます。verbやkind、namespaceで照合するKubernetes API単位のルールは、ハンドラの中で呼ばれるAPIごとに働きます。1つのルールにツール単位とAPI単位の項目は混ぜられません。たとえばverb = "get"とkind = "Secret"を組み合わせれば、Secretの読み取りにも確認を挟めます。
ツール単位で許可・禁止を切り替えたいだけなら、enabled_tools(許可リスト)とdisabled_tools(拒否リスト、許可リストの後に適用)も使えます。「Pod関連のツールだけ使わせたい」といった細かい要求には、toolsets単位の粗い制御よりenabled_toolsの方が向いています。
enabled_tools = ["pods_list", "pods_get", "pods_log"]validation_enabled = trueを追加すると、スキーマ検証とSelfSubjectAccessReviewによるRBACの事前チェックが走ります。検証はHTTPのRoundTripperの位置で働き、HelmやKubeVirtなどのプラグインが出すAPI呼び出しも対象です。権限不足やマニフェストの不備を、APIに送る前にエラーとして返せます。リソース名の打ち間違い(Deploymntのような綴り)の検査は、この設定にかかわらず常に行われます。
複数人からHTTPで使うときの認証設計
チーム共有のMCPサーバーとしてHTTPモードで公開する場合は、require_oauthでOAuth認証を必須にできます。ドキュメントにはKeycloakとMicrosoft Entra IDの設定例があり、OIDCプロバイダーのURLをauthorization_urlに書く構成です。個人利用のstdio接続と違い、HTTPモードは誰が呼んでいるかを検証する層が別途必要です。stdioでportを空にしたままrequire_oauth = trueにすると、読み込みに失敗します。OAuthはHTTP専用の機能だからです。
require_oauthは「認証を要求する」だけで、トークンをどう検証するかは決めません。検証の方式はauthorization_urlとskip_jwt_verificationの組み合わせで決まります。
skip_jwt_verificationは「認証を省略する」設定ではありません。authorization_urlが未設定のとき、受け取ったBearerトークンを署名やaudienceの検査なしでそのままクラスタへ転送します。authorization_urlを設定している場合は効果を持たず、OIDCプロバイダーが通常どおり検証します。MCPサーバーはBearerヘッダーがあることしか見ないため、クラスタ側のAPIサーバーやリバースプロキシがトークンを検証する構成でなければ、実質的に無認証のサーバーになります。
検証を通ったユーザーの権限をKubernetes側にどう渡すかは、別の設定cluster_auth_modeが決めます。
cluster_auth_modeの2つの値
passthrough
Authorizationヘッダーがあれば、そのトークンをKubernetes APIへ転送し、ユーザー本人の権限で動かします。ヘッダーがないときはkubeconfigの権限にフォールバックします。
kubeconfig
ヘッダーの有無にかかわらず、常にkubeconfigの資格情報を使います。全ユーザーの操作が、サーバーに渡したServiceAccountの権限で実行されます。
ヘッダーのない呼び出しはkubeconfigの権限で動くため、HTTPで公開するサーバーに渡すkubeconfigにも、この記事で作った読み取り専用のServiceAccountを使います。
read_only・disable_destructive・denied_resourcesの効き方の違い
3つの制限はどれも「書き込みを止める」ように見えて、効く階層が異なります。
| 設定 | 止めるもの | 効かないケース |
|---|---|---|
read_only | 止めるものreadOnlyHint=trueのツールだけを公開し、書き込み系を非公開にする | 効かないケースリソース種別ごとの許可・拒否はできない |
disable_destructive | 止めるものdestructiveHint=trueのツール(削除・更新系)を無効にする | 効かないケースread_onlyが有効だとこの設定は効果を持たない |
denied_resources(TOML) | 止めるもの特定のGroup/Version/Kindへのアクセスを丸ごと拒否 | 効かないケース許可した種別への読み書きまでは制限しない |
本番クラスタではread_onlyを基本にしつつ、Secretのような機密リソースはdenied_resourcesで二重に塞ぐ構成が堅実です。似た考え方はClaude Codeのpermission・サンドボックス設計にも共通しており、AIエージェントに渡す権限は、ツール単位(enabled_tools)と操作単位(RBACやread_only)の両方で絞り込みます。
よくあるつまずき
- トークンが2時間で切れて動かなくなる:
--durationを延ばすか、set-credentialsでトークンを再発行する運用を先に決めておきます。kubeconfigを書き換えても、すでに起動したサーバーは再読み込みしません。kubeconfigのパスは、再起動が必要な設定の側に入ります viewロールなのに一部リソースが見えない:viewClusterRoleはSecretとRole、RoleBindingを閲覧対象から外しています。CustomResourceは、集約ルールでviewに加えられたものしか読めません。必要なら別途Roleを組みますkubectl auth can-iがnoを返す: RoleBinding/ClusterRoleBindingのServiceAccount名やNamespaceが実際の値と一致しているか確認します。system:serviceaccount:<namespace>:<name>の形式が正確でないと判定がずれます- TOMLを変更したのに反映されない: 設定ファイルの再読み込みにはプロセスへの
SIGHUP送信が必要ですが、これは--configまたは--config-dirを指定して起動したプロセスに限られます。指定なしで起動した場合、SIGHUPは無視されます。まず起動コマンドに設定ファイルの指定があるか見て、あればkill -HUP <PID>を実行します。WindowsではSIGHUPが使えないため、再起動で反映します - SIGHUPを送ったらサーバーが落ちた: ロギング、ツールの絞り込み、確認ルール、OAuthの設定はSIGHUPで反映されます。一方で
kubeconfigやcluster_provider_strategy、port、bind_address、metrics_port、tls_*、statelessは再起動が必要です。mainブランチのドキュメントでは、これらを変える内容でSIGHUPを受けると、反映されずにプロセスが終了すると説明されています。パース失敗やスキーマにないキーが原因のときも同様です
検証や一時利用が終わったら片付ける
検証用に作ったServiceAccountとRBACバインディングは、使い終えたら残さず削除します。放置したServiceAccountは、トークンさえ再発行できれば誰でもクラスタの読み取りに使える踏み台になりかねません。
kubectl delete clusterrolebinding mcp-viewer-crb
kubectl delete rolebinding mcp-viewer-rb -n production
kubectl delete serviceaccount mcp-viewer -n mcp
rm "$HOME/.kube/mcp-viewer.kubeconfig"継続利用する場合も、トークンの有効期限を短く保ち、kubectl auth can-iでの棚卸しを定期的に行うと、権限が意図せず広がっていないかを確認できます。
まとめ
RBACはKubernetes側の権限、read_onlyはMCPサーバーが見せるツールの範囲を絞るもので、片方では他方の穴を埋められません。kubeconfigが管理者権限のままなら、MCPサーバーの外側から書き込みが通ってしまうため、土台のRBACから先に決めるのが順序です。基本的な追加手順はKubernetes MCPサーバーの導入ガイドにあります。