Role-Based Access Control (RBAC) is the authorization layer of the Kubernetes API. After a request is authenticated, RBAC decides whether that user or service account may perform a given verb (such as get or delete) on a given resource (such as pods or secrets). In this tutorial you will create a namespace-scoped role for a CI/CD pipeline, bind it to a ServiceAccount, generate a kubeconfig that only has those permissions, and verify every rule with kubectl auth can-i. You will also grant read-only cluster-wide access and learn which permissions are riskier than they look.

Prerequisites

To follow this tutorial you need:

  • A Kubernetes cluster (1.30 or newer) with RBAC enabled. RBAC is on by default in kubeadm, k3s and every major managed Kubernetes service.
  • kubectl on your workstation with a cluster-admin kubeconfig, for example on an Ubuntu 24.04 machine or a CubePath VPS.

Confirm that RBAC is active and that you are an administrator:

kubectl api-versions | grep rbac
kubectl auth can-i '*' '*' --all-namespaces
rbac.authorization.k8s.io/v1
yes

How RBAC fits together

RBAC uses four object types:

ObjectScopePurpose
RoleOne namespaceA list of rules: API groups, resources and verbs
ClusterRoleClusterSame, but can cover cluster-scoped resources (nodes, namespaces) or be reused in many namespaces
RoleBindingOne namespaceGrants a Role or ClusterRole to subjects inside that namespace
ClusterRoleBindingClusterGrants a ClusterRole to subjects in every namespace

A few rules that shape every design:

  • Permissions are purely additive. There are no "deny" rules; anything not explicitly allowed is denied.
  • Subjects are User, Group or ServiceAccount. Kubernetes has no user objects: users and groups come from your authentication method (client certificate CN and O fields, or OIDC claims). ServiceAccounts are real API objects that live in a namespace.
  • A RoleBinding can reference a ClusterRole. The permissions then apply only inside the binding's namespace, which lets you define a role once and reuse it everywhere.

Each rule names an API group. The core group (pods, services, secrets, configmaps) is the empty string ""; Deployments and StatefulSets live in apps; Jobs and CronJobs in batch. To find the group and exact resource name for anything, run:

kubectl api-resources -o wide | grep -E '^NAME|deployments|pods '
NAME          SHORTNAMES   APIVERSION   NAMESPACED   KIND         VERBS
deployments   deploy       apps/v1      true         Deployment   create,delete,deletecollection,get,list,patch,update,watch
pods          po           v1           true         Pod          create,delete,deletecollection,get,list,patch,update,watch

Subresources are written as resource/subresource, for example pods/log to read logs, pods/exec to run commands in a container and deployments/scale to change replicas.

Step 1 - Creating a namespace and a ServiceAccount

You will give a CI/CD pipeline the ability to update Deployments in an apps namespace, and nothing else. Create the namespace and the ServiceAccount the pipeline will use:

kubectl create namespace apps
kubectl create serviceaccount ci-deployer -n apps
namespace/apps created
serviceaccount/ci-deployer created

A new ServiceAccount has no permissions at all. Prove it by impersonating it. The username of a ServiceAccount is always system:serviceaccount:<namespace>:<name>:

kubectl auth can-i list deployments -n apps \
  --as=system:serviceaccount:apps:ci-deployer
no

Step 2 - Writing a least-privilege Role

A pipeline that rolls out new images needs to read and patch Deployments, watch the rollout, and read pods and their logs to diagnose failures. It does not need to create or delete anything, and it must not read Secrets.

Create the manifest:

