Arthur Chiao
Arthur Chiao

Limiting access to Kubernetes resources with RBAC

March 2022


Limiting access to Kubernetes resources with RBAC

This is part 2 of 4 of the Authentication and authorization in Kubernetes series. More

TL;DR In this article, you will learn how to recreate the Kubernetes RBAC authorization model from scratch and practice the relationships between Roles, ClusterRoles, ServiceAccounts, RoleBindings and ClusterRoleBindings.

As the number of applications and actors increases in your cluster, you might want to review and restrict the actions they can take.

For example, you might want to restrict access to production systems to a handful of individuals.

Or you might want to grant a narrow set of permissions to an operator deployed in the cluster.

The Role-Based Access Control (RBAC) framework in Kubernetes allows you to do just that.

Table of contents

  1. The Kubernetes API
  2. Decoupling users and permission with RBAC roles
  3. RBAC in Kubernetes
  4. Assigning identities: humans, bots and groups
  5. Modelling access to resources
  6. Granting permissions to users
  7. Namespaces and cluster-wide resources
  8. Making sense of Roles, RoleBindings, ClusterRoles, and ClusterBindings
  9. Scenario 1: Role and RoleBinding in the same namespace
  10. Scenario 2: Role and RoleBinding in a different namespace
  11. Scenario 3: Using a ClusterRole with a RoleBinding
  12. Scenario 4: Granting cluster-wide access with ClusterRole and ClusterRoleBinding
  13. Bonus #1: Make RBAC policies more concise
  14. Bonus #2: Using Service Account to create Kubernetes accounts

The Kubernetes API

Before discussing RBAC, let's see where the authorization model fits into the picture.

Let's imagine you wish to submit the following Pod to a Kubernetes cluster:

pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: my-pod
spec:
  containers:
  - name: sise
    image: ghcr.io/learnk8s/app:1.0.0
    ports:
    - containerPort: 8080

You could deploy the Pod to the cluster with:

bash

kubectl apply -f pod.yaml

When you type kubectl apply, a few things happen.

The kubectl binary:

  1. Reads the configs from your KUBECONFIG.
  2. Discovers APIs and objects from the API.
  3. Validates the resource client-side (is there any obvious error?).
  4. Sends a request with the payload to the kube-apiserver.

When the kube-apiserver receives the request, it doesn't store it in etcd immediately.

First, it has to verify that the requester is legitimate.

In other words, it has to authenticate the request.

Once authenticated, does the requester have permission to create the resource?

Identity and permission are not the same things.

Just because you have access to the cluster doesn't mean you can create or read all the resources.

The authorization is commonly done with Role-Based Access Control (RBAC).

With Role-Based Access Control (RBAC), you can assign granular permissions and restrict what a user or app can do.

In more practical terms, the API server executes the following operations sequentially:

  1. On receiving the request, authenticate the user.
    1. When the validation fails, reject the request by returning 401 Unauthorized.
    2. Otherwise, move on to the next stage.
  2. The user is authenticated, but do they have access to the resource?
    1. If they don't, reject the request by returning 403 Forbidden.
    2. Otherwise, continue.

In this article, you will focus on the authorization part.

Decoupling users and permission with RBAC roles

RBAC is a model designed to grant access to resources based on the roles of individual users within an organization.

To understand how that works, let's take a step back and imagine you had to design an authorization system from scratch.

How could you ensure that a user has write access to a particular resource?

A simple implementation could involve writing a list with three columns like this:

| User  | Permission | Resource |
| ----- | ---------- | -------- |
| Bob   | read+write |   app1   |
| Alice |    read    |   app2   |
| Mo    |    read    |   app2   |

In this example:

The table works well with a few users and resources but shows some limitations as soon as you start to scale it.

Let's imagine that Mo & Alice are in the same team, and they are granted read access to app1.

You will have to add the following entries to your table:

| User      | Permission | Resource |
| --------- | ---------- | -------- |
| Bob       | read+write |   app1   |
| Alice     |    read    |   app2   |
| Mo        |    read    |   app2   |
| Alice     |    read    |   app1   |
| Mo        |    read    |   app1   |

