Skip to content
docktionary

Kubernetes

RBAC

Every request to the API server -- kubectl, a controller, a Pod calling back into the cluster -- gets authenticated as someone and then checked against RBAC to see what that someone is actually allowed to do. The default most clusters hand out (cluster-admin, to everyone) skips that second check entirely.

RBAC is specifically about calls to the API server -- it has nothing to say about whether one Pod can open a network connection straight to another. That's a separate, equally wide-open-by-default problem; see NetworkPolicy.

Role and ClusterRole: the permissions themselves

A Role is a set of permissions scoped to one namespace. A ClusterRole is the same shape, but either cluster-wide (for cluster-scoped resources like Nodes, or for granting the same permissions across every namespace at once) or reusable -- defined once, bound into as many individual namespaces as you want.

role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: staging
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]

Neither a Role nor a ClusterRole does anything by itself -- it's just a named list of allowed verbs on a resource. Nothing is granted to anyone until it's bound.

RoleBinding and ClusterRoleBinding: granting it to someone

A RoleBinding grants a Role (or a ClusterRole, used this way to reuse one definition across namespaces) to a subject -- a User, Group, or ServiceAccount -- within one namespace. A ClusterRoleBinding grants a ClusterRole across the whole cluster, with no namespace boundary.

rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: staging
subjects:
  - kind: ServiceAccount
    name: ci-deployer
    namespace: staging
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

The same ClusterRole bound with a RoleBinding (namespace-scoped) versus a ClusterRoleBinding (cluster-wide) is the single most common RBAC mistake worth knowing about: it's the difference between "can read pods in staging" and "can read pods everywhere, including kube-system."

ServiceAccount: identity for a Pod

A User authenticates from outside the cluster (your own kubeconfig, SSO, a cloud IAM identity). A ServiceAccount is the equivalent for something inside the cluster -- a Pod calling back into the API server to list other Pods, read a Secret, or watch for changes. Every Pod runs as some ServiceAccount whether you set one or not; leaving it unset means the namespace's default one, which usually has no permissions bound to it but still gets a token mounted into the Pod for no reason.

serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-deployer
  namespace: staging

Reference it explicitly, and skip the token mount entirely if this Pod never actually calls the API server:

yaml
spec:
  serviceAccountName: ci-deployer
  automountServiceAccountToken: false   # only if this Pod never calls the API server

Putting it together

The Role, the RoleBinding, and the ServiceAccount above are a complete, working example: a ServiceAccount named ci-deployer that can read Pods and their logs in the staging namespace, and nothing else -- not Secrets, not Deployments, not any other namespace. A Pod that mounts this ServiceAccount gets exactly that, no more:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: log-shipper
  namespace: staging
spec:
  serviceAccountName: ci-deployer
  containers:
    - name: shipper
      image: docktionary/log-shipper:1.0

Checking what a Role actually grants

Reading a pile of rules and figuring out whether they add up to what you intended is slow and error-prone. Ask the API server directly instead:

terminal
# Can I do this?
kubectl auth can-i get pods --namespace staging

# Can this ServiceAccount do this? (--as uses impersonation, which
# requires its own RBAC permission to use at all)
kubectl auth can-i list secrets \
  --as=system:serviceaccount:staging:ci-deployer \
  --namespace staging

# Everything a ServiceAccount is bound to, read back out
kubectl auth can-i --list \
  --as=system:serviceaccount:staging:ci-deployer \
  --namespace staging

# Find every RoleBinding/ClusterRoleBinding mentioning a name
kubectl get rolebindings,clusterrolebindings --all-namespaces \
  -o json | jq '.items[] | select(.subjects[]?.name=="ci-deployer")'

Where this goes wrong in practice

  • Wildcards. resources: ["*"], verbs: ["*"], or both together on a Role someone meant to scope down -- functionally cluster-admin, written to look narrower than it is.
  • ClusterRoleBinding by default. Reaching for a ClusterRoleBinding because it's the first result that comes to mind, when every actual use case was namespace-scoped and a RoleBinding (referencing a shared ClusterRole, if the rules need to be reused) would have contained the blast radius.
  • Binding to cluster-admin directly. The built-in cluster-admin ClusterRole exists for actual cluster administrators -- binding an application's ServiceAccount to it to unblock a permissions error is solving "I don't know which permission I'm missing" with "grant all of them," instead of using kubectl auth can-i above to actually find the one rule that's missing.
  • Unused token mounts. Every Pod gets a ServiceAccount token mounted by default, readable by anything that can exec into it, even when the Pod never calls the API server at all. automountServiceAccountToken: false removes a credential that was never needed in the first place.