Certified Kubernetes Security Specialist (CKS) #4: RBAC least privilege in depth (Cluster Hardening)

In CKA #9 RBAC, you learned, from an operator’s point of view, the combination rules of Role and ClusterRole, RoleBinding and ClusterRoleBinding, the structure of subjects and rules, and how to check permissions with auth can-i. The goal there was to design and make work who can do what. The CKS Cluster Hardening domain adds one more question on top of that: is this permission really only as much as needed?

Being able to build RBAC and being able to safely narrow RBAC are different skills. In day-to-day operations it’s common to hand out broad permissions just to get things working, and the excess permissions that accumulate become a direct path to privilege escalation for an attacker. This post applies the principle of least privilege to RBAC, covering the work of finding wide-open permissions and narrowing them to exactly the scope you need. On the exam, “narrow this overly broad Role down to least privilege” is a regular.

What the principle of least privilege is #

The principle of least privilege is the security design principle that every subject gets only the permissions strictly needed to do its job, and nothing beyond that. Applied to RBAC, it breaks down into three:

  • Narrow resources. Instead of * (all resources), spell out only the resources actually handled.
  • Narrow verbs. Instead of * (all actions), spell out only the actions actually performed. If it only reads, give only get, list, and watch.
  • Narrow scope. Instead of the whole cluster, limit to a specific namespace, and where possible to specific resource names (resourceNames).

The value of the principle becomes clear if you imagine an attacker has stolen a single ServiceAccount inside the cluster. If that token can read secrets, other credentials spill out one after another; if it can create Pods, it becomes a foothold for privilege escalation. The narrower the permissions, the smaller the blast radius a single theft can produce.

Finding excessive permissions #

To narrow, you first have to see where things are wide. Let’s start with the diagnostic commands CKS uses often.

List what a specific subject can do #

auth can-i --list lays out at a glance the permissions a specific subject holds. Use --as to impersonate a user or ServiceAccount when checking.

# List the current user's full permissions
kubectl auth can-i --list

# List a specific ServiceAccount's permissions
kubectl auth can-i --list \
  --as=system:serviceaccount:dev:app-sa -n dev

If the output shows *.* in the Resources column and [*] in the Verbs column, that subject can effectively do anything. This is the first signal to narrow.

To check whether an individual action works, use auth can-i in a single line. If kubectl auth can-i get secrets --as=system:serviceaccount:dev:app-sa -n dev returns yes, there’s a chance the permissions are excessive.

Why wildcards are dangerous #

The moment a * enters the rules of a Role or ClusterRole, least privilege breaks.

# Dangerous: allows every action on every resource
rules:
  - apiGroups: ["*"]
    resources: ["*"]
    verbs: ["*"]

This rule is effectively the same as cluster-admin. Because permissions automatically extend to any new resource kind that is added, unintended access keeps widening too. Wherever a * shows up in apiGroups, resources, or verbs, let’s mark it as something to narrow.

A list of dangerous permissions #

The same verb carries different risk depending on which resource it’s attached to. Let’s lay out the combinations to be especially wary of in CKS.

PermissionRisk
get/list secretsReads the tokens, passwords, and certificates inside a Secret directly. Spreads into leaking credentials for other systems
create pods/execGets a shell into a running container. A foothold for lateral movement to the node or network
create podsLaunches a Pod mounting a high-privilege ServiceAccount, escalating privileges
escalate (verb on roles)Creates a Role with broader permissions than its own, escalating itself
bind (verb on roles)Binds an arbitrary Role to another subject, bypassing the controls on granting permissions
impersonate (users/groups/serviceaccounts)Impersonates another subject and acts with that person’s permissions
* on *Effectively cluster-admin

escalate and bind deserve special caution. Kubernetes normally has a privilege-escalation guard that says “you can’t grant others more than you hold yourself,” and these two verbs are the path that bypasses that guard. get on secrets and create on pods/exec show up often on the exam in the form of “remove this permission.”

Narrowing a Role #

The most common exam type is the task of narrowing a broad Role down to least privilege. Let’s compare before and after narrowing.

Before narrowing: an excessive Role #

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: dev
  name: app-role
rules:
  - apiGroups: ["*"]
    resources: ["*"]
    verbs: ["*"]

This Role can do anything inside the dev namespace. Even though the app actually only needs to read one ConfigMap, it can touch secrets, pods, and deployments too.

After narrowing: a least-privilege Role #

If the app only needs to read a ConfigMap named app-config, you narrow resources, verbs, and resourceNames all the way down like this.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: dev
  name: app-role
rules:
  - apiGroups: [""]
    resources: ["configmaps"]
    resourceNames: ["app-config"]
    verbs: ["get"]

There are three points narrowed. resources down to configmaps, verbs down to just get, and with resourceNames, limited to only the specific object named app-config. Now a subject holding this Role can’t touch other ConfigMaps, secrets, or pods either.

The limits of resourceNames #

resourceNames narrows powerfully when giving actions like get, update, delete, and patch on a named object. However, it does not apply to list, create, or deletecollection. list is an action that enumerates everything without knowing the name, and create is an action that makes an object that doesn’t have a name yet, so they can’t be filtered by name. This limit is easy to get confused about on the exam, so let’s commit it to memory.

Binding with a RoleBinding #

Connect the narrowed Role to a specific ServiceAccount.

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: dev
  name: app-role-binding
subjects:
  - kind: ServiceAccount
    name: app-sa
    namespace: dev
roleRef:
  kind: Role
  name: app-role
  apiGroup: rbac.authorization.k8s.io

Since both the Role and the RoleBinding live in the dev namespace, this permission is limited to within dev.