That's great, but it is not evident that Alice and Mo have the same access because they are part of the same team.

  • In a typical authorization system, you have users accessing resources.
    1/4

    In a typical authorization system, you have users accessing resources.

  • You can assign permissions directly to a user and define what resources they can consume.
    2/4

    You can assign permissions directly to a user and define what resources they can consume.

  • Those permissions map the resources directly. Notice how they are user-specific.
    3/4

    Those permissions map the resources directly. Notice how they are user-specific.

  • If you decide to have a second user with the same permissions, you will have to duplicate the entry.
    4/4

    If you decide to have a second user with the same permissions, you will have to duplicate the entry.

You could solve this by adding a "Team" column to your table, but a better alternative is to break down the relationships:

  1. You could define a generic container for permissions: a role.
  2. Instead of assigning permissions to users, you could include them in the roles that reflect their role in the organisation.
  3. And finally, you could link roles to users.

Let's see how this is different.

Instead of having a single table, now you have two:

  1. In the first table, permissions are mapped to roles.
  2. In the second table, roles are linked to identities.
| Role     | Permission | Resource |
| -------- | ---------- | -------- |
| admin1   | read+write |   app1   |
| reviewer |    read    |   app2   |

| User  |   Roles  |
| ----- | -------- |
| Bob   |  admin1  |
| Alice | reviewer |
| Mo    | reviewer |

What happens when you want Mo to be an admin for app1?

You can add the role to the user like this:

| User  |        Roles        |
| ----- | ------------------- |
| Bob   |        admin1       |
| Alice |       reviewer      |
| Mo    |   reviewer,admin1   |

You can already imagine how decoupling users from permissions with Roles can facilitate security administration in large organizations with many users and permissions.

  • When using RBAC, you have users, resources and roles.
    1/4

    When using RBAC, you have users, resources and roles.

  • The permissions are not assigned directly to a user. Instead, they are included in the role.
    2/4

    The permissions are not assigned directly to a user. Instead, they are included in the role.

  • Users are linked to a role with a binding.
    3/4

    Users are linked to a role with a binding.

  • Since roles are generic, when a new user needs access to the same resources, you can use the existing role and link it with a new binding.
    4/4

    Since roles are generic, when a new user needs access to the same resources, you can use the existing role and link it with a new binding.

RBAC in Kubernetes

Kubernetes implements an RBAC model (as well as several other models) for protecting resources in the cluster.

So Kubernetes uses the same three concepts explained earlier: identities, roles and bindings.

It just calls them with slightly different names.

As an example, let's inspect the following YAML definition needed to grant access to Pods, Services, etc.:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: serviceaccount:app1
  namespace: demo-namespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: role:viewer
  namespace: demo-namespace
rules:          # Authorization rules for this role
  - apiGroups:  # 1st API group
      - ''      # An empty string designates the core API group.
    resources:
      - services
      - pods
    verbs:
      - get
      - list
  - apiGroups: # 2nd API group
      - apiextensions.k8s.io
    resources:
      - customresourcedefinitions
    verbs:
      - list
  - apiGroups: # 3rd API group
      - cilium.io
    resources:
      - ciliumnetworkpolicies
      - ciliumnetworkpolicies/status
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: rolebinding:app1-viewer
  namespace: demo-namespace
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: role:viewer
subjects:
  - kind: ServiceAccount
    name: serviceaccount:app1
    namespace: demo-namespace

The file is divided into three blocks:

  1. A Service Account — this is the identity of who is accessing the resources.
  2. A Role which includes the permission to access the resources.
  3. A RoleBinding that links the identity (Service Account) to the permissions (Role).

After submitting the definition to the cluster, the application that uses the Service Account is allowed to issue requests to the following endpoints:

# 1. Kubernetes builtin resources
/api/v1/namespaces/{namespace}/services
/api/v1/namespaces/{namespace}/pods

# 2. A specific API extention provided by cilium.io
/apis/cilium.io/v2/namespaces/{namespace}/ciliumnetworkpolicies
/apis/cilium.io/v2/namespaces/{namespace}/ciliumnetworkpolicies/status

