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.
kubectlon 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:
| Object | Scope | Purpose |
|---|---|---|
Role | One namespace | A list of rules: API groups, resources and verbs |
ClusterRole | Cluster | Same, but can cover cluster-scoped resources (nodes, namespaces) or be reused in many namespaces |
RoleBinding | One namespace | Grants a Role or ClusterRole to subjects inside that namespace |
ClusterRoleBinding | Cluster | Grants 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,GrouporServiceAccount. 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
RoleBindingcan reference aClusterRole. 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
TipFor quick experiments you can create the same objects imperatively with
kubectl create role deployment-updater --verb=get,list,watch,patch,update --resource=deployments.apps -n appsandkubectl create rolebinding ... --role=deployment-updater --serviceaccount=apps:ci-deployer. Keep the YAML version in Git for anything real.
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.
WarningIf you really need a token that does not expire, create a Secret of type
kubernetes.io/service-account-tokenwith the annotationkubernetes.io/service-account.name: ci-deployer. Kubernetes fills in a permanent token. Treat it like a password, and delete the Secret to revoke it.
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:
listorwatchonsecretsreturns the full content of every Secret, not only their names.createonpods(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/execandpods/attachgive a shell inside running containers, with access to their files and tokens.escalate,bindandimpersonatelet a subject grant itself or assume permissions it does not have.- Wildcards (
"*") inverbsorresourcesalso 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 inapps, not the core group. Check withkubectl 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 tokenfails withunknown command: yourkubectlis older than 1.24. Upgrade it to a version within one minor release of your cluster.- Token stops working: tokens from
kubectl create tokenexpire. Request a new one, and note that the API server can cap--durationto 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.
