Documentation

Least-privilege RBAC

The shipped manifests give janus-mcp its own ServiceAccount with read verbs on a handful of kinds, in named namespaces only. Note what is absent: Secrets, nowhere, ever.


Why a second layer

Server-side scope already blocks out-of-policy calls before they reach the API server: allow and deny namespace lists, cluster-scope opt-in, a pinned context. RBAC is the independent layer underneath, enforced by Kubernetes rather than by this process.

If a bug ever let a call slip past ScopeGuard, the credential still cannot read a Secret, patch an unlisted kind, or touch a namespace outside its Role bindings. Two mechanisms, one outcome, no shared failure mode.


What the manifests grant

A ServiceAccount in its own janus-mcp namespace, one narrow ClusterRole, and a per-namespace Role bound to it.

ScopeResourcesVerbs
Cluster-widenamespacesget list
Per namespace, corepods pods/log events services configmaps persistentvolumeclaimsget list
Per namespace, appsdeployments replicasets statefulsets daemonsetsget list
Per namespace, batchjobs cronjobsget list
Per namespace, networkingingressesget list
Per namespace, autoscalinghorizontalpodautoscalersget list
Per namespace, events.k8s.ioeventsget list

Repeat the Role and RoleBinding pair for every namespace in scope.allowed_namespaces. A namespace with no Role is invisible twice over: server-side scope refuses it, and the credential could not read it anyway.


What is absent, deliberately

The absence is the control. These are not filtered at runtime; they were never granted.

  • secrets in any namespace, in any verb. Nowhere, ever.
  • serviceaccounts, certificatesigningrequests and tokenreviews, the adjacent credential-bearing kinds.
  • create, delete and deletecollection on everything.
  • pods/exec, pods/attach and pods/portforward, the shell-shaped subresources.
  • Node and other cluster-scoped reads beyond namespaces; those stay behind allow_cluster_scoped as well.

If the credential you point janus-mcp at can read Secrets, the startup access-review probe says so loudly, and refuses outright under --strict.


Apply it

Edit the namespaces in the manifest first: it ships with prod as the example.

kubectl create namespace janus-mcp
kubectl apply -f rbac/janus-mcp-rbac.yaml

The file contains, in order: the ServiceAccount, the namespace-read ClusterRole and its ClusterRoleBinding, then the per-namespace read Role and RoleBinding, then the optional write Role and RoleBinding.


The optional write role

Only apply this if write tools are enabled in the janus-mcp config. It grants patch and nothing else, on the workload kinds a rollout restart or a scale actually touches.

rules:
  - apiGroups: ["apps"]
    resources:
      - deployments
      - deployments/scale
      - statefulsets
      - statefulsets/scale
      - daemonsets
    verbs: ["patch"]

Even with this bound, every write still needs an out-of-band human approval, and the config gates which tools register at all. RBAC is the floor, not the policy.


Verify before you trust it

Check the negative cases, not just the positive ones. The Secret probe must come back no.

SA=system:serviceaccount:janus-mcp:janus-mcp
kubectl auth can-i list pods            -n prod --as $SA   # yes
kubectl auth can-i get  secrets         -n prod --as $SA   # no
kubectl auth can-i create pods/exec     -n prod --as $SA   # no
kubectl auth can-i patch deployments    -n prod --as $SA   # yes only with the write role
kubectl auth can-i list pods            -n kube-system --as $SA   # no

The threat model lists the invariants these checks stand in for.


Bind a kubeconfig context

Create a context that uses the ServiceAccount token, then pin its name in the config. A short-lived token is fine; janus-mcp reads the kubeconfig at startup.

# in ~/.config/janus-mcp/config.yaml
context: limited-sa@prod-cluster
scope:
  allowed_namespaces: ["prod"]
  denied_namespaces: ["kube-system", "kube-node-lease"]
  allow_cluster_scoped: false