The proliferation of the cloud and Kubernetes made it “easier” to provision new environments dynamically, democratizing the access to resources that can be used, for example, for testing. That comes with its own set of challenges; dynamically provisioning Roles and RoleBindings being some of them. If we think of a new “environment” as a namespace in Kubernetes, each of them will have its own Roles and RoleBindings and we would like for them to be managed dynamically.
We could attack this problem with several approaches but we’ll leverage Kyverno and one of its advanced features to manage that for us. Kyverno “is a policy engine designed for Kubernetes. (…) Kyverno policies can validate, mutate, and generate Kubernetes resources”. Kyverno can:
- validate - check resource configurations for policy compliance;
- mutate - modify resources during admission control;
- generate - create additional resources based on resource creation or updates.
We will leverage Kyverno’s generate capabilities to create Roles and RoleBindings every time a new namespace is created.
Get the ball rolling
We will be spinning up a local Kubernetes cluster with k3d and we’ll need the following tools to make it happen:
We start by creating a local Kubernetes cluster with k3d:
k3d cluster create -a 2 kyverno
k3d kubeconfig merge kyverno --switch-context
We then deploy Kyverno into the cluster:
kubectl create -f https://raw.githubusercontent.com/kyverno/kyverno/main/definitions/release/install.yaml
Because we’re generating resources we need to give Kyverno permissions to do so:
# kyverno.yaml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: kyverno:generatecontroller
rules:
- apiGroups:
- "*"
resources:
- namespaces
- networkpolicies
- secrets
- configmaps
- resourcequotas
- limitranges
- pods
- roles
- rolebindings
verbs:
- create
- get
- update
- delete
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: kyverno-admin-generate
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kyverno:generatecontroller
subjects:
- kind: ServiceAccount
name: kyverno-service-account
namespace: kyverno
There’s a bit more than what we need but we can go ahead and apply these permissions:
kubectl apply -f kyverno.yaml
Now that Kyverno is equipped with the necessary permissions to manage our Roles and RoleBindings we need to create the necessary policies that handle that:
# role-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: role
spec:
rules:
- name: role
match:
resources:
kinds:
- Namespace
exclude:
resources:
namespaces:
- "kube-system"
- "default"
- "kube-public"
- "kyverno"
generate:
kind: Role
name: pod-reader
namespace: "{{request.object.metadata.name}}"
data:
rules:
- apiGroups: [""] # "" indicates the core API group
resources: ["pods"]
verbs: ["get", "watch", "list"]
---
# rolebinding-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: rolebinding
spec:
rules:
- name: rolebinding
match:
resources:
kinds:
- Namespace
exclude:
resources:
namespaces:
- "kube-system"
- "default"
- "kube-public"
- "kyverno"
generate:
kind: RoleBinding
name: read-pods
namespace: "{{request.object.metadata.name}}"
data:
subjects:
- kind: User
name: "{{request.object.metadata.labels.user}}"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
To achieve our goal we create two policies, one to create a Role and another for a RoleBinding where Kyverno will be “listening” for changes to Namespace resources (excluding the ones it doesn’t care about). The {{request.object}} contains the fields of the Namespace resource and Kyverno makes use of them to identify the namespace and decide to whom the permission is given, which in our case is a specific user:
kubectl apply -f role-policy.yaml
kubectl apply -f rolebinding-policy.yaml
If we now create a namespace, with the proper fields, Kyverno will do it’s magic:
apiVersion: v1
kind: Namespace
metadata:
labels:
user: jane
name: test-namespace
~ kubectl -n test-namespace get role
NAME CREATED AT
pod-reader 2020-12-27T20:09:49Z
➜ kubectl -n test-namespace get rolebinding
NAME ROLE AGE
read-pods Role/pod-reader 11s
How did Kyverno do this?
Kubernetes has the concept of Admission Controller which is software that can intercept requests to the API server, prior to its creation, and do something with it. It also possesses the concept of Dynamic Admission Control which are HTTP callbacks that provide similar functionality, allowing us to code the behavior we desired. While we could have developed our own Dynamic Admission Control, Kyverno already has the capabilities we seek and makes leveraging it a no-brainer.