Kubernetes Security · RBAC · Service Accounts
MCP server Kubernetes RBAC security — service account least-privilege audit
Every MCP server Pod in Kubernetes runs as a service account, and the RBAC bindings attached to that account determine what the compromised process can do inside the cluster. Wildcard resource grants, secrets/get permissions, and ClusterRoles scoped cluster-wide instead of to the MCP namespace are the three most common misconfigurations that turn an MCP server compromise into a cluster-wide privilege escalation. This reference covers how to enumerate exactly what your MCP server's service account can do, what to look for, and how to reduce it to the minimum necessary surface.
Enumerating MCP server SA permissions with kubectl
Before you can reduce permissions you need to know what the service account currently has. The kubectl auth can-i --list subcommand accepts an --as flag that impersonates any service account, printing every resource/verb combination the SA can perform. Run it against your MCP server's SA in its deployment namespace:
# Full permission dump for the mcp-server SA in the mcp-prod namespace kubectl auth can-i --list \ --as=system:serviceaccount:mcp-prod:mcp-server \ --namespace mcp-prod # Example output (dangerous configuration): # Resources Verbs # *.* [*] # secrets [get list watch create update delete] # pods/exec [create] # pods/attach [create] # deployments.apps [get list watch update patch] # Safe output looks like: # configmaps [get watch] # (nothing else)
For a more readable cross-resource view, use rakkess (access matrix tool). It shows a compact table of what the SA can do across every resource type registered in the cluster, making it easy to spot unexpected grants at a glance:
# Install rakkess via krew plugin manager kubectl krew install access-matrix # Show what mcp-server SA can do across all resource types kubectl access-matrix \ --as=system:serviceaccount:mcp-prod:mcp-server \ --namespace mcp-prod # Output columns: Resource | get | list | create | update | delete | watch # Look for any row with a checkmark in create/update/delete for sensitive resources
To answer the inverse question — who can perform a specific sensitive action — use kubectl-who-can:
# Who can read secrets in the mcp-prod namespace? kubectl who-can get secrets --namespace mcp-prod # Who can exec into pods cluster-wide? kubectl who-can create pods/exec # Who can list all secrets cluster-wide? kubectl who-can list secrets
Dangerous RBAC misconfigs: wildcards, secrets, and ClusterRole scope
Three patterns are reliably dangerous for MCP server service accounts. The first is wildcard resource access — resources: ["*"] — in any role bound to the SA. This grants the SA the ability to read, modify, or delete every API resource in scope, including secrets, pods, and deployments. A single prompt injection or SSRF that reaches the Kubernetes API via the automounted service account token turns into full namespace or cluster ownership:
# DANGEROUS — wildcard ClusterRole bound to MCP server SA apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: mcp-server-role # ClusterRole: applies cluster-wide rules: - apiGroups: ["*"] resources: ["*"] # Every resource type verbs: ["*"] # Every verb including create/delete/escalate --- # This ClusterRoleBinding makes it cluster-wide (not namespace-scoped) apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: mcp-server-binding subjects: - kind: ServiceAccount name: mcp-server namespace: mcp-prod roleRef: kind: ClusterRole name: mcp-server-role apiGroup: rbac.authorization.k8s.io
The second dangerous pattern is secrets/get permission. Kubernetes Secrets store TLS certificates, database passwords, API keys, and other MCP server credentials. An SA that can GET secrets can read every secret in scope — including the secrets of every other workload in the namespace. The third is using ClusterRole + ClusterRoleBinding instead of a namespace-scoped Role + RoleBinding. Even a narrow ClusterRole (only configmaps/get) scoped cluster-wide lets the MCP server read ConfigMaps in every namespace including kube-system.
Minimum-permission RBAC for a typical MCP server
A typical MCP server needs to read its own ConfigMap for configuration and possibly watch it for live reload. It does not need to read secrets (credentials should be injected via environment variables from a secret volume, not fetched via the API), exec into pods, list other workloads, or access any other namespace. The minimum-permission RBAC manifest looks like this:
# SAFE — namespace-scoped Role with exactly the permissions needed
apiVersion: v1
kind: ServiceAccount
metadata:
name: mcp-server
namespace: mcp-prod
automountServiceAccountToken: false # Opt out: no API access needed at all
# Only set true if the SA must call the K8s API
---
# If the SA does need API access, create a minimal namespace-scoped Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # Role: namespace-scoped, not ClusterRole
metadata:
name: mcp-server-role
namespace: mcp-prod
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["mcp-server-config"] # Only THIS specific ConfigMap by name
verbs: ["get", "watch"] # Only get and watch; no list, create, update, delete
# Explicitly NOT granted:
# - secrets (any verb)
# - pods/exec or pods/attach
# - deployments, services, or any other resource type
# - any verb in other namespaces
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding # RoleBinding: namespace-scoped
metadata:
name: mcp-server-binding
namespace: mcp-prod
subjects:
- kind: ServiceAccount
name: mcp-server
namespace: mcp-prod
roleRef:
kind: Role # Binds to Role, not ClusterRole
name: mcp-server-role
apiGroup: rbac.authorization.k8s.io
Notice automountServiceAccountToken: false on the ServiceAccount itself. This prevents Kubernetes from mounting the SA token into every Pod that uses this account, even if the Pod's spec does not explicitly request it. Set this to false as the default for all MCP server SAs, and override it to true only on Pods that genuinely need to make Kubernetes API calls.
Using rbac-lookup to audit binding chains
RBAC bindings can be indirect: a service account may be in a group, and the group may be bound to a ClusterRole. Standard kubectl auth can-i resolves these chains, but rbac-lookup makes the binding chain human-readable:
# Install rbac-lookup via krew kubectl krew install rbac-lookup # Show all roles/clusterroles bound to the mcp-server SA kubectl rbac-lookup mcp-server --kind ServiceAccount --namespace mcp-prod # Example output: # SUBJECT SCOPE ROLE # mcp-server (SA) mcp-prod Role/mcp-server-role # mcp-server (SA) cluster-wide ClusterRole/view ← unexpected cluster-wide binding # Find all SAs bound to ClusterRoles (cluster-wide scope = high risk) kubectl rbac-lookup --output wide | grep -E "cluster-wide.*ClusterRole" | grep mcp
The view ClusterRole is a built-in role that grants read access to most resource types across the entire cluster. It is frequently attached to SAs as a "safe read-only role" — but it includes secrets/get in many Kubernetes versions and grants visibility into every namespace's workloads. Never bind the built-in view ClusterRole to an MCP server SA.
SkillAudit findings
resources: ["*"] or verbs: ["*"] in a ClusterRole grants the SA ability to read, modify, or delete every API object in the cluster. A single SSRF to the Kubernetes API service yields full cluster control. Remove the wildcard and enumerate only required resources explicitly.
automountServiceAccountToken: false. The SA token is mounted into the container filesystem at /var/run/secrets/kubernetes.io/serviceaccount/token and is accessible to any process in the container. If the MCP server does not call the Kubernetes API, disable token mounting.
pods/exec subresource grant from the role.
Paste a GitHub URL at skillaudit.dev to get a graded report card covering RBAC configuration alongside all other MCP security dimensions.