This is great, but there are a lot of details that we've glossed over.

What resources are you granting access to, exactly?

What is a Service Account? Aren't the identities just "Users" in the cluster?

Why does the Role contain a list of Kubernetes objects?

To understand how those work, let's set aside the Kubernetes RBAC model and try to rebuild it from scratch.

We will focus on three elements:

  1. Identifying and assigning identities.
  2. Granting permissions.
  3. Linking identities to permissions.

Let's start.

Assigning identities: humans, bots and groups

Suppose your new colleague wishes to log in to the Kubernetes dashboard.

In this case, you should have an entity for an "account" or a "user", with each of them having a unique name or ID (such as the email address).

Introducing Users: identify human users and other accounts outside of the cluster

How should you store the User in the cluster?

Kubernetes does not have objects which represent regular user accounts.

Users cannot be added to a cluster through an API call.

Instead, any actor that presents a valid certificate signed by the cluster's certificate authority (CA) is considered authenticated.

In this scenario, Kubernetes assigns the username from the common name field in the 'subject' of the certificate (e.g., "/CN=bob").

A temporary User info object is created and passed to the authorization (RBAC) module.

Digging into the code reveals that a struct maps all of the details collected from the Authentication module.

type User struct {
    name string   // unique for each user
    ...           // other fields
}

Note that the User is used for human or processes outside the cluster.

If you want to identify a process in the cluster, you should use a Service Account instead.

The account is very similar to a regular user, but it's different because Kubernetes manages it.

A Service Account is usually assigned to pods to grant permissions.

For example, you could have the following applications accessing resources from inside the cluster:

For those apps, you can define a ServiceAccount (SA).

Introducing ServiceAccounts: identify applications inside the Kubernetes cluster

Since Service Accounts are managed in the cluster, you can create them with YAML:

service-account.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: sa:app1             # arbitrary but unique string
  namespace: demo-namespace

To facilitate Kubernetes administration, you could also define a group of Users orServiceAccounts.

Introducing Groups: a collection of Users or ServiceAccounts

This is convenient if you wish to reference all ServiceAccounts in a specific namespace.

Now that you have defined how to access the resources, it's time to discuss the permissions.

Excellent!

At this point, you have a mechanism to identify who has access to resources.

It could be a human, a bot or a group of them.

But what resources are they accessing in the cluster?

Modelling access to resources

In Kubernetes, we are interested in controlling access to resources such as Pods, Services, Endpoints, etc.

Those resources are usually stored in the database (etcd) and accessed via built-in APIs such as:

/api/v1/namespaces/{namespace}/pods/{name}
/api/v1/namespaces/{namespace}/pods/{name}/log
/api/v1/namespaces/{namespace}/serviceaccounts/{name}

The best way to limit access to those resources is to control how those API endpoints are requested.

You will need two things for that:

  1. The API endpoint of the resource.
  2. The type of permission granted to access the resource (e.g. read-only, read-write, etc.).

For the permissions, you will use a verb such as get, list, create, patch, delete, etc.

Imagine that you want to get, list and watch Pods, logs and Services.

You could combine those resources and permission in a list like this:

resources:
  - /api/v1/namespaces/{namespace}/pods/{name}
  - /api/v1/namespaces/{namespace}/pods/{name}/log
  - /api/v1/namespaces/{namespace}/serviceaccounts/{name}
verbs:
  - get
  - list
  - watch

You could simplify the definition and make it more concise if you notice that:

That leads to:

resources:
  - pods
  - pods/logs
  - serviceaccounts
verbs:
  - get
  - list
  - watch

The list is more human-friendly, and you can immediately identify what's going on.

There's more, though.

Besides APIs for built-in objects such as pods, endpoints, services, etc., Kubernetes also supports API extensions.

For example, when using install the Cilium CNI, the script creates a CiliumEndpoint custom resource (CR):

cilium-endpoint.yaml

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: ciliumendpoints.cilium.io
spec:
  group: cilium.io
  names:
    kind: CiliumEndpoint
  scope: Namespaced
  # truncated...

