microk8s-mcp View on GitHub

Security model

Giving a language model a path into your cluster is a real decision. This page is about what that path is bounded by — and, just as importantly, what it is not bounded by.

The two layers, and only one is a boundary

Two independent things decide what can happen to your cluster:

LayerConfigured byEnforced byIs it a boundary?
RBAC — what the credential is able to do rbac.yaml The Kubernetes API server Yes. Real, and enforced no matter who holds the token.
Env vars — what this server is willing to do MICROK8S_MCP_* This Python process No. A convenience. Anything else holding the token ignores it.

The asymmetry matters, and it only runs one way. A narrow RBAC binding makes a permissive env var harmless: if the token is bound only to the reader role, then MICROK8S_MCP_MODE=read-write still cannot hurt you, because the API server refuses.

The reverse is not true. MICROK8S_MCP_NAMESPACES=apps against a cluster-wide ClusterRoleBinding still leaves a token that can write anywhere — this server simply declines to be the one doing it. Anything else that reads the kubeconfig has no such scruples.

Set both layers. Use the env vars to keep the agent focused, and RBAC to make the boundary true.

What rbac.yaml grants

Applying it creates the mcp-system namespace, a claude-mcp ServiceAccount, a long-lived token Secret, and two ClusterRoles:

claude-mcp-reader — always bound

get/list/watch across the cluster, except Secrets. That exclusion is deliberate and it is the point: your database passwords, TLS keys and API tokens live in Secrets, and nothing that can read them should be feeding a model context window.

claude-mcp-operator — bound as shipped

create/update/patch/delete on deployments, statefulsets, daemonsets, pods, services, configmaps, PVCs, jobs, cronjobs, ingresses and networkpolicies — plus node patch and pod eviction. Secrets stay excluded here too, and the credential cannot escalate its own permissions.

The operator ClusterRoleBinding is active in the repository as shipped. kubectl apply -f rbac.yaml therefore grants cluster-wide write, in every namespace. That is a defensible default for a homelab you can rebuild in ten minutes, and the wrong one for anything else. Decide before you apply.

Three ways to shape the grant

In descending order of how much they actually protect you:

ApproachEnforced byEffect
Comment out the claude-mcp-operator binding API server No writes at all
Replace it with per-namespace RoleBindings API server Writes only in the namespaces you name
MICROK8S_MCP_NAMESPACES=apps,staging This server Server declines elsewhere; the credential could still do it

To narrow rather than remove, swap the ClusterRoleBinding for a RoleBinding per namespace — same roleRef and subjects, plus a namespace:

kind: RoleBinding
metadata:
  name: claude-mcp-operator
  namespace: apps          # one per namespace you want writable

Removing a binding needs more than an edit. kubectl apply will not delete a binding you merely deleted from the YAML. If you already applied it and want it gone now:

kubectl delete clusterrolebinding claude-mcp-operator

Audit what you actually have

Do not trust either layer's configuration — ask the API server. This is the only answer that counts:

kubectl --kubeconfig ~/.kube/claude-mcp.kubeconfig auth can-i --list

Two specific checks worth running after any change:

# Secrets must be unreadable — this is the one that must never come back "yes"
kubectl --kubeconfig ~/.kube/claude-mcp.kubeconfig auth can-i list secrets -A

# Can it write where you think it cannot?
kubectl --kubeconfig ~/.kube/claude-mcp.kubeconfig auth can-i delete deployments -n kube-system

The kubeconfig itself holds a live bearer token. Keep it at mode 600 — the setup targets write it that way — and delete it when you tear the identity down, since the token stays valid until the Secret is gone.

What is deliberately missing

Some capabilities are absent by design, not oversight:

The kubectl fallback, and why it matters

One path deserves singling out. If the server cannot find a local kubectl, it falls back to running microk8s kubectl on the node — which executes under that node's admin credential. On that path the scoped kubeconfig and both roles stop applying, and Secrets become readable again.

This is a genuine escalation, and it happens quietly. The trigger is mundane: a kubectl in ~/.local/bin is on your PATH but not necessarily on the PATH Claude Code hands the server.

Two defences:

Audit logging

Every command the server executes is logged to stderr before it runs, with arguments quoted. Capture that stream if you want a trail of everything the agent did:

--env MICROK8S_MCP_LOG_LEVEL=INFO

Startup also logs the effective policy — mode, kubeconfig, backend and namespace allowlist — so the first lines of any session tell you what this process believes it is allowed to do.

Reporting a vulnerability: please open an issue on GitHub. This is a small hobby project, not a vendor product — there is no SLA, and you should read the code before trusting it with anything that matters.