Ch. 14 · Kubernetes

Kubernetes RBAC and Least-Privilege Access

Build a least-privilege Role for a CI deployer, verify it with kubectl auth can-i, and spot grants like pods/exec that quietly escalate.

~6 min readintermediateupdated Oct 7, 2026

RBAC questions in interviews rarely stop at “what’s the difference between a Role and a ClusterRole”. The follow-up is usually a scenario: a CI pipeline needs to deploy, a monitoring agent needs to read metrics, a developer needs to debug. The good answer grants exactly that and can explain which innocent-looking permissions would give away much more. This guide builds a deployer identity step by step and then looks at the grants that escalate.

The manifests target Kubernetes 1.30+ and parse as valid YAML. The can-i answers and error text show what kubectl returns for these rules; they weren’t captured from a real cluster.

How a request gets authorised

Every API request carries an identity (a user, a group or a ServiceAccount) and asks to do a verb (get, list, watch, create, update, patch, delete, deletecollection) on a resource (deployments, pods, secrets) in an API group (apps, "" for the core group, batch), optionally in a namespace and optionally on a named object.

RBAC answers with four kinds of object:

  • Role: a list of rules valid in one namespace.
  • ClusterRole: the same, but not tied to a namespace. Can also cover cluster-scoped resources like nodes, and non-resource URLs like /healthz.
  • RoleBinding: grants a Role or ClusterRole to subjects, inside the binding’s namespace.
  • ClusterRoleBinding: grants a ClusterRole to subjects across every namespace.

There is no deny. If any binding anywhere allows the request, it’s allowed. So you can’t take a permission away from someone by adding a rule; you have to find and remove the binding that grants it. This is why auditing RBAC means listing bindings, not reading one Role.

The combination that confuses people is ClusterRole plus RoleBinding. It grants the ClusterRole’s rules but only within the RoleBinding’s namespace. It lets you define view, edit or a custom deployer once and bind it per namespace, rather than copying a Role into each namespace.

A deployer for one namespace

The CI system deploys the checkout app into shop with kubectl apply and waits for kubectl rollout status. Start with what it actually does, then write rules for exactly that:

  • apply a Deployment, Service and ConfigMap: get, create, patch (server-side apply uses patch)
  • wait on the rollout: get, list, watch on deployments and replicasets, and list/watch on pods for status
  • nothing with Secrets, nothing outside shop