nano ci-deployer-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployment-updater
  namespace: apps
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "patch", "update"]
  - apiGroups: ["apps"]
    resources: ["replicasets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-deployer-deployment-updater
  namespace: apps
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: deployment-updater
subjects:
  - kind: ServiceAccount
    name: ci-deployer
    namespace: apps

kubectl rollout status reads ReplicaSets, which is why they are included. The roleRef of a binding cannot be changed after creation; to point a binding at a different role, delete and recreate it.

Apply the manifest:

kubectl apply -f ci-deployer-rbac.yaml
role.rbac.authorization.k8s.io/deployment-updater created
rolebinding.rbac.authorization.k8s.io/ci-deployer-deployment-updater created

Step 3 - Verifying permissions with kubectl auth can-i

Test what the ServiceAccount can and cannot do. Store the username in a variable to keep the commands short:

SA=system:serviceaccount:apps:ci-deployer
kubectl auth can-i patch deployments -n apps --as="$SA"
kubectl auth can-i get pods --subresource=log -n apps --as="$SA"
kubectl auth can-i delete deployments -n apps --as="$SA"
kubectl auth can-i get secrets -n apps --as="$SA"
kubectl auth can-i patch deployments -n default --as="$SA"
yes
yes
no
no
no

The last check confirms that a RoleBinding never grants anything outside its own namespace. List everything the account may do in apps:

kubectl auth can-i --list -n apps --as="$SA"
Resources                                       Non-Resource URLs   Resource Names   Verbs
selfsubjectreviews.authentication.k8s.io        []                  []               [create]
selfsubjectaccessreviews.authorization.k8s.io   []                  []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                  []               [create]
pods/log                                        []                  []               [get]
pods                                            []                  []               [get list watch]
deployments.apps                                []                  []               [get list watch patch update]
replicasets.apps                                []                  []               [get list watch]
...

The extra self* entries and non-resource URLs come from the built-in system:basic-user and system:discovery roles that every authenticated identity has.

Step 4 - Creating a kubeconfig for the ServiceAccount

Since Kubernetes 1.24, ServiceAccounts no longer get a long-lived token Secret automatically. Request a short-lived, signed token with the TokenRequest API instead:

TOKEN=$(kubectl create token ci-deployer -n apps --duration=1h)

Build a separate kubeconfig file that uses the current cluster's address and CA but authenticates with this token, so your admin config stays untouched:

CLUSTER=$(kubectl config view --minify -o jsonpath='{.clusters[0].name}')
SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
kubectl config view --minify --raw \
  -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d > ca.crt

export KUBECONFIG="$PWD/ci-deployer.kubeconfig"
kubectl config set-cluster "$CLUSTER" --server="$SERVER" \
  --certificate-authority=ca.crt --embed-certs=true
kubectl config set-credentials ci-deployer --token="$TOKEN"
kubectl config set-context ci-deployer --cluster="$CLUSTER" \
  --user=ci-deployer --namespace=apps
kubectl config use-context ci-deployer

Check which identity the new kubeconfig uses:

kubectl auth whoami
ATTRIBUTE                                           VALUE
Username                                            system:serviceaccount:apps:ci-deployer
UID                                                 ...
Groups                                              [system:serviceaccounts system:serviceaccounts:apps system:authenticated]
Extra: authentication.kubernetes.io/credential-id   [JTI=...]

Now try allowed and forbidden actions for real:

kubectl get deployments
kubectl get secrets
No resources found in apps namespace.
Error from server (Forbidden): secrets is forbidden: User "system:serviceaccount:apps:ci-deployer" cannot list resource "secrets" in API group "" in the namespace "apps"

Return to your admin config and remove the CA copy:

unset KUBECONFIG
rm ca.crt

For a CI system, store the contents of ci-deployer.kubeconfig as a protected secret variable in your pipeline. Because the token expires, the pipeline needs a fresh one periodically; the warning below shows the long-lived alternative.

Step 5 - Reusing ClusterRoles and granting cluster-wide read access

Kubernetes ships user-facing ClusterRoles that are kept up to date as new resource types are added:

  • view: read most objects in a namespace, but not Secrets.
  • edit: read and write most objects, including Secrets, but not Roles or RoleBindings.
  • admin: full control of a namespace, including its RBAC.
  • cluster-admin: everything, everywhere.

Binding one of them with a RoleBinding limits it to that namespace. For example, give a developer group read-only access to apps:

kubectl create rolebinding developers-view \
  --clusterrole=view --group=developers -n apps

The group name developers must match what your authenticator sends, such as the O field of a client certificate or a groups claim from your OIDC provider. Verify it:

kubectl auth can-i list pods -n apps --as=jane --as-group=developers
kubectl auth can-i get secrets -n apps --as=jane --as-group=developers
yes
no

Some resources, such as nodes and namespaces, are cluster-scoped and can only be granted with a ClusterRole and a ClusterRoleBinding. Give a monitoring ServiceAccount read access to nodes across the cluster:

kubectl create namespace monitoring
kubectl create serviceaccount node-reader -n monitoring
kubectl create clusterrole node-reader --verb=get,list,watch --resource=nodes
kubectl create clusterrolebinding node-reader \
  --clusterrole=node-reader --serviceaccount=monitoring:node-reader
kubectl auth can-i list nodes --as=system:serviceaccount:monitoring:node-reader
yes

Step 6 - Avoiding privilege escalation paths

Some permissions quietly grant much more than their name suggests. Review any role that includes these before binding it:

  • list or watch on secrets returns the full content of every Secret, not only their names.
  • create on pods (or on Deployments, Jobs and other workload objects) lets the user start a pod that mounts any ServiceAccount or Secret in that namespace, and therefore act with its permissions.
  • pods/exec and pods/attach give a shell inside running containers, with access to their files and tokens.
  • escalate, bind and impersonate let a subject grant itself or assume permissions it does not have.
  • Wildcards ("*") in verbs or resources also match resources that are added to the cluster in the future.

To audit who holds a powerful role, list its bindings:

kubectl get clusterrolebindings -o wide | grep -w cluster-admin
kubectl get rolebindings -A -o wide

Keep RBAC manifests in version control and review changes to them like code.

Troubleshooting

  • Forbidden ... cannot list resource "deployments" in API group "": the rule uses the wrong API group. Deployments are in apps, not the core group. Check with kubectl api-resources.
  • The rule looks right but access is denied: check the resource name. Logs are pods/log (singular), and the Role and RoleBinding must be in the same namespace as the object being accessed.
  • kubectl create token fails with unknown command: your kubectl is older than 1.24. Upgrade it to a version within one minor release of your cluster.
  • Token stops working: tokens from kubectl create token expire. Request a new one, and note that the API server can cap --duration to a lower maximum.

Conclusion

You built a least-privilege Role for a CI pipeline, bound it to a ServiceAccount, issued a short-lived token and kubeconfig for it, and verified each permission with kubectl auth can-i before relying on it. You also reused the built-in view ClusterRole per namespace and granted narrow cluster-wide read access. Next, consider enabling API server audit logging to record who changed what, enforcing Pod Security Admission on application namespaces, and combining RBAC with network policies to limit what compromised workloads can reach.