Those objects are stored in the cluster and are available through kubectl:

bash

kubectl get ciliumendpoints.cilium.io -n demo-namespace
NAME   ENDPOINT ID   IDENTITY ENDPOINT STATE   IPV4
IPV6
app1   2773          1628124  ready            10.6.7.54
app2   3568          1624494  ready            10.6.7.94
app3   3934          1575701  ready            10.6.4.24

The custom resources can be similarly accessed via the Kubernetes API:

/apis/cilium.io/v2/namespaces/{namespace}/ciliumendpoints
/apis/cilium.io/v2/namespaces/{namespace}/ciliumendpoints/{name}

If you want to map those into a YAML file, you could write the following:

resources:
  - ciliumnetworkpolicies
  - ciliumnetworkpolicies/status
verbs:
  - get

However, how does Kubernetes know that the resources are custom?

How can it differentiate between APIs that use custom resources and built-in?

Unfortunately, dropping the base URL from the API endpoint wasn't such a good idea.

You could restore it with a slight change.

You could define it at the top and use it later to expand the URL for the resources.

apiGroups:
  - cilium.io     # APIGroup name
resources:
  - ciliumnetworkpolicies
  - ciliumnetworkpolicies/status
verbs:
  - get

What about resources such as Pods that don't have a namespaced API?

The Kubernetes "" empty API group is a special group that refers to the built-in objects.

So the previous definition should be expanded to:

apiGroups:
  - '' # Built-in objects
resources:
  - pods
  - pods/logs
  - serviceaccounts
verbs:
  - get
  - list
  - watch

Kubernetes reads the API group and automatically expands them to:

Mapping resources and API groups in RBAC

Now that you know how to map resources and permissions, it's finally time to glue access to multiple resources together.

In Kubernetes, a collection of resources and verbs is called a Rule, and you can group rules into a list:

rules:
  - rule 1
  - rule 2

Each rule contains the apiGroups, resources and verbs that you just learned:

rules: # Authorization rules
  - apiGroups: # 1st API group
      - '' # An empty string designates the core API group.
    resources:
      - pods
      - pods/logs
      - serviceaccounts
    verbs:
      - get
      - list
      - watch
  - apiGroups: # another API group
      - cilium.io # Custom APIGroup
    resources:
      - ciliumnetworkpolicies
      - ciliumnetworkpolicies/status
    verbs:
      - get
An RBAC rule is a collection or Resource, API Groups and Verbs

A collection of rules has a specific name in Kubernetes, and it is called a Role.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: viewer
rules: # Authorization rules
  - apiGroups: # 1st API group
      - '' # An empty string designates the core API group.
    resources:
      - pods
      - pods/logs
      - serviceaccounts
    verbs:
      - get
      - list
      - watch
  - apiGroups: # another API group
      - cilium.io # Custom APIGroup
    resources:
      - ciliumnetworkpolicies
      - ciliumnetworkpolicies/status
    verbs:
      - get
An RBAC Role is a collection of Rules

Excellent!

So far, you modelled:

The missing part is linking the two.

Granting permissions to users

A RoleBinding grants the permissions defined in a Role to a User, Service Account or Group.

Let's have a look at an example:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: role-binding-for-app1
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: viewer
subjects:
  - kind: ServiceAccount
    name: sa-for-app1
    namespace: kube-system

The definition has two important fields:

As soon as you submit the resource to the cluster, the application or user using the Service Account will have access to the resources listed in the Role.

If you remove the binding, the app or user will lose access to those resources (but the Role will stay ready to be used by other bindings).

Note how the subjects field is a list that contains kind, name and namespace.

The kind property is necessary to identify Users from Service Accounts and Groups.

But what about namespace?

It's often helpful to break the cluster up into namespaces and limit access to namespaced resources to specific accounts.

In most cases, Roles and RoleBindings are placed inside and grant access to a specific namespace.

However, it is possible to mix these two types of resources — you will see how later.

Before we wrap up the theory and start with the practice, let's have a look at a few examples for the subjects field:

subjects:
  - kind: Group
    name: system:serviceaccounts
    apiGroup: rbac.authorization.k8s.io
    # when the namespace field is not specified, this targets all service accounts in all namespace

You can also have multiple Groups, Users or Service Accounts as subjects:

subjects:
  - kind: Group
    name: system:authenticated # for all authenticated users
    apiGroup: rbac.authorization.k8s.io
  - kind: Group
    name: system:unauthenticated # for all unauthenticated users
    apiGroup: rbac.authorization.k8s.io

To recap what you've learned so far, let's look at how to grant permissions for an app to access some custom resources.

First, let's present the challenge: you have an app that needs access to the resources exposed by Cilium.

  • Imagine having an app deployed in the cluster that needs to access a Custom Resource through the API.
    1/2

    Imagine having an app deployed in the cluster that needs to access a Custom Resource through the API.

  • If you don't grant access to those APIs, the request will fail with a 403 Forbidden error message.
    2/2

    If you don't grant access to those APIs, the request will fail with a 403 Forbidden error message.

How can you grant permissions to access those resources?

With a Service Account, Role and RoleBinding.

  • First, you should create an identity for your workload. In Kubernetes, that means creating a Service Account.
    1/4

    First, you should create an identity for your workload. In Kubernetes, that means creating a Service Account.

  • Then, you want to define the permissions and include them into a Role.
    2/4

    Then, you want to define the permissions and include them into a Role.

  • And finally, you want to link the identity (Service Account) to the permissions (Role) with a RoleBinding.
    3/4

    And finally, you want to link the identity (Service Account) to the permissions (Role) with a RoleBinding.

  • The next time the app issues a request to the Kubernetes API, it will be granted access to the Cilium resources.
    4/4

    The next time the app issues a request to the Kubernetes API, it will be granted access to the Cilium resources.

Namespaces and cluster-wide resources

When we discussed the resources, you learned that the structure of the endpoints is similar to this:

/api/v1/namespaces/{namespace}/pods/{name}
/api/v1/namespaces/{namespace}/pods/{name}/log
/api/v1/namespaces/{namespace}/serviceaccounts/{name}

But what about resources that don't have a namespace, such as Persistent Volumes and Nodes?

Namespaced resources can only be created within a namespace, and the name of that namespace is included in the HTTP path.

If the resource is global, like in the case of a Node, the namespaces name is not present in the HTTP path.

/api/v1/nodes/{name}
/api/v1/persistentvolume/{name}

Can you add those to a Role?

You can.

After all, we did not discuss any namespace limitation when Roles and RoleBindings were introduced.

Here's an example:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: viewer
rules: # Authorization rules
  - apiGroups: # 1st API group
      - '' # An empty string designates the core API group.
    resources:
      - persistentvolumes
      - nodes
    verbs:
      - get
      - list
      - watch

If you try to submit that definition and link it to a Service Account, you might realize it doesn't work, though.

Persistent Volumes and Nodes are cluster-scoped resources.

However, Roles can grant access to scoped resources to a namespace.

If you'd like to use a Role that applies to the entire cluster, you can use a ClusterRole (and the corresponding ClusterRoleBinding to assign it a subject).

The previous definition should be changed to:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: viewer
rules:          # Authorization rules
  - apiGroups:  # 1st API group
      - ''      # An empty string designates the core API group.
    resources:
      - persistentvolumes
      - nodes
    verbs:
      - get
      - list
      - watch

Notice how the only change is the kind property, and everything else stays the same.

You can use ClusterRoles to grant permissions to all resources — for example, all Pods in the cluster.

This functionality isn't restricted to cluster-scoped resources.

Kubernetes ships with a few Roles and ClusterRoles already.

Let's explore them.

bash

kubectl get roles -n kube-system | grep "^system:"
NAME
system::leader-locking-kube-controller-manager
system::leader-locking-kube-scheduler
system:controller:bootstrap-signer
system:controller:cloud-provider
system:controller:token-cleaner
# truncated output...

Many are system: prefixed to denote that the resource is directly managed by the cluster control plane.