Cutting ClusterRoleBinding overuse #

The most common mistake that widens permissions is the ClusterRoleBinding. A ClusterRole itself is merely a cluster-scoped definition, but binding it with a ClusterRoleBinding fires that permission across every namespace.

# Dangerous: grants the edit ClusterRole across the whole cluster
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: app-edit-everywhere
subjects:
  - kind: ServiceAccount
    name: app-sa
    namespace: dev
roleRef:
  kind: ClusterRole
  name: edit
  apiGroup: rbac.authorization.k8s.io

The dev ServiceAccount ends up able to modify resources in every namespace. If the app only operates within dev, this permission is excessive.

Limiting to a namespace with a RoleBinding #

The key pattern is to leave the ClusterRole as is, but bind it with a RoleBinding to limit it to a specific namespace. When you reference a ClusterRole from a RoleBinding, that permission fires only inside the namespace where the RoleBinding lives.

# Safe: limits the edit ClusterRole to within dev only
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  namespace: dev
  name: app-edit-in-dev
subjects:
  - kind: ServiceAccount
    name: app-sa
    namespace: dev
roleRef:
  kind: ClusterRole
  name: edit
  apiGroup: rbac.authorization.k8s.io

It uses the same edit ClusterRole, but now the permission is tied to within the dev namespace. “Change a ClusterRoleBinding into a RoleBinding to narrow the permission” is a regular exam variant. Keep a ClusterRoleBinding only when cluster-wide scope is truly needed (for example, non-namespaced resources like nodes or PersistentVolumes).

Removing default ServiceAccount permissions #

The default ServiceAccount that is created automatically in every namespace is attached automatically when a Pod doesn’t explicitly specify another SA. Binding no permissions at all to this default SA is the baseline. Going further, if you turn off token automount itself, an attacker who hijacks a Pod still can’t get hold of an API token.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: default
  namespace: dev
automountServiceAccountToken: false

You can also turn it off per Pod with spec.automountServiceAccountToken: false. The safe direction is to keep a dedicated ServiceAccount only for workloads that genuinely need a token and turn it on explicitly. The details of token management continue in #5.

Watch out for aggregated ClusterRole #

Kubernetes provides aggregationRule to combine multiple ClusterRoles by label into one. The default view, edit, and admin ClusterRoles are built this way.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring
aggregationRule:
  clusterRoleSelectors:
    - matchLabels:
        rbac.example.com/aggregate-to-monitoring: "true"
rules: []

The thing to watch is that if you create a new ClusterRole carrying this label, its rules are automatically merged into the aggregated ClusterRole. If someone adds secrets get via a ClusterRole tagged with that label, every subject using monitoring quietly gains the ability to read secrets. When narrowing an aggregated ClusterRole, you have to inspect not just the body but every ClusterRole that merges in via that label together.

Verifying narrowed permissions #

After narrowing, always verify. CKS grading too, in the end, looks at “does what’s needed work, and what’s unneeded not work.” Let’s check by impersonating the target subject with --as.

# What should work: reading app-config is yes
kubectl auth can-i get configmaps/app-config \
  --as=system:serviceaccount:dev:app-sa -n dev

# What should not work: reading secrets is no
kubectl auth can-i get secrets \
  --as=system:serviceaccount:dev:app-sa -n dev

# What should not work: another namespace is no
kubectl auth can-i get configmaps \
  --as=system:serviceaccount:dev:app-sa -n kube-system

# Lay out the full permissions to confirm the wildcards are gone
kubectl auth can-i --list \
  --as=system:serviceaccount:dev:app-sa -n dev

The expectation is yes only on the needed actions and no on the rest. If *.* and [*] don’t show up in the --list output, the narrowing was done right.

Exam points #

  • Least privilege is narrowing resources, verbs, and scope all together. Limit resources to a specific kind, verbs to the actual actions only, and scope to a namespace or resourceNames.
  • A wildcard is the first signal to narrow. Wherever a * shows up in a rule’s apiGroups, resources, or verbs, it’s something to remove.
  • Identify the dangerous permissions. get and list on secrets, create on pods/exec, create on pods, and escalate, bind, and impersonate are the core ones.
  • Changing a ClusterRoleBinding into a RoleBinding to limit it to a namespace is a regular pattern. Leave the ClusterRole as is and change only the binding to a RoleBinding.
  • resourceNames does not apply to list, create, or deletecollection. Remember that there are actions that can’t be narrowed by name.
  • Grant no permissions to the default ServiceAccount, and turn off token mounting with automountServiceAccountToken: false.
  • Verify with auth can-i --as. Check both sides: that what’s needed returns yes and what’s unneeded returns no.

Wrap-up #

What this post locked in:

  • The principle of least privilege. Narrow resources, verbs, and scope all together to shrink the blast radius of a single theft.
  • Finding excessive permissions. Use auth can-i --list to find wildcard subjects and auth can-i to check individual actions.
  • Narrowing and limiting. Narrow a Role with resourceNames, and change a ClusterRoleBinding into a RoleBinding to tie it to a namespace.
  • Dangerous permissions and the default SA. Be wary of secrets get, pods/exec, escalate, bind, and impersonate, grant no permissions to the default SA, and turn off token mounting.

Next — ServiceAccount token management #

Now that permissions are narrowed, it’s time to manage the token itself that those permissions ride out on.

In #5 ServiceAccount token management, restricting API access, cluster upgrades, we’ll lay out how ServiceAccount tokens get issued and mounted, short-lived projected tokens and how to turn off automount, settings to restrict anonymous access to the apiserver and API access, and the cluster upgrade flow for security patches.

X