Limiting access to Kubernetes resources with RBAC
March 2022
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
- The Kubernetes API
- Decoupling users and permission with RBAC roles
- RBAC in Kubernetes
- Assigning identities: humans, bots and groups
- Modelling access to resources
- Granting permissions to users
- Namespaces and cluster-wide resources
- Making sense of Roles, RoleBindings, ClusterRoles, and ClusterBindings
- Scenario 1: Role and RoleBinding in the same namespace
- Scenario 2: Role and RoleBinding in a different namespace
- Scenario 3: Using a ClusterRole with a RoleBinding
- Scenario 4: Granting cluster-wide access with ClusterRole and ClusterRoleBinding
- Bonus #1: Make RBAC policies more concise
- 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:
- Reads the configs from your
KUBECONFIG
. - Discovers APIs and objects from the API.
- Validates the resource client-side (is there any obvious error?).
- 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:
- On receiving the request, authenticate the user.
- When the validation fails, reject the request by returning
401 Unauthorized
. - Otherwise, move on to the next stage.
- When the validation fails, reject the request by returning
- The user is authenticated, but do they have access to the resource?
- If they don't, reject the request by returning
403 Forbidden
. - Otherwise, continue.
- If they don't, reject the request by returning
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:
- Bob has read & write access to
app1
but has no access toapp2
. - Mo & Alice have only read access to
app2
and have no access toapp1
.
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.
- 1/4
In a typical authorization system, you have users accessing resources.
- 2/4
You can assign permissions directly to a user and define what resources they can consume.
- 3/4
Those permissions map the resources directly. Notice how they are user-specific.
- 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:
- You could define a generic container for permissions: a role.
- Instead of assigning permissions to users, you could include them in the roles that reflect their role in the organisation.
- 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:
- In the first table, permissions are mapped to roles.
- 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.
- 1/4
When using RBAC, you have users, resources and roles.
- 2/4
The permissions are not assigned directly to a user. Instead, they are included in the role.
- 3/4
Users are linked to a role with a 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:
- A Service Account — this is the identity of who is accessing the resources.
- A Role which includes the permission to access the resources.
- 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:
- Identifying and assigning identities.
- Granting permissions.
- 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).
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.
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:
- The
cilium-agent
has to list all pod resources on a specific node. - The
ingress-nginx-controller
has to list all the backend endpoints for a service.
For those apps, you can define a ServiceAccount (SA).
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 User
s orServiceAccount
s.
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:
- The API endpoint of the resource.
- 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:
- The base URL
/api/v1/namespaces/
is common for all. Perhaps we could omit it. - You could assume that all resources are in the current namespace and drop the
{namespace}
path.
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:
- If it is empty
""
to/api/v1/xxx
. - Otherwise
/apis/{apigroup_name}/{apigroup_version}/xxx
.
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
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
Excellent!
So far, you modelled:
- Identities with Users, Service Accounts and Groups.
- Permissions to resources with Roles.
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:
- the
roleRef
that references theviewer
Role. - the
subjects
links to thesa-for-app1
Service Account.
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.
- 1/2
Imagine having an app deployed in the cluster that needs to access a Custom Resource through the API.
- 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.
- 1/4
First, you should create an identity for your workload. In Kubernetes, that means creating a Service Account.
- 2/4
Then, you want to define the permissions and include them into a Role.
- 3/4
And finally, you want to link the identity (Service Account) to the permissions (Role) with a RoleBinding.
- 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:
- How to create identities with Users, Service Accounts and groups.
- How to assign permissions to resources in a namespace with a Role.
- How to assign permissions to cluster resources with a ClusterRole.
- 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:
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:
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:
- User-impersonation with
kubectl <verb> <resource> --as=jenkins
. - 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:
auth can-i
is necessary to query the authorization model (RBAC).get pods
is theverb
andresource
.-n test
is the namespace where you want to issue the command.--as=system:serviceaccount:test:myaccount
is used to impersonate themyaccount
Service Account.
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.
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:
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.
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.
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:
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
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:
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
From these examples, you can observe some behaviours and limitations of RBAC resources:
- Roles and RoleBindings must exist in the same namespace.
- RoleBindings can exist in separate namespaces to Service Accounts.
- RoleBindings can link ClusterRoles, but they only grant access to the namespace of the RoleBinding.
- ClusterRoleBindings link accounts to ClusterRoles and grant access across all resources.
- ClusterRoleBindings can not reference Roles.
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:
- A ServiceAccount admission controller that injects the Service Account property in the Pod definition.
- A Token controller that creates a companion Secret object.
- 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:
- How RBAC decouples permissions from users with a more flexible model.
- How RBAC integrates with the Kubernetes API.
- How to identify subjects for RBAC with Users, Service Accounts and Groups.
- How to map Resources into Rules using Verbs and API groups.
- How to group rules into Roles and link those roles to identities using RoleBindings.
- The relationship between Roles, RoleBindings, ClusterRoles and ClusterRole Bindings.