Besides, all of the default ClusterRoles and ClusterRoleBindings are labelled with kubernetes.io/bootstrapping=rbac-defaults.

Let's also list the ClusterRoles with:

bash

kubectl get clusterroles -n kube-system | grep "^system:"
NAME
system:aggregate-to-admin
system:aggregate-to-edit
system:aggregate-to-view
system:discovery
system:kube-apiserver
system:kube-controller-manager
system:kube-dns
system:kube-scheduler
# truncated output...

You can inspect the details for each Role and ClusterRole with:

bash

kubectl get role <role> -n kube-system -o yaml
# or
kubectl get clusterrole <clusterrole> -n kube-system -o yaml

Excellent!

At this point, you know the basic building blocks of Kubernetes RBAC.

You learned:

  1. How to create identities with Users, Service Accounts and groups.
  2. How to assign permissions to resources in a namespace with a Role.
  3. How to assign permissions to cluster resources with a ClusterRole.
  4. How to link Roles and ClusterRoles to subjects.

There's only one missing topic left to explore: a few unusual edge cases of RBAC.

Making sense of Roles, RoleBindings, ClusterRoles, and ClusterBindings

At a high level, Roles and RoleBindings are placed inside and grant access to a specific namespace, while ClusterRoles and ClusterRoleBindings do not belong to a namespace and grant access across the entire cluster.

However, it is possible to mix these two types of resources.

For example, what happens when a RoleBinding links an account to a ClusterRole?

Let's explore this next with some hands-on practice.

Let's start by creating a local cluster with minikube:

bash

minikube start
😄  minikube v1.24.0
✨  Automatically selected the docker driver
👍  Starting control plane node in cluster
🚜  Pulling base image ...
🔥  Creating docker container (CPUs=2, Memory=4096MB) ...
🐳  Preparing Kubernetes v1.22.3 on Docker 20.10.8 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟  Enabled addons: storage-provisioner, default-storageclass
🏄  Done! kubectl is now configured to use the cluster and "default" namespace by default

To start, create four namespaces:

bash

kubectl create namespace test
namespace/test created
kubectl create namespace test2
namespace/test2 created
kubectl create namespace test3
namespace/test3 created
kubectl create namespace test4
namespace/test4 created

And finally, create a Service Account in the test namespace:

service-account.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: myaccount
  namespace: test

You can submit the resource with:

bash

kubectl apply -f service-account.yaml
serviceaccount/myaccount created

At this point, your cluster should look like this:

Kubernetes setup for testing RBAC with four namespaces

Scenario 1: Role and RoleBinding in the same namespace

Let's start with creating a Role and a RoleBinding to grant the Service Account access to the test namespace:

scenario1.yaml

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: testadmin
  namespace: test
rules:
  - apiGroups: ['*']
    resources: ['*']
    verbs: ['*']
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: testadminbinding
  namespace: test
subjects:
  - kind: ServiceAccount
    name: myaccount
    namespace: test
roleRef:
  kind: Role
  name: testadmin
  apiGroup: rbac.authorization.k8s.io

You can submit the resource with:

bash

kubectl apply -f scenario1.yaml
role.rbac.authorization.k8s.io/testadmin created
rolebinding.rbac.authorization.k8s.io/testadminbinding created

Your cluster looks like this:

Role and RoleBinding in the same namespace as the Service Account

All resources (the Service Account, Role, and RoleBinding) are in the test namespace.

The Role grants access to all resources, and the RoleBinding links the Service Account and the Role.

How do you test that the Service Account has access to the resources?

You can combine two features of kubectl:

  1. User-impersonation with kubectl <verb> <resource> --as=jenkins.
  2. Verifying API access with kubectl auth can-i <verb> <resource>.

Please note that your user should have the impersonate verb as permission for this to work.

To issue a request as the myaccount Service Account and check if you can list Pod in the namespace, you can issue the following command:

bash

kubectl auth can-i get pods -n test --as=system:serviceaccount:test:myaccount
yes

Let's break down the command:

Note how the --as= flag needs some extra hints to identify the Service Account.

