MCP server Kubernetes security

MCP server Kubernetes security — RBAC, NetworkPolicy, Secrets CSI, and Pod Security Standards

Running an MCP server on Kubernetes introduces a layer of infrastructure security that Docker containers alone don't address. Kubernetes has its own identity system (service accounts), network model (NetworkPolicy), secrets management layer (Secrets Store CSI Driver), and workload admission controls (Pod Security Standards). A compromised MCP server pod in a misconfigured cluster can escalate to cluster-admin via the default service account token, exfiltrate secrets from other namespaces, or pivot to cloud provider APIs via IMDS. Each risk has a specific Kubernetes control that eliminates it.

Pattern 1: Service account RBAC least-privilege

Kubernetes auto-mounts a service account token in every pod by default. The default service account in many clusters has been granted broad roles — sometimes cluster-admin — by operators who wanted to "just make it work." An attacker who gains code execution in an MCP server pod can read the mounted token at /var/run/secrets/kubernetes.io/serviceaccount/token and use it to query the Kubernetes API for secrets, create new pods, or modify deployments.

# WRONG: MCP server pod using default service account (which may have broad permissions)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-server
spec:
  template:
    spec:
      # No serviceAccountName = uses 'default' service account
      containers:
      - name: mcp-server
        image: myorg/mcp-server:1.2.3

---
# RIGHT: dedicated service account with no roles, auto-mount disabled
apiVersion: v1
kind: ServiceAccount
metadata:
  name: mcp-server
  namespace: mcp-prod
automountServiceAccountToken: false   # disable auto-mount; re-enable only if K8s API calls needed

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mcp-server
  namespace: mcp-prod
spec:
  template:
    spec:
      serviceAccountName: mcp-server   # dedicated SA
      automountServiceAccountToken: false
      containers:
      - name: mcp-server
        image: myorg/mcp-server@sha256:abc123  # digest-pinned image

If the MCP server does need to make Kubernetes API calls (e.g. to read a ConfigMap for dynamic configuration), bind only the minimum required verbs to a namespaced Role — never a ClusterRole — and never grant secrets/get or wildcard resource access:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: mcp-server
  namespace: mcp-prod
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["mcp-server-config"]  # specific resource only
  verbs: ["get", "watch"]               # no create/update/delete/patch

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: mcp-server
  namespace: mcp-prod
subjects:
- kind: ServiceAccount
  name: mcp-server
  namespace: mcp-prod
roleRef:
  kind: Role
  name: mcp-server
  apiGroup: rbac.authorization.k8s.io

Pattern 2: NetworkPolicy default-deny isolation

Without a NetworkPolicy, all pods in a Kubernetes cluster can communicate with each other freely. A compromised MCP server can probe databases, internal APIs, and other MCP server pods in any namespace. NetworkPolicy lets you define what traffic the pod is allowed to initiate or receive at the network layer, independent of application-level controls.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mcp-server-deny-all-then-allow
  namespace: mcp-prod
spec:
  podSelector:
    matchLabels:
      app: mcp-server
  policyTypes: [Ingress, Egress]

  ingress:
  # Allow only from the ingress controller
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: ingress-nginx
    ports:
    - protocol: TCP
      port: 3000

  egress:
  # Allow DNS resolution
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53

  # Allow only to specific downstream services by label
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432

  # Block: IMDS (169.254.169.254), other pods, external IPs not listed above
  # NetworkPolicy is additive allowlisting — anything not listed is denied

Pattern 3: Secret Store CSI Driver instead of Kubernetes Secrets

Kubernetes Secrets are only base64-encoded, not encrypted at rest by default. Any pod with secrets/get permission in a namespace can read all secrets in that namespace. The Secrets Store CSI Driver integrates with external secret stores (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, GCP Secret Manager) and mounts secrets directly as files in the pod's filesystem — they never touch the Kubernetes etcd database.

# WRONG: storing credentials as a Kubernetes Secret (base64-encoded in etcd)
apiVersion: v1
kind: Secret
metadata:
  name: mcp-server-db-credentials
  namespace: mcp-prod
type: Opaque
data:
  DB_PASSWORD: c2VjcmV0cGFzc3dvcmQ=   # base64("secretpassword") — readable by anyone with secrets/get

---
# RIGHT: Secret Store CSI with AWS Secrets Manager
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: mcp-server-secrets
  namespace: mcp-prod
spec:
  provider: aws
  parameters:
    objects: |
      - objectName: "prod/mcp-server/db-credentials"
        objectType: "secretsmanager"
        jmesPath:
          - path: "password"
            objectAlias: "db-password"

---
# Pod spec that mounts secrets via CSI
spec:
  volumes:
  - name: secrets-store
    csi:
      driver: secrets-store.csi.k8s.io
      readOnly: true
      volumeAttributes:
        secretProviderClass: mcp-server-secrets
  containers:
  - name: mcp-server
    volumeMounts:
    - name: secrets-store
      mountPath: /mnt/secrets
      readOnly: true
    env:
    # Application reads from file, not env var — avoids /proc/PID/environ exposure
    - name: DB_PASSWORD_FILE
      value: /mnt/secrets/db-password

Pattern 4: Pod Security Standards restricted profile

The Kubernetes Pod Security Standards define three profiles: privileged, baseline, and restricted. The restricted profile enforces that pods run as non-root, drop all Linux capabilities, use a read-only root filesystem, disallow privilege escalation, and use a seccomp runtime default profile. Enforcing the restricted profile via namespace labels means that any Deployment that violates these constraints is rejected at admission — even if someone accidentally adds privileged: true to the container spec.

# Label the namespace to enforce the restricted profile
kubectl label namespace mcp-prod \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted

---
# Pod spec that passes the restricted profile
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 65534
    runAsGroup: 65534
    fsGroup: 65534
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: mcp-server
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]
    volumeMounts:
    - name: tmp
      mountPath: /tmp           # writable tmpfs for any temp file needs
  volumes:
  - name: tmp
    emptyDir: {}                # in-memory only, not persisted to node disk

These four Kubernetes controls — service account RBAC, NetworkPolicy isolation, Secrets Store CSI, and Pod Security Standards — form the baseline for any production MCP server deployment. SkillAudit's infrastructure scan checks for these patterns in submitted Kubernetes manifests alongside the application-level security analysis. For a full review, run a SkillAudit scan. Related: container security, secrets management, network security.