Anyone who can reach the Kubernetes API with valid credentials can do whatever RBAC (role-based access control) allows. Defaults matter: in a cluster where everything runs as cluster-admin, one leaked token is a full compromise. RBAC lets you grant exactly the verbs on exactly the resources each person or workload needs.
Versions in use on this date. Kubernetes 1.22, where bound service account tokens are stable: pods now receive a time-limited, audience-bound token that is refreshed by the kubelet, instead of a never-expiring Secret-based token. Only rbac.authorization.k8s.io/v1 exists; the v1beta1 version was removed in 1.22.
The model
flowchart TB
subgraph WHO[Subjects - who]
U[User from OIDC or certificate]
G[Group]
SA[ServiceAccount in a namespace<br/>used by pods]
end
subgraph WHAT[Permissions - what]
R[Role<br/>namespaced rules]
CR[ClusterRole<br/>cluster-wide or reusable rules]
end
RB[RoleBinding<br/>grants within one namespace] --> R
RB --> CR
CRB[ClusterRoleBinding<br/>grants in all namespaces] --> CR
U --> RB
G --> RB
SA --> RB
SA --> CRB
REQ[API request] --> AUTHN[Authentication] --> AUTHZ{RBAC allows verb on resource?}
AUTHZ -->|yes| ADM[Admission controllers] --> STORE[etcd]
AUTHZ -->|no| DENY[403 Forbidden]
RBAC is additive: there are no deny rules. A subject can do the union of everything granted to it.
A namespaced example: a read-only role for developers
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: dev-readonly, namespace: shop }
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "services", "configmaps", "events"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: dev-readonly-binding, namespace: shop }
subjects:
- kind: Group
name: shop-developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: dev-readonly
apiGroup: rbac.authorization.k8s.io
Notice: no access to secrets. A role that can list or get Secrets can read every credential in the namespace, so treat that permission as highly sensitive.
Verify before and after:
kubectl auth can-i list pods -n shop --as=jane --as-group=shop-developers # yes
kubectl auth can-i get secrets -n shop --as=jane --as-group=shop-developers # no
kubectl auth can-i --list -n shop --as=system:serviceaccount:shop:web
Service accounts for workloads
Every pod runs as a ServiceAccount (the namespace’s default one unless set). Create one per application and grant it only what the application calls from the API; most apps need nothing at all.
apiVersion: v1
kind: ServiceAccount
metadata: { name: web, namespace: shop }
automountServiceAccountToken: false # the app never talks to the Kubernetes API
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: web, namespace: shop }
spec:
template:
spec:
serviceAccountName: web
Practices:
- Disable token mounting (
automountServiceAccountToken: false) for pods that do not call the API. If a pod is compromised, an attacker with a mounted token can try the API. - Do not use the
defaultservice account for anything with permissions. - Controllers (operators, CI agents) get a narrow
RoleorClusterRolebuilt for them; read the vendor’s documented rules rather than bindingcluster-admin. - Cloud IAM integration: on EKS use IAM Roles for Service Accounts to give a service account AWS permissions without keys; on GKE, Workload Identity.
# inspect a service account and the token a pod actually receives
kubectl -n shop get sa web -o yaml
kubectl -n shop exec deploy/web -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | cut -c1-20
In 1.22 the mounted token is a projected volume with an expiry (default one hour, refreshed automatically) and is invalidated when the pod is deleted. Long-lived token Secrets are still created for service accounts in this release, so audit and delete those you do not need.
Humans
Authentication is external to RBAC: certificates, OIDC (recommended), or the cloud provider’s IAM (on EKS the aws-auth ConfigMap maps IAM roles to Kubernetes groups). Bind groups, not individual users, so access follows your identity provider.
| Role of person | Typical grant |
|---|---|
| Developer | edit or custom read/exec in their own namespaces |
| On-call | view cluster-wide plus edit in production namespaces |
| Platform admin | cluster-admin, held by few, with MFA, preferably time-bound |
| CI/CD | A service account limited to deploy verbs in specific namespaces |
The built-in ClusterRoles view, edit, admin and cluster-admin are good starting points; bind them with a RoleBinding to scope them to a namespace.
Dangerous permissions
Watch for: * verbs or resources, secrets read, pods/exec, create on pods (can mount host paths unless policy forbids), escalate, bind and impersonate verbs, nodes/proxy, and write access to clusterrolebindings. Any of these can become cluster-admin indirectly.
Audit routine
kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name + " -> " + ([.subjects[]? | .kind + ":" + .name] | join(","))'
kubectl get rolebindings,clusterrolebindings -A -o wide | grep -v "system:"
kubectl get sa -A | grep -v default
kubectl auth can-i '*' '*' --as=system:serviceaccount:kube-system:default
Tools such as kubectl-who-can, rbac-lookup, rakkess and kubeaudit summarise access. Review quarterly, remove stale bindings, and keep all RBAC in Git. Pair RBAC with admission policy (Pod Security, OPA Gatekeeper or Kyverno) and network policies, because RBAC controls API access, not what a running container can do.