Kubernetes RBAC - Tips and Tricks

In September 2016 I was contracted to an organisation that was adopting a new technology called Kubernetes. At that point I had never heard of it but I had played with Docker and looked at the security associated with it. Little did I know what a huge technological impact Kubernetes would have and how fortunate I was to be exposed to it early. Back then Kubernetes didn’t have RBAC and I remember quite the stir in the Spring of 2017 when they announced its availability in beta. Over time I’ve become more and more accustomed to Kubernetes RBAC being able to exploit it during penetration tests as well provide advice to customers of how to apply least privilege principles and lock it down for Kubernetes based Capture the Flag (CTF) events.

During KubeCon + CloudNativeCon North America 2023, I was surprised at the number of questions I was asked surrounding it and the lack of depth of knowledge in the area so I’ve decided to provide a few tips and tricks I’ve found during my journey with Kubernetes RBAC.

This post assumes you have a good grasp of Kubernetes and the basics of RBAC. If you want to know more about the fundamentals, I recommend starting at the Official Kubernetes Site

What Action can I do?

RBAC is bound to service accounts either via a role or clusterrole binding. When you obtain a service account token you want to know what you can do with it, that is, what permissions it has against the Kubernetes API. kubectl has a magical command which will allow you to enumerate permissions called auth can-i. It essentially determines whether an action is allowed or not. For example, I could do the following to determine whether I can list pods within my current namespace.

$ kubectl auth can-i list pods
yes

If I wanted to know if I could list pods for all namespaces I could append it with --all-namespaces or in shorthand -A.

$ kubectl auth can-i list pods -A
yes

Although this is useful, it would take a considerable time to determine what resources within the cluster you have access to. This is where the --list function comes in. It allows you to list all allowed actions by a namespace.

$ kubectl auth can-i --list -n kube-system
Resources                                       Non-Resource URLs   Resource Names   Verbs
*.*                                             []                  []               [*]
                                                [*]                 []               [*]
