Last week, a new tool called Kyverno captured my interest. I spent some time diving into their documentation to understand the benefits and use cases it addresses.
Kyverno is a policy engine designed specifically for Kubernetes. But what exactly is a policy engine? Simply put, it sets acceptable configuration standards for Kubernetes objects and resources. When these standards are deviated from, the engine can block the deployment, send alerts, or even mutate the configurations to comply with the policies. The main use cases for a policy engine like Kyverno revolve around enhancing security, ensuring compliance, automating tasks, and improving governance within Kubernetes clusters.
Basics of Admission Controllers
An admission controller is a component within Kubernetes that intercepts requests to the Kubernetes API server before the object is persisted, but after the request has been authenticated and authorized. As shown in the figure below, when a request to create a deployment is sent to the API server, it first undergoes authentication and authorization. Afterward, it is passed to the Admission Controller, depicted as a police officer in the diagram. In this scenario, the Admission Controller is configured to ensure that any deployment must have a minimum of two replicas to maintain high availability (HA).
Thus, the best way to think about an admission controller is as a police officer that examines requests sent to the API server to ensure they align with the organization's intent. It acts as a controller or watchdog, persistently ensuring conformance to organizational policies and intents.
Built in Admission Controllers
It's worth noting that Kubernetes includes several built-in admission controllers.. You can find a list of these controllers here.
Before delving further, it's important to differentiate between the two general types of admission controllers:
Validating Admission Controllers: These controllers simply allow or reject requests.
Mutating Admission Controllers: These controllers have the ability to modify objects related to the requests they receive.
Let's take the example of LimitRanger, an in-built admission controller or plugin. A LimitRange defines policies to govern resource allocations for pods or other applicable resources. More details can be found here. It's important to note that LimitRange functions as both a validating and a mutating admission controller.
To try it out, the first step is to add it to the list of admission controllers or plugins using the "--enable-admission-plugins" flag.
apiVersion: v1
kind: Pod
metadata:
annotations:
kubeadm.kubernetes.io/kube-apiserver.advertise-address.endpoint: 172.18.0.3:6443
creationTimestamp: null
labels:
component: kube-apiserver
tier: control-plane
name: kube-apiserver
namespace: kube-system
spec:
containers:
- command:
- kube-apiserver
- --advertise-address=172.18.0.3
- --allow-privileged=true
- --authorization-mode=Node,RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --enable-admission-plugins=NodeRestriction,LimitRanger
Now, let's define the LimitRange object where we configure:
Minimum and maximum CPUs for all pods within the cluster.
Default limits applied when a user doesn't explicitly configure the resources for a pod.
This configuration ensures that all pods adhere to the specified CPU limits, promoting efficient resource utilization across the cluster. Additionally, it provides a safety net by automatically assigning default limits when users do not specify resource constraints for their pods.
apiVersion: v1
kind: LimitRange
metadata:
name: cpu-resource-constraint
spec:
limits:
- default: # this section defines default limits
cpu: 500m
defaultRequest: # this section defines default requests
cpu: 500m
max: # max and min define the limit range
cpu: 600m
min:
cpu: 100m
type: Container
Below is an example of a Pod configuration that requests 700m of CPU resources, which exceeds the defined limit of 500m. When attempting to apply this configuration, you'll immediately encounter an error, showcasing the validating behavior of the admission controller.
apiVersion: v1
kind: Pod
metadata:
name: example-conflict-with-limitrange-cpu
spec:
containers:
- name: demo
image: registry.k8s.io/pause:2.0
resources:
requests:
cpu: 700m
The Pod "example-conflict-with-limitrange-cpu" is invalid: spec.containers[0].resources.requests: Invalid value: "700m": must be less than or equal to cpu limit of 500m
Now, let's attempt to create another pod without specifying any resource requests or limits.
kubectl run nginx --image nginx
We can observe that the default requests and limits have been automatically applied to the pod, courtesy of the admission controller. This exemplifies a mutating behavior, wherein the original request has been modified to include the necessary resource requests and limits.
kubectl get pods nginx -o yaml
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: nginx
resources:
limits:
cpu: 500m
requests:
cpu: 500m
External Admission Controllers
In the previous section, we delved into the inherent admission controllers within Kubernetes. However, these built-in controllers often lack the configurability or flexibility needed to accommodate diverse organizational requirements.
Below is a diagram (Source: Kyverno documentation) illustrating the stages of an API request to a Kubernetes API Server, highlighting the crucial steps involving Mutating and Validating admissions.
Here's a concise overview of the mentioned components:
Webhook Controller: This controller is responsible for managing webhooks, which play a pivotal role in programming the Kubernetes API server to forward API requests to Kyverno. Webhooks are essential building blocks that enable seamless integration between Kubernetes and Kyverno, facilitating the enforcement of policies and configurations.
Policy Engine: Kyverno's policy engine serves as the cornerstone component tasked with evaluating and enforcing policies on Kubernetes resources. It empowers administrators to define policies as Kubernetes resources within Kyverno, enabling them to manage, validate, and mutate other Kubernetes resources effectively. By processing these policies, the policy engine ensures that the cluster complies with desired configurations and standards, fostering consistency and adherence to organizational policies.
Cert Renewer: This component focuses on managing TLS (Transport Layer Security) communications between the Kubernetes API Server and the Kyverno webhook service. By handling TLS certificates, the Cert Renewer ensures secure transmission of data between these components, safeguarding the integrity and confidentiality of communications.
The ValidatingWebhookConfiguration plays a crucial role in configuring the Kubernetes API server to route requests to Kyverno. Here's how it works:
Webhooks Section: This section defines where to redirect matching API requests. It typically references a Kubernetes Service and specifies a particular path to send the requests for validation.
Rules Section: The rules section allows you to specify the resources and operations that you want to send to the admission controller for validation. This granularity enables precise control over which requests are subject to validation.
Namespace Selector: This field enables you to focus admission control on specific namespaces where stricter governance is required or where you want to apply policies gradually. It provides flexibility in applying policies across different parts of the cluster.
Failure Policy: This field determines how the Kubernetes API server responds when it receives a negative admission response from the admission controller. The options are "Fail" and "Ignore". "Fail" immediately rejects the intended operation upon receiving a negative admission response, ensuring strict enforcement of policies. On the other hand, "Ignore" provides more leniency by allowing the operation to proceed despite policy violations. This can be useful when you want to identify and understand gaps before fully enforcing policies.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: kyverno-validating-webhook
webhooks:
- name: validate.kyverno.svc
clientConfig:
service:
name: kyverno-svc
namespace: kyverno
path: "/validate"
caBundle: <ca-bundle> # Base64 encoded CA certificate
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: ["*"]
apiVersions: ["*"]
resources: ["*"]
failurePolicy: Fail
matchPolicy: Equivalent
namespaceSelector: {}
objectSelector: {}
sideEffects: None
timeoutSeconds: 10
admissionReviewVersions: ["v1"]
Sample Policies
This example policy ensures that each configured pod must have the "app" label.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-label-app
spec:
validationFailureAction: Audit
background: false
validationFailureActionOverrides:
- action: Enforce # Action to apply
namespaces: # List of affected namespaces
- default
rules:
- name: check-label-app
match:
any:
- resources:
kinds:
- Pod
validate:
message: "The label `app` is required."
pattern:
metadata:
labels:
app: "?*"
Explanation:
ClusterPolicy: as the name suggests is a policy that applies Cluster wide vs policy which is scoped to the namespace.
background: When set to true, this field ensures that the policy is applied retroactively to existing resources upon creation or update. This means that the policy will be evaluated against resources that were created before the policy was implemented.
validationFailureAction: This field determines the default action to take when a validation rule fails. In this case, it is set to "audit", meaning that failed validations will be recorded, but resource creation will continue.
validationFailureActionOverrides: This section allows you to override the default validation failure action for specific namespaces. In the example, validation failures in the default namespace will be enforced, overriding the default action set in validationFailureAction.
rules: This section defines the rules of the policy. In this example, there is one rule named "check-app-label" that applies to Pods. The rule specifies that Pods must have an 'app' label.
Attempting to create an nginx pod within the blue namespace succeeds with a warning as per the policy.
kubectl -n blue run nginx --image=nginx
Warning: policy check-label-app.check-label-app: validation error: The label `app` is required. rule check-label-app failed at path /metadata/labels/app/
pod/nginx created
Attempting the same operation within the default namespace is denied again as per policy.
k.eljamali@LLJXN1XJ9X admission % kubectl run nginx --image=nginx
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:
resource Pod/default/nginx was blocked due to the following policies
check-label-app:
check-label-app: 'validation error: The label `app` is required. rule check-label-app
failed at path /metadata/labels/app/'
Here's the example policy enforcing a minimum level of high availability for deployments:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: validate
spec:
validationFailureAction: Enforce
rules:
- name: validate-replica-count
match:
any:
- resources:
kinds:
- Deployment
validate:
message: "Replica count for a Deployment must be greater than or equal to 2."
pattern:
spec:
replicas: ">=2"
Cilium Policies
After reviewing several examples, I've considered implementing Kyverno for admission control of Cilium Network Policies. For that matter I found an interesting repository here that helped me get started.
After making little modifications to various examples on GitHub, I crafted a policy aimed at whitelisting specific namespaces where Cilium policies can be configured. Any attempts to create Cilium policies in namespaces not included in the whitelist will be denied. I think this is pretty useful to ensure no mishaps can happen as part of Network Policy configuration and helps with locking things down one namespace at a time.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: limit-ciliumnetworkpolicy-scope
spec:
validationFailureAction: enforce
background: false
rules:
- name: limit-scope
match:
any:
- resources:
kinds:
- CiliumNetworkPolicy
validate:
message: "CiliumNetworkPolicy can only be applied to specific namespaces"
anyPattern:
- metadata:
namespace: blue
- metadata:
namespace: default
I attempt to use the below Network Policy.
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
name: "isolate-ns1"
namespace: green
spec:
endpointSelector:
matchLabels:
{}
ingress:
- fromEndpoints:
- matchLabels:
{}
As expected, the request is blocked by the admission controller.
kubectl apply -f mypolicy.yaml
Error from server: error when creating "mypolicy.yaml": admission webhook "validate.kyverno.svc-fail" denied the request:
resource CiliumNetworkPolicy/green/isolate-ns1 was blocked due to the following policies
limit-ciliumnetworkpolicy-scope:
limit-scope: 'validation error: CiliumNetworkPolicy can only be applied to specific
namespaces. rule limit-scope[0] failed at path /metadata/namespace/ rule limit-scope[1]
failed at path /metadata/namespace/'
The example below provides more insight on Kyverno's capabilities whereby we leverage a ClusterPolicy whose objective is to ensure that pods to be created need to have the label "app.kubernetes.io/name".
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
annotations:
policies.kyverno.io/title: Require Labels
policies.kyverno.io/category: Best Practices
policies.kyverno.io/minversion: 1.6.0
policies.kyverno.io/severity: medium
policies.kyverno.io/subject: Pod, Label
policies.kyverno.io/description: >-
Define and use labels that identify semantic attributes of your application or Deployment.
A common set of labels allows tools to work collaboratively, describing objects in a common manner that
all tools can understand. The recommended labels describe applications in a way that can be
queried. This policy validates that the label `app.kubernetes.io/name` is specified with some value.
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-for-labels
match:
any:
- resources:
kinds:
- Pod
validate:
message: "The label `app.kubernetes.io/name` is required."
pattern:
metadata:
labels:
app.kubernetes.io/name: "?*"
Now, I try to create a deployment (not a Pod) & interestingly enough the admission controller reacts in the right manner not allowing the deployment to happen.
kubectl create deployment kubernetes-bootcamp --image=gcr.io/google-samples/kubernetes-bootcamp:v1
error: failed to create deployment: admission webhook "validate.kyverno.svc-fail" denied the request:
resource Deployment/default/kubernetes-bootcamp was blocked due to the following policies
require-labels:
autogen-check-for-labels: 'validation error: The label `app.kubernetes.io/name`
is required. rule autogen-check-for-labels failed at path /spec/template/metadata/labels/app.kubernetes.io/name/'
Conclusion
Kyverno and more generally admission controllers can help ensure better security, compliance and governance of Kubernetes Clusters. On top of this, there is a good number of readily available policies making it easier to consume. I even tried asking ChatGPT to generate a Kyverno policy:
please craft a kyverno policy that would allow cilium network policies to only touch pods that have environment: dev as not to disrupt production
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-cilium-policy
spec:
background: true
validationFailureAction: enforce
rules:
- name: restrict-cilium-network-policy
match:
resources:
kinds:
- CiliumNetworkPolicy
validate:
message: "Cilium Network Policies can only target pods with the label 'environment: dev'."
pattern:
spec:
endpointSelector:
matchLabels:
environment: dev
It might be worthwhile if you are getting started to deploy it to have the validationFailureAction as Audit as this would provide context on how objects being created deviate from the norm and have the background set to true as this will ensure that your existing resources are scanned in terms of adherence to organizational policies.ValidateActionOverride will be useful to enforce policies on namespaces one by one to ensure a smooth adoption.
Finally, a good one to mention is Policy Reports that provide insights into the compliance status of resources in the Kubernetes cluster. Though I have noticed that some resources don't show up in the reports, it is still very useful when background is set to true as this will give an indication of the compliance state of all resources within Kubernetes.
kubectl get policyreports.wgpolicyk8s.io -A
NAMESPACE NAME KIND NAME PASS FAIL WARN ERROR SKIP AGE
cilium-monitoring 6228a955-195b-4795-83ee-5c1a662bf532 Deployment prometheus 0 1 0 0 0 8d
cilium-monitoring 81242176-f6ac-4331-8be3-bdedccc32f67 Pod grafana-6f4755f98c-4cxzx 0 1 0 0 0 8d
cilium-monitoring bbe6c1f9-a91f-4628-b524-bf04c2603c0a ReplicaSet grafana-6f4755f98c 0 1 0 0 0 8d
cilium-monitoring c70e65cc-800a-4986-90ab-c52078189f29 ReplicaSet prometheus-67fdcf4796 0 1 0 0 0 8d
cilium-monitoring d7006776-21e2-4b86-a684-d6cf173f2f2e Pod prometheus-67fdcf4796-gj4kb 0 1 0 0 0 8d
cilium-monitoring dd37f6a5-0bd7-43f8-a7d5-337f089df95f Deployment grafana 0 1 0 0 0 8d
kube-system 0064fbf9-80ce-4517-934e-5b34d98e88c5 DaemonSet cilium 0 1 0 0 0 8d
kube-system 0be0cbb5-25b0-448e-922c-074e510d269c Pod cilium-zx692 0 1 0 0 0 8d
kube-system 0d9c47b0-2b55-4752-ae1f-1d22ab63e9c9 Pod cilium-operator-5d64788c99-g2f48 0 1 0 0 0 8d
kube-system 14ae51f5-ba2f-4942-aa35-d93e94d2d76f Pod kube-scheduler-kind-control-plane 0 1 0 0 0 8d
kube-system 23a370a8-ef0e-4d1c-aa19-ffab703b5f0d DaemonSet kube-proxy 0 1 0 0 0 8d
kube-system 35e9706f-7a88-4932-a78c-37745a397abe Pod coredns-76f75df574-xvxns 0 1 0 0 0 8d
kube-system 501429e0-4f00-4f30-9cc8-9eb633d9a0e5 ReplicaSet coredns-76f75df574 0 1 0 0 0 8d
kube-system 6911c7d8-4128-470d-89ef-6fc969e62b05 Pod kube-controller-manager-kind-control-plane 0 1 0 0 0 8d
kube-system 69a7f09e-a753-4dd9-8fe2-e5eb70e3691d Pod kube-proxy-jvllh 0 1 0 0 0 8d
kube-system 8c6c30a5-1ef6-4bd1-9978-381b0aeca611 Deployment cilium-operator 0 1 0 0 0 8d
kube-system 9cd0150a-34fd-49df-837b-d2f19ea575dd ReplicaSet cilium-operator-5d64788c99 0 1 0 0 0 8d
kube-system 9ecef8f0-c9b1-4989-9fa6-15b07f8de581 Deployment coredns 0 1 0 0 0 8d
kube-system a8482fb9-10bc-4819-8230-17a776d1079f Pod cilium-kfrnt 0 1 0 0 0 8d
kube-system be23c6c2-8ad5-4a86-adc1-de4180dc8d81 Pod kube-proxy-g4gnt 0 1 0 0 0 8d
kube-system cc4021e0-2360-403b-b013-fad70807f04b Pod coredns-76f75df574-7zdjj 0 1 0 0 0 8d
kube-system dba5f38a-e8e5-46d0-9e4a-ec269ae9ddb4 Pod cilium-bztgh 0 1 0 0 0 8d
kube-system e0146bde-f48c-4663-997f-019e24402bcd Pod cilium-operator-5d64788c99-hdlsz 0 1 0 0 0 8d
kube-system eadc9e7d-90dd-42c1-9dbe-62b3822d668a Pod kube-proxy-fn567 0 1 0 0 0 8d
kube-system f3d97e60-2a0f-4985-9644-a98e98541103 Pod etcd-kind-control-plane 0 1 0 0 0 8d
kube-system ff45c31d-ca5d-43c5-90da-bb0f113b03b3 Pod kube-proxy-6jfqw 0 1 0 0 0 8d
kyverno 0840b270-f6a2-499c-b1d7-a37a716ef440 Pod kyverno-background-controller-84b5ff7875-fkd2x 0 1 0 0 0 8d
kyverno 13f74dc4-4e27-4a78-b34c-cc9ae3e7b57c CronJob kyverno-cleanup-admission-reports 0 1 0 0 0 8d
kyverno 27bf13f4-2bc8-4dae-8e0c-2a29d57438c0 Deployment kyverno-reports-controller 0 1 0 0 0 8d
kyverno 29bcbffc-d253-4d8e-97d6-98043bf9600b CronJob kyverno-cleanup-cluster-admission-reports 0 1 0 0 0 8d
kyverno 532c51d8-843e-42ae-82d2-d5a8f6f9b62e Pod kyverno-cleanup-controller-574fc8c6b6-kpbbg 0 1 0 0 0 8d
kyverno 6fea3745-3b96-46c9-9c62-59cc22f75722 Deployment kyverno-admission-controller 0 1 0 0 0 8d
kyverno 80e5c9d7-9677-4a71-a4c3-89370afd665e ReplicaSet kyverno-cleanup-controller-574fc8c6b6 0 1 0 0 0 8d
kyverno 94048b9d-8a52-4f70-bbc4-cb814b8ee671 Pod kyverno-reports-controller-5557c85bb4-8jpm6 0 1 0 0 0 8d
kyverno a0948eaa-9332-488c-a7a9-cb9c3ee4b045 ReplicaSet kyverno-admission-controller-6797d4bd6b 0 1 0 0 0 8d
kyverno ab1c5601-072a-4782-b08a-d9c2e9ae3d92 Deployment kyverno-cleanup-controller 0 1 0 0 0 8d
kyverno ead76fa0-bfec-4385-9a00-4b26293c069b Pod kyverno-admission-controller-6797d4bd6b-l2kwf 0 1 0 0 0 8d
kyverno f47b4093-46fd-4eb7-abe3-0e431a98d23a ReplicaSet kyverno-background-controller-84b5ff7875 0 1 0 0 0 8d
local-path-storage 13e4514a-18e1-484d-aa39-c591f34c4d56 Pod local-path-provisioner-7577fdbbfb-6hb2v 0 1 0 0 0 8d
local-path-storage 45119b4b-a214-4d6c-b06a-7c8a2c4af402 ReplicaSet local-path-provisioner-7577fdbbfb 0 1 0 0 0 8d
local-path-storage adc91343-2e69-4059-aebb-1e4a50f80a2b Deployment local-path-provisioner 0 1 0 0 0 8d