TL;DR

Namespaces group namespaced resources; RBAC decides who can do which verbs on which resources. Use Role/RoleBinding for namespace-scoped access, ClusterRole/ClusterRoleBinding for cluster-wide access, and kubectl auth can-i to debug permission problems safely.

Mental Model

RBAC is a mapping between a subject, permissions, and a binding. The subject is a user, group, or service account. The permissions live in a Role or ClusterRole. The binding attaches those permissions to the subject.

Subject User / Group ServiceAccount Binding RoleBinding ClusterRoleBinding Permissions Role / ClusterRole verbs + resources Authorization Question Can this subject perform this verb on this resource in this namespace or cluster scope? Example: can serviceaccount/app/deployer patch deployments in namespace app?

RBAC connects subjects to permissions through bindings.

Namespaces

Namespaces isolate names for namespaced resources such as Pods, Deployments, Services, ConfigMaps, Secrets, Roles, and RoleBindings. They do not isolate nodes, persistent volumes, storage classes, namespaces themselves, or cluster-wide RBAC objects.

bash namespace-checks.sh
# List namespaces and age.
kubectl get namespaces

# Show labels, annotations, finalizers, and lifecycle status for one namespace.
kubectl describe namespace <namespace>

# Show common namespaced resources in one namespace.
kubectl get deploy,sts,ds,job,cronjob,pod,svc,ingress,cm,secret,pvc -n <namespace>

# Set a default namespace for the current context.
# Be careful: this changes where kubectl commands run when -n is omitted.
kubectl config set-context --current --namespace=<namespace>

# Clear the default namespace for the current context.
kubectl config set-context --current --namespace=default

RBAC Objects

Object Scope Use It For
Role Namespace Permissions inside one namespace.
RoleBinding Namespace Attach a Role or ClusterRole to a subject inside one namespace.
ClusterRole Cluster Cluster-wide permissions or reusable permission templates.
ClusterRoleBinding Cluster Attach a ClusterRole cluster-wide. Use sparingly.
ServiceAccount Namespace Identity used by Pods, controllers, CI jobs, and operators inside the cluster.

Debug Permissions With can-i

kubectl auth can-i is the safest first tool. It asks the API server authorization layer whether a subject can perform a specific action.

bash rbac-can-i.sh
# Check your current user's permission in a namespace.
kubectl auth can-i get pods -n <namespace>
kubectl auth can-i create deployments -n <namespace>
kubectl auth can-i patch deployments -n <namespace>

# Check a ServiceAccount's permission.
# Format is system:serviceaccount:<namespace>:<serviceaccount-name>.
kubectl auth can-i list pods -n <namespace> \
  --as=system:serviceaccount:<namespace>:<serviceaccount-name>

# Check cluster-scoped permissions. Do not add -n for cluster-scoped resources.
kubectl auth can-i list nodes
kubectl auth can-i get clusterroles

# Show all permissions the current identity has in one namespace.
kubectl auth can-i --list -n <namespace>

Least-Privilege Patterns

Start with the smallest permission that matches the job. Most app teams need namespace-level access, not cluster-admin. Most workloads need a custom ServiceAccount only if they call the Kubernetes API.

yaml namespace-deployer-rbac.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: app # Namespace where the application and RBAC objects live.
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: deployer # Identity used by CI/CD or an in-cluster deploy job.
  namespace: app
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployment-manager
  namespace: app # Role permissions apply only inside this namespace.
rules:
  - apiGroups: ["apps"] # Deployments and ReplicaSets are in the apps API group.
    resources: ["deployments", "replicasets"]
    verbs: ["get", "list", "watch", "create", "update", "patch"] # No delete unless the job truly needs it.
  - apiGroups: [""] # Empty string is the core API group.
    resources: ["pods", "services", "configmaps"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["get", "list", "watch"] # Useful for deployment diagnostics.
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: deployer-can-manage-deployments
  namespace: app
subjects:
  - kind: ServiceAccount
    name: deployer
    namespace: app # ServiceAccount namespace must be explicit.
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: deployment-manager # The Role being granted.

Read-Only App Viewer

yaml readonly-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-readonly
  namespace: app
rules:
  - apiGroups: ["", "apps", "batch", "networking.k8s.io"]
    resources:
      - pods
      - pods/log # Required to read Pod logs.
      - services
      - endpoints
      - configmaps
      - events
      - deployments
      - replicasets
      - statefulsets
      - daemonsets
      - jobs
      - cronjobs
      - ingresses
    verbs: ["get", "list", "watch"] # Read-only verbs.
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-readonly-binding
  namespace: app
subjects:
  - kind: Group
    name: sre-readonly@example.com # Replace with the client's identity provider group name.
    apiGroup: rbac.authorization.k8s.io
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: app-readonly

ServiceAccounts In Pods

Pods run as a ServiceAccount. If you do not specify one, they use the namespace's default ServiceAccount. Avoid granting permissions to default; create a named ServiceAccount per workload or controller that needs API access.

yaml pod-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-api-reader
  namespace: app
automountServiceAccountToken: true # Set false if this workload does not need the Kubernetes API token.
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
  namespace: app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      serviceAccountName: app-api-reader # Pod identity used for Kubernetes API calls.
      automountServiceAccountToken: true # Can be false to prevent token mounting.
      containers:
        - name: app
          image: nginx:1.27

Inspect Existing RBAC

bash inspect-rbac.sh
# Namespace-scoped RBAC.
kubectl get role,rolebinding,serviceaccount -n <namespace>
kubectl describe role <role-name> -n <namespace>
kubectl describe rolebinding <binding-name> -n <namespace>

# Cluster-scoped RBAC. Be careful with broad ClusterRoleBindings.
kubectl get clusterrole,clusterrolebinding
kubectl describe clusterrole <clusterrole-name>
kubectl describe clusterrolebinding <clusterrolebinding-name>

# Find bindings that mention a specific ServiceAccount name.
kubectl get rolebinding,clusterrolebinding --all-namespaces -o yaml | grep -B5 -A10 '<serviceaccount-name>'

Troubleshooting Forbidden Errors

A Forbidden error means authentication succeeded but authorization denied the action. Copy the exact verb, resource, API group, namespace, and subject from the error.

bash forbidden-debug.sh
# Example Forbidden error:
# User "system:serviceaccount:app:deployer" cannot patch resource "deployments" in API group "apps" in namespace "app"

# Reproduce the authorization question exactly.
kubectl auth can-i patch deployments.apps -n app \
  --as=system:serviceaccount:app:deployer

# If denied, inspect the subject's bindings.
kubectl get rolebinding -n app -o yaml | grep -B8 -A20 'deployer'
kubectl get clusterrolebinding -o yaml | grep -B8 -A20 'deployer'

# Inspect the referenced Role or ClusterRole and confirm it has the missing verb/resource.
kubectl describe role <role-name> -n app
kubectl describe clusterrole <clusterrole-name>

Common Gotchas

  • !RoleBinding to ClusterRole is valid: it grants that ClusterRole's permissions only inside the RoleBinding namespace.
  • !ClusterRoleBinding is broad: it grants access across the cluster. Avoid it for app team namespace access.
  • !Secrets need explicit access: many read-only roles intentionally exclude secrets because Secret read is sensitive.
  • !Subresources matter: pods, pods/log, pods/exec, and deployments/scale are different permissions.
  • !Namespace deletion stuck: check finalizers and unavailable API services before force-removing finalizers.