selfsubjectreviews.authentication.k8s.io        []                  []               [create]
selfsubjectaccessreviews.authorization.k8s.io   []                  []               [create]
selfsubjectrulesreviews.authorization.k8s.io    []                  []               [create]
                                                [/api/*]            []               [get]
                                                [/api]              []               [get]
                                                [/apis/*]           []               [get]
                                                [/apis]             []               [get]
                                                [/healthz]          []               [get]
                                                [/healthz]          []               [get]
                                                [/livez]            []               [get]
                                                [/livez]            []               [get]
                                                [/openapi/*]        []               [get]
                                                [/openapi]          []               [get]
                                                [/readyz]           []               [get]
                                                [/readyz]           []               [get]
                                                [/version/]         []               [get]
                                                [/version/]         []               [get]
                                                [/version]          []               [get]
                                                [/version]          []               [get]

If you are able to identify all namespaces and combine it with this auth can-i command, you’ll soon have a complete picture of what the current service account has access to in the cluster.

$ available_ns=$(kubectl get namespaces -o name | cut -d "/" -f 2) && for i in $available_ns; do echo "namespace: $i" && kubectl auth can-i --list -n $i; done

Be Explicit

If you are required to configure service accounts in a Kubernetes cluster, you’ll need to delve into creating roles, cluster roles and their associated bindings. Depending on how much time you are given and your understanding of the resource you are configuring, creating the RBAC manifests could be relatively simple or hugely frustrating. It can be tempting to make use of the ‘*’ parameter when you believe that a service account requires access to all resources or be able to perform all operations.

The primary issue of doing this is that you may overlook the impact of assigning a permission set or not realize that an additional permission is granted. For example, you may want to allow a service account to have access to all resources associated with the Core API (v1). A quick look at the Core API, returns the following resource kinds.

$ kubectl api-resources --api-group=’’
NAME                     SHORTNAMES   APIVERSION   NAMESPACED   KIND
bindings                              v1           true         Binding
componentstatuses        cs           v1           false        ComponentStatus
configmaps               cm           v1           true         ConfigMap
endpoints                ep           v1           true         Endpoints
events                   ev           v1           true         Event
limitranges              limits       v1           true         LimitRange
namespaces               ns           v1           false        Namespace
nodes                    no           v1           false        Node
persistentvolumeclaims   pvc          v1           true         PersistentVolumeClaim
persistentvolumes        pv           v1           false        PersistentVolume
pods                     po           v1           true         Pod
podtemplates                          v1           true         PodTemplate
replicationcontrollers   rc           v1           true         ReplicationController
resourcequotas           quota        v1           true         ResourceQuota
secrets                               v1           true         Secret
serviceaccounts          sa           v1           true         ServiceAccount
services                 svc          v1           true         Service

Defining a manifest to access these resources would look something like this:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: clusterrole-core-star
rules:
- apiGroups:
  - ""
  resources:
  - "*"
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch

In this instance, we are not applying ’*’ to all verbs so we can assume that the role only has the operational verbs to the kinds we discovered previously. Although this is true, it is not the complete picture. Kubernetes has sub-resources which reside under the primary resources listed. A good example of this is serviceaccounts/token sub-resource which allows a user to issue tokens for existing service accounts if granted the create permission. As you can imagine this permission allows a user to escalate privileges within the cluster.

This demonstrates the power of the ’*’ parameter for a resource kind but what damage can be done with verbs? If applied to a specific resource, a lot of damage. Some Kubernetes resources have special verbs associated with them. Examples include Role and ClusterRole which has escalate and bind (verbs which allow a service account to perform privileged operations) and User, Group, ServiceAccounts which has impersonate (allows users gain the rights of other users in the cluster). If you want to know more about the impact of these verbs I recommend looking at the Kubernetes RBAC Good Practices and Privilege Escalation Prevention.

The point of all this is…Be Explicit.

If you believe you need all the permissions to a specific resource, list out all the verbs that you need. Do not assume that ’*’ is safe. Also consider what would happen if a new verb is introduced by the Kubernetes maintainers. Rather than be caught out by granting that permission unknowingly, you know that your manifest will not have that verb unless a user has explicitly put it in.

One last point on this topic. It is becoming more common to leverage third party tooling within Kubernetes. Whilst you may trust the publisher of the tool, it has been found that the RBAC permissions that are applied to these products are aimed at ensuring it will work in any environment. As such they can be over privileged and in some cases for Kubernetes Operators I’ve seen service accounts granted cluster-admin. Like any other third party tooling, check the permissions it requires and ensure it is not granted anything beyond what it needs.

Cross Namespace Access with a Role

A common mistake I see with Kubernetes RBAC is the belief that a ClusterRole must be used to access resources in another namespace. This is untrue and if a service account only requires access to specific namespaces, it can be defined in a Role and RoleBinding. Let’s look through an example.

Let’s say I have two namespaces, public and private. I have a pod deployed into both namespaces and for the public pod, I attach a service account called user. If I wanted the user service account to be able to get and list a pod in the private namespace I can specify the following Role and RoleBinding.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: private-pod-access
  namespace: private
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: private-pod-access
  namespace: private
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: private-pod-access
subjects:
- kind: ServiceAccount
  name: user
  namespace: public

The important part of the RBAC manifests is the namespace to which they are applied. Both are applied to the private namespace but the service account, associated with Role by the RoleBinding, is bound to the public namespace. With this in place, I can now exec into the public pod and be able to get pods in the private namespace.

asciicast

Restricting Access to a Specific Resource

Another feature I’ve rarely seen used by a customer is the ability to restrict access to a specific resource. This is useful if a service account requires access to a resource, such as a secret, but you don’t want it to have access to all secrets. This is achieved with the resourceNames rule within a Role manifest.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-user-role
  namespace: private
rules:
- apiGroups:
  - ""
  resources:
  - secrets
  verbs:
  - get
  resourceNames:
  - user-secret

If I were to attempt to get secrets for the private namespace, I would receive a warning from API server that I do not have permissions to do that, however if I were to get the specific resource in this instance (user-secret), it will return with a success.

asciicast

Kubernetes RBAC Tools

The following is a list of tools I’ve found useful for numerating or exploiting Kubernetes RBAC.

Kubernetes RBAC Examples

To help understand useful and dangerous Kubernetes RBAC permissions I’ve created a repository (https://github.com/wakeward/kubernetes-rbac-examples) with examples to review and test. A reminder the dangerous manifests are for educational means and please don’t run them in a production environment.