When I joined a new team's cluster for the first time, I ran a quick audit and found eight service accounts with cluster-admin access. Eight. If any one of those services was compromised, an attacker could delete deployments, read secrets across every namespace, and spin up workloads anywhere in the cluster. The blast radius was the entire cluster.
This is more common than people realise. Most clusters start life with broad permissions because it's faster. You give things cluster-admin because you don't want to debug permission errors at 11pm, then move on. The problem is that "move on" rarely comes back to fix it.
We went from eight cluster-admin bindings to one over three weeks of incremental changes. Zero service disruptions. Here's how.
What You Need
A running Kubernetes cluster with kubectl configured and admin access. Install jq for the audit commands.
Install Krew (kubectl plugin manager — needed for who-can)
kubectl cluster-info
kubectl auth can-i '*' '*' --all-namespaces
# Should return: yesStep 1: Audit What You Actually Have
Before changing anything, understand the full picture. This command shows every ClusterRoleBinding that grants cluster-admin:
kubectl get clusterrolebindings -o json | jq '
.items[] |
select(.roleRef.name == "cluster-admin") |
{
binding: .metadata.name,
subjects: .subjects
}'Also check what roles exist across all namespaces:
# See all ClusterRoles
kubectl get clusterroles | grep -v system:
# See all RoleBindings across namespaces
kubectl get rolebindings -A
# See all ClusterRoleBindings
kubectl get clusterrolebindingsInstall kubectl-who-can to query who can do specific things:
kubectl krew install who-can
# Who can delete pods in production?
kubectl who-can delete pods -n production
# Who can read secrets?
kubectl who-can get secrets -n production
# Who can exec into containers?
kubectl who-can create pods/exec -n productionStep 2: Understand the Three RBAC Objects
Before making changes, understand what you're working with:
# Role — permissions in a specific namespace
# ClusterRole — permissions across all namespaces
# RoleBinding — attaches a Role or ClusterRole to a subject (in a namespace)
# ClusterRoleBinding — attaches a ClusterRole to a subject cluster-wideThe key decision: if a service only works in one namespace, use a Role and RoleBinding. Only use ClusterRole when the service genuinely needs cluster-wide access.
Step 3: Replace cluster-admin with Minimal Roles
For each service account that has cluster-admin, figure out what it actually needs. A CI/CD pipeline that only deploys to the production namespace needs:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: production
name: app-deployer
rules:
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods", "pods/log", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: cicd-deployer
namespace: production
subjects:
- kind: ServiceAccount
name: cicd-bot
namespace: production
roleRef:
kind: Role
name: app-deployer
apiGroup: rbac.authorization.k8s.io# Apply the new role
kubectl apply -f cicd-role.yaml
# Test the new permissions (do this before removing cluster-admin)
kubectl auth can-i update deployments --as=system:serviceaccount:production:cicd-bot -n production
# yes
kubectl auth can-i delete namespaces --as=system:serviceaccount:production:cicd-bot
# noOnce you've confirmed the restricted role works, remove the old cluster-admin binding:
kubectl delete clusterrolebinding ci-bot-cluster-adminStep 4: Stop Pods from Auto-Mounting Tokens They Don't Need
By default, every pod gets the default service account token mounted at /var/run/secrets/kubernetes.io/serviceaccount/. Most application pods never use this. Disable it:
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: production
automountServiceAccountToken: falseFor pods that genuinely need Kubernetes API access, create a dedicated service account with explicit RBAC — don't re-enable the default one.
Step 5: Verify After Changes
# Confirm what changed
kubectl get clusterrolebindings | grep cluster-admin
# Verify the application still works after permission changes
kubectl get pods -n production
kubectl describe deployment my-app -n production
# Test that restricted permissions work
kubectl auth can-i --list --as=system:serviceaccount:production:cicd-bot -n productionWork through service accounts one at a time. Change one, verify nothing broke, then move to the next. After three weeks of incremental changes, we went from eight cluster-admin bindings to one. Zero service disruptions.
Once RBAC is locked down, the next layer is restricting pod-to-pod traffic. That's covered in Kubernetes Network Policies.

Comments
All comments are reviewed before appearing.
Leave a Comment