apiVersion: v1
kind: ServiceAccount
metadata:
  name: ci-deployer
  namespace: shop
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployer
  namespace: shop
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "patch"]
  - apiGroups: ["apps"]
    resources: ["replicasets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["services", "configmaps"]
    verbs: ["get", "create", "patch"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: ci-deployer
  namespace: shop
subjects:
  - kind: ServiceAccount
    name: ci-deployer
    namespace: shop
roleRef:
  kind: Role
  name: deployer
  apiGroup: rbac.authorization.k8s.io
yaml

A few deliberate choices:

  • No delete. Removing a resource from the repo shouldn’t silently delete it in production. If the pipeline needs pruning, add delete with intent.
  • No update. Apply uses patch; update would let the pipeline replace whole objects.
  • No wildcards. resources: ["*"] would include secrets, roles and anything a CRD adds later. A rule that seems tidy today grows silently.
  • automountServiceAccountToken: false. This identity is used from outside the cluster via a short-lived token (kubectl create token ci-deployer -n shop --duration=15m, or the CI platform’s workload identity). No Pod needs it mounted.

Then check it, using impersonation. You need permission to impersonate, which cluster admins have:

$ SA=system:serviceaccount:shop:ci-deployer
$ kubectl auth can-i patch deployments -n shop --as=$SA
yes
$ kubectl auth can-i delete deployments -n shop --as=$SA
no
$ kubectl auth can-i get secrets -n shop --as=$SA
no
$ kubectl auth can-i patch deployments -n payments --as=$SA
no
$ kubectl auth can-i --list -n shop --as=$SA
Resources                Non-Resource URLs   Resource Names   Verbs
deployments.apps         []                  []               [get list watch create patch]
replicasets.apps         []                  []               [get list watch]
configmaps               []                  []               [get create patch]
services                 []                  []               [get create patch]
pods                     []                  []               [get list watch]
...
Terminal

The ... stands for permissions every authenticated identity gets from built-in ClusterRoles like system:basic-user (for example, creating selfsubjectaccessreviews, which is what can-i itself uses).

When the pipeline fails with a missing permission, the error names exactly what’s missing:

Error from server (Forbidden): deployments.apps "checkout" is forbidden:
User "system:serviceaccount:shop:ci-deployer" cannot delete resource "deployments"
in API group "apps" in the namespace "shop"
Text

Read it as verb, resource, group, namespace, and add exactly that, or decide the pipeline shouldn’t be doing it.

Grants that are bigger than they look

This is where least privilege usually fails, and it’s what a security-minded interviewer will push on.

create pods (or deployments, jobs, cronjobs). A Pod can mount any Secret in its namespace and can run as any ServiceAccount in its namespace. So anyone who can create Pods in shop can read every Secret in shop and act with every ServiceAccount’s permissions there. The Pod is the escalation path, not the Secret rule. That’s why the deployer above is still a sensitive identity, and why Pod Security Admission matters: without it, the same person can create a privileged Pod with a hostPath mount and reach the node.

list secrets, or watch. get on a named Secret with resourceNames is narrow. list returns every Secret’s contents in the namespace in one call; there’s no metadata-only list in RBAC.

pods/exec and pods/attach. These are subresources, so get pods doesn’t include them, but granting them is a shell inside the container with its environment variables (often credentials) and its ServiceAccount token. Debug access for developers usually means pods/log (read logs) and maybe pods/portforward, and exec only in non-production namespaces.

escalate, bind and impersonate. RBAC normally stops you creating a Role with permissions you don’t hold. The escalate verb on roles lifts that check; bind lets you bind roles you don’t hold; impersonate lets you act as any user, group or ServiceAccount. Treat all three as admin.

Editing RoleBindings at all. If an identity can create or patch RoleBindings in a namespace, it can bind itself the namespace’s admin ClusterRole (subject to the escalation check, which bind bypasses).

resourceNames narrows a rule to specific objects, which is useful for “read the feature-flags ConfigMap and nothing else”. It can’t restrict create or list, because at the time of those requests the object name isn’t known (create) or there are many (list).

Interview practice

“A developer asks for the built-in edit ClusterRole in the production namespace so they can read logs while debugging incidents. What would you grant instead, and why?”

Answer

edit is much more than reading logs. It allows creating and modifying most namespaced workload resources, including Pods and Deployments, which means it can read every Secret in production by mounting it, and it includes pods/exec, which is a shell in production containers. It doesn’t include Roles or RoleBindings, but it already covers more than incident debugging needs.

I’d create a small ClusterRole, say log-reader, with get, list and watch on pods, get on pods/log, and read access to events and deployments/replicasets so they can see rollout state, then bind it with a RoleBinding in production to the developer’s group rather than the individual. I’d verify with kubectl auth can-i get pods --subresource=log -n production --as=... --as-group=... and confirm create pods/exec returns no. If incidents occasionally need exec, that should be a time-limited, audited elevation (a break-glass group, or a just-in-time access tool), not a standing grant.

Further reading

More in Kubernetes

read ✓Kubernetes · easy

Kubernetes ConfigMaps Versus Secrets

ConfigMap or Secret? Why base64 isn't encryption, what actually protects a Secret, and how env vars and mounted files behave on update.

~6 min readread →
read ✓Kubernetes · mid

Kubernetes Namespaces and Isolation Limits

What a namespace scopes (names, RBAC, quotas, policies) and what it leaves open: network traffic, nodes, cluster-scoped objects and DNS.

~6 min readread →
esc