Kyverno is a Kubernetes admission controller used to add policies to your cluster. Admission controllers intercept incoming requests to the Kubernetes api server and check if a field matches a regular expression, then approve or deny the request based on that determination. If you’re not familiar with admission controllers already, I’d recommend reading the official documentation.
If you’re in the process of deciding which admisson controller to use in your environment, consider the background of the team that will be maintaining the policies. Do they all know how to program in Golang? If so, OPA Gatekeeper may be a better choice for you. I personally believe that Kyverno is a better admission controller because the barrier for reading and writing policies is significantly lower than OPA. I haven’t run into a scenario where I need more verbose or complex syntax beyond what ships in Kyverno by default.
Summary
- Have some way to whitelist resources
- Make all your transformations with JSON patches
- Narrow down the scope of your whitelists as much as possible
- Configure all policies to accept an array of rules rather than a single ruleset
- Have some way to unit test against admission controller policies when working with k8s manifests in a github repository
- Separate your Kyverno installation from the underlying policies
- Have some way to toggle rule actions between audit and enforcement
- Add remote policies using raw.githubusercontent.com rather than copying them locally
- When referencing remote policies, target a commit hash or branch version rather than the main branch’s head
- Avoid mutating resources with policies when possible
Example Boilerplate
I created this example github repository that illustrates all of the principles in this article https://github.com/salineselin/kyverno-example
Recommendations
The following sections expand on the gists described earlier
Have some way to whitelist resources
It is inevitable that you will have resources in your kubernetes clusters that violate policies. Defining some generic templatized process around how you add exceptions to a whitelist is crucial. You can whitelist resources by using the exclude
key like so:
|
|
Make all your changes with JSON patches
This applies if you’re using Kustomize to manage your Kyverno transformations. Kustomize overlays are excellent at implicitly overlaying all the necessary parameters, but when you start working with array indexes more, you start wiping data that you don’t intend to and you usually end up repeating yourself a lot. If you make all your transformations with JSON patches rather than overlays, you have a complete list of all your transformations, and debugging those transformations becomes a lot easier when kustomize executes and can explicitly point out a faulty JSON patch.
Narrow down the scope of your whitelists as much as possible
Whitelisting a namespace is a very primitive control for adding exceptions for entities. It’s fast and easy to understand, but is grossly overpermissive. Unless you have your RBAC hardened to a point where clusters users don’t have visibility into what the policy exceptions are, its trivial for an attacker to just use a different namespace that’s been whitelisted.
Configure all policies to accept an array of rules rather than a single ruleset
There are multiple valid syntaxes when defining a policy. You can match according to one object or an array of objects. The preferred method is an array of objects. It’s not mentioned in Kyverno’s documentation, but if you use an any
match selector, you also have to use the any
selector when creating exclusions. To demonstrate why you want to only work with an array of matches rather than a single defined match, let’s work with the following policy:
|
|
This policy which you can find in source here is perfectly valid. The problem is when you want to whitelist a particular namespace or add another ruleset with more advanced targeting, you have to heavily modify the underlying ruleset with JSON patches.
Let’s say I wanted to add two exceptions according to a label selector. Instead of a single JSON patch that looks something like the following, you would have to add multiple patches to get your policy into your defined state. Transformations are necessary, but excessive transformations mean you have more code to maintain, and legibility is decreased.
|
|
This is the desirable syntax
|
|
And this is the less-than-desirable syntax
|
|
Almost all of the policies you find today in the public Kyverno policies repository now use the array syntax by default, but it wasn’t always that way. In this earlier commit you can find remnants of when the configured rules did not use the array syntax.
Have some way to unit test against admission controller policies when working with k8s manifests in a github repository
Debugging CI sucks. If your CI|CD pipeline is verbose and takes five minutes to deploy something to a cluster, but it fails due to a failed policy check, it chisels at your soul. Feedback loops become repetitive, slow, and unfruitful. If you’re using a CI provider like Github Actions, make a pipeline that runs a unit test using kubectl apply -f /path/to/yaml --dry-run=server
. If this method is unpalatable for your use-case, it’s worth looking into tools like Datree that offer admission controller unit testing. I personally don’t use Datree because until they ship their own admission controller, there will always be a lack of parity between the policies Datree provides and the policies you provide in Kyverno.
If that isn’t soon enough and you’re still experiencing pain with iteration, you could potentially use a git hook when you push source control changes to remote (similar to how husky does it) to get that feedback even sooner. I’d recommend only implementing a hook like this if the developers working on kubernetes manifests are acclimated to kubernetes and have their policies pulled down into their local dev cluster, or they’re authenticated into a remote cluster with a context they can use to --dry-run=server
against.
Most of the manifests you write are likely written and then rarely touched, so iteration slow and frequent enough to cause admission controller heartache is likely seldom.
Separate your Kyverno installation from the underlying policies
Respect that the policies you install are separate from the application that enforces the policies. They have divergent lifecycles, but should be respected as harmonious companions
Have some way to toggle rule actions between audit and enforcement
If you’re working with remote policies a lot, most of them are usually set to audit
rather than enforce
, so you’ll need to make a transformation that changes the validationFailureAction
value. I use this JSON patch:
|
|
Add remote policies using raw.githubusercontent.com rather than copying them locally
Kustomize allows you to target local as well as remote resources too! Make use of the Kyverno/policies repository and use those instead of rewriting what’s likely already been written. An example kustomize manifest that uses remote resources looks like this:
|
|
When referencing remote policies, target a commit hash or branch version rather than the main branch’s head
In the previous example, you can see that explicit commit hashes are targeted rather than the latest and greatest. If you’re using transformations, you’ll likely have to stage the changes and update your patches on major updates.
Avoid mutating resources with policies when possible
You can use an admission controller to mutate offending resources rather than failing them outright. In my opinion this is something you should not do if you can avoid it. It is generally better to correct the resource manifest at its source rather than mutating it. Mutations are excellent for objects like logging sidecars and other container adjacent services.
If you enjoyed this article, have any questions, noticed something inaccurate, or you just want to say hi feel free to drop a comment below or send an email to me@norling.io