The entire string can be broken down to:

--as=system:serviceaccount:{namespace}:{service-account-name}
     ^^^^^^^^^^^^^^^^^^^^^
     This should always be included for Service Accounts.

With this Role+ServiceAccount+RoleBindings combination, you can access all resources in the test namespace.

Role and RoleBinding in the same namespace as the Service Account grant access to the resources from Role's namespace

Excellent!

Let's move on to a more complex example.

Scenario 2: Role and RoleBinding in a different namespace

Let's create a new Role and RoleBinding in the test2 namespace.

Notice how the RoleBinding links the role from test2 and the service account from test:

scenario2.yaml

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: test2
  name: testadmin
rules:
  - apiGroups: ['*']
    resources: ['*']
    verbs: ['*']
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: testadminbinding
  namespace: test2
subjects:
  - kind: ServiceAccount
    name: myaccount
    namespace: test
roleRef:
  kind: Role
  name: testadmin
  apiGroup: rbac.authorization.k8s.io

You can submit the resource with:

bash

kubectl apply -f scenario2.yaml
role.rbac.authorization.k8s.io/testadmin created
rolebinding.rbac.authorization.k8s.io/testadminbinding created

Your cluster looks like this:

Role and RoleBinding in a different namespace from the Service Account

Let's test if the Service Account located in test has access to the resources in test2:

bash

kubectl auth can-i get pods -n test2 --as=system:serviceaccount:test:myaccount
yes

This works, granting the Service Account access to resources outside of the namespace it was created.

Role and RoleBinding in a different namespace from the Service Account grant access to the resources from Role's namespace

It's worth noting that the roleRef property in the RoleBinding does not have a namespace field.

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: testadminbinding
  namespace: test2
subjects:
  - kind: ServiceAccount
    name: myaccount
    namespace: test
roleRef:
  kind: Role
  name: testadmin
  apiGroup: rbac.authorization.k8s.io

The implication is that a RoleBinding can only reference a Role in the same namespace.

A RoleBinding can only reference a Role in the same namespace

Scenario 3: Using a ClusterRole with a RoleBinding

As noted earlier, ClusterRoles do not belong to a namespace.

This means the ClusterRole does not scope permissions to a single namespace.

However, when a ClusterRole is linked to a Service Account via a RoleBinding, the ClusterRole permissions only apply to the namespace in which the RoleBinding was created.

Let's have a look at an example.

Create a RoleBinding in namespace test3 and link the Service Account to the ClusterRole cluster-admin:

cluster-admin is one of those built-in ClusterRoles in Kubernetes.

scenario3.yaml

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: testadminbinding
  namespace: test3
subjects:
  - kind: ServiceAccount
    name: myaccount
    namespace: test
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

You can submit the resource with:

bash

kubectl apply -f scenario3.yaml
rolebinding.rbac.authorization.k8s.io/testadminbinding created

Your cluster looks like this:

Binding a ClusterRole and Service Account with a RoleBinding

Let's test if the Service Account located in test has access to the resources in test3:

bash

kubectl auth can-i get pods -n test3 --as=system:serviceaccount:test:myaccount
yes

But it does not have access to other namespaces:

bash

kubectl auth can-i get pods -n test4 --as=system:serviceaccount:test:myaccount
no
kubectl auth can-i get pods --as=system:serviceaccount:test:myaccount
no
When a ClusterRole is linked to a Service Account via a RoleBinding, the ClusterRole permissions only apply to the namespace in which the role binding has been created.

In this scenario, when you use a RoleBindings to link a Service Account to a ClusterRole, the ClusterRole behaves as if it were a regular Role.

It grants permissions only to the current namespace where the RoleBinding is located.

Scenario 4: Granting cluster-wide access with ClusterRole and ClusterRoleBinding

In this last scenario, you'll create a ClusterRoleBinding to link the ClusterRole to the Service Account:

scenario4.yaml

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: testadminclusterbinding
subjects:
  - kind: ServiceAccount
    name: myaccount
    namespace: test
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

Note the lack of a namespace field on the roleRef again.

This implies that a ClusterRoleBinding cannot identify a Role to link to because Roles belong in namespaces, and ClusterRoleBindings (along with the ClusterRoles they reference) are not namespaced.

You can submit the resource with:

bash

kubectl apply -f scenario3.yaml
rolebinding.rbac.authorization.k8s.io/testadminbinding created

Your cluster looks like this:

Binding a ClusterRole and Service Account with a ClusterRoleBinding

Even though neither the ClusterRole nor the ClusterRole binding defined any namespaces, the Service Account now has access to everything:

bash

kubectl auth can-i get pods -n test4 --as=system:serviceaccount:test:myaccount
yes
kubectl auth can-i get namespaces --as=system:serviceaccount:test:myaccount
Warning: resource 'namespaces' is not namespace scoped
yes
The Service Account has access to everything

From these examples, you can observe some behaviours and limitations of RBAC resources:

Perhaps the most interesting implication here is that a ClusterRole can define common permissions expressed in a single namespace when referenced by a RoleBinding.

This removes the need to have duplicated roles in many namespaces.

Bonus #1: Make RBAC policies more concise

The typical rules section of a Role or ClusterRole looks like this:

rules:
- apiGroups:
    -"
  resources:
  - pods
  - endpoints
  - namespaces
    verbs:
  - get
  - watch
  - list
  - create
  - delete

However, the above configurations can be re-written using the following format:

- apiGroups: ['']
  resources: ['services', 'endpoints', 'namespaces']
  verbs: ['get', 'list', 'watch', 'create', 'delete']

The alternative notation reduces the number of lines significantly and is more concise.

However, Kubernetes still manages the content as a YAML list when you retrieve it from the database.

So every time you get the Role, the array will be rendered into a list:

bash

kubectl get role pod-reader -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
rules:
- apiGroups:
  -""
  resources:
  - pods
# truncated output...

Bonus #2: Using Service Account to create Kubernetes accounts

Service Accounts are usually created automatically by the API server and associated with pods running in the cluster.

Three separate components fulfil this task:

  1. A ServiceAccount admission controller that injects the Service Account property in the Pod definition.
  2. A Token controller that creates a companion Secret object.
  3. A ServiceAccount controller creates the default Service Account in every namespace.

Service Accounts can be used outside the cluster to create identities for users or long-standing jobs that wish to talk to the Kubernetes API.

To manually create a Service Account, you can issue the following commands:

bash

kubectl create serviceaccount demo-sa
serviceaccount/demo-sa created

kubectl get serviceaccounts demo-sa -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: demo-sa
  namespace: default
  resourceVersion: "1985126654"
  selfLink: /api/v1/namespaces/default/serviceaccounts/demo-sa
  uid: 01b2a3f9-a373-6e74-b3ae-d89f6c0e321f
secrets:
- name: demo-sa-token-hrfq2

You might notice a secrets field at the end of the Service Account YAML definition.

What is that?

Every time you create a Service Account, Kubernetes creates a Secret.

The Secret holds the token for the Service Account, and you can use that token to call the Kubernetes API.

It also includes the public Certificate Authority (CA) of the API server:

bash

kubectl get secret demo-sa-token-hrfq2 -o yaml
apiVersion: v1
data:
  ca.crt: (APISERVER'S CA BASE64 ENCODED)
  namespace: ZGVmYXVsdA==
  token: (BEARER TOKEN BASE64 ENCODED)
kind: Secret
metadata:
  # truncated output ...
type: kubernetes.io/service-account-token

The token is a signed JWT that can be used as a bearer token to authenticate against the kube-apiserver.

Usually, these secrets are mounted into pods for accessing the API server but can be used from outside the cluster.

Summary

RBAC in Kubernetes is the mechanism that enables you to configure fine-grained and specific sets of permissions that define how a given user, or group of users, can interact with any Kubernetes object in the cluster or a particular cluster namespace.

In this article, you learned:

There are more articles like this!

Be notified every time we publish articles, insights and new research on Kubernetes!

You are in!

We're also maintain an active Telegram, Slack & Twitter community!