Policy-based validation for Kubernetes (and more!)
Getting started with Conftest to build policies for Kubernetes
While the Kubernetes API server will provide basic schema validation for Kubernetes manifests, we often want to be able to apply our own validation rules to the actual contents of the manifests.
Instead of ad-hoc, imperative checks, we would like to be able to define policies that we can reuse time-after-time, across many resources, and in a way that can embed into our existing software & operational lifecycle.
Enter Conftest, an policy-as-code tool built upon Open Policy Agent - a general purpose policy framework - and its policy langage, Rego language.
Why would we need this?
Validating our resources doesn’t just come down to making them work, it means providing add a whole extra layer of security and confidence. This ranges from unit-testing our resources to full end-to-end integration and regression testing our running infrastructure.
Some use-cases include…
🛠️ Running a unit test suite of policies locally when developing new resources, ensuring the resources adhere to best practices
- Ensuring team labels are applied to resources
- Warning when using
:latest
tag - Ensuring a
Service
has an appropriateselector
configured - Requiring credentials to be provided in the event a private image registry is used
🐳 Running unit & integration tests in a CI/CD pipeline that validate expected changes
- Ensuring that a PVC reference will not change
- Ensuring that Pod
resources
are within an acceptable range for an environment - Ensuring relevant labels & annotations are present as required by Prometheus
✅ Running infrastructure validation/acceptance tests to ensure that newly-provisioned infrastructure was deployed correctly
- Ensuring that a
Service
has liveEndpoint
s - Validating all expected resources are deployed as part of a Helm release
- Validating a blue-green deploy successfully completed, leaving only the appropriate colour running
🔒 Protecting against regressions when changing existing resources, or reporting infomation between releases
- Warning when using a deprecated application env var in a
Pod
spec - Ensuring volume names are accidentally changed between versions
- Diffing container images between releases to detect changes
- Ensuring that version information is updated in annotations
And that’s just for starters!
Since OPA natively works with JSON, and Conftest extends this by supporting
.yaml
, .env
, .dockerignore
, Dockerfile
, .hcl
, and more - we can
see how these practices can be extended across the entire stack.
How does it work?
First of all, we write our policies in the Rego language, then run these policies against one or more documents.
These documents can be local Kubernetes manifests; Kubernetes resources provided via. stdin; a custom data structure; or even a mix of multiple documents at once - Conftest is completely data-agnostic!
Let’s start simple with the following Secret
manifest:
A basic policy for this could look like:
This policy lives in the namespace secrets.example
and contains a single rule,
which is comprised of a rule head - deny contains reason if
- and the body,
which contains expressions to evalute.
The expressions in the rule body are implicitly combined with a logical and
,
meaning that if all of the expressions are truthy, the entire rule passes, and
if any of them are falsey, the rule evaluation is stopped and the rule fails.
As you can see, we can access to the input
object, which is the file we provided.
Since our rule head is deny
, then if the rule passes (all expressions are true),
then we get a failure reported in Conftest.
Trying it out
Now, if we run this with:
We should see the following output, with the message being denoted by the
rule’s reason
variable:
This is because the provided Secret
does not have a token-b
data entry
and as such, will cause all of the expressions to evaluate as true
and
therefore the entire rule will pass, reporting the failure.
However, if we run this rule against a Deployment
manifest, the first expression will
evaluate as false:
And the rule will fail, stopping processing any more expressions and nothing is reported in Conftest’s output.
🎉 The beauty of this, is that we can apply an entire suite of tests against a resource and only applicable ones will report failures!
Alternatively, you can define a rule’s head using violation
or warn
, and
optionally suffix these terms with an underscored identifier.
-
violation
- This allows you to return an object, ie. to provide extra metadata or context -
warn
- Prints a warning instead of a failure in the console output
And if we run this with -o json
, we can see that the other keys in the
reason
object are exposed as the metadata
object.
Conftest uses OPA under the hood, which works by building a JSON object that is the result of evaluating each rule against the input data.
If you run this directly in the Rego playground, you will see the following output:
This is because each passing rule is adding to the deny
array.
Then this data structure is parsed by Conftest to provide more relevant console
output or structured data (when using -o json
)
Building more complex rules
Now that we’ve grasped the fundamentals, let’s build some interesting rules.
Evaluating rules over multiple items
If any expressions in a rule evaluate to multiple items, then the rule will return multiple results. This essentially ‘forks’ the execution of the rule for each item. While this can take some getting used to, it’s also incredibly powerful.
Comprehensions
If you instead wanted to collect multiple values, you can use use a comprehension. This can be used to build an array, a set or an object.
The body inside the comprehension can also contain additional expressions, allowing us to filter or transform elements:
🗒️ A note on syntax changes
To preserve backwards compatibility, new syntax is being introduced as part of opt-in package imports. The newer syntax is (in my opinion) more expressive, but you may still find lots of examples online using the old syntax.
For example, iterating over an array can be written as following when importing
future.keywords.in
/rego.v1
:
Where the older syntax (which is still usable) is:
You can either selectively opt-in to new keywords using the future.keywords
package
(See Future keywords
for more info), or you can opt in to some major breaking changes by importing the
rego.v1
package, which notably changes rule heads:
Helper rules & functions
Helper rules
We can also define helper rules which allow us to abstract some of our logic, but won’t produce a failure in Conftest.
We can then leverage these helper rules in our rules:
Functions
As well as this, we can define functions which allow us to parameterise the input:
Other use-cases
It’s also worth noting that you can do something very interesting things with helper rules & functions.
For example, this blog post by Styra explains how to model ‘or’ logic, by definining the same helper rule/function multiple times.
Partial rules
Building upon the previous example, if we were to try and return different values from this helper rule:
We get the following error:
This is because it has_token
is a ‘complete rule’.
However, we can also define ‘partial rules’ which are allowed to return multiple values as a set:
These helers should look very familiar to the Conftest rules we’ve writing!
Sharing rules & functions
If you have a number of helper rules or functions you would like to share
between policies, you can also import
their namespace and re-use them
between files.
Writing dynamic rules using ‘data’
So far, we’ve written some fairly static rules. Sure, we can add more flexibility by making them more data-driven:
But this hard-coded approach doesn’t scale, or allow us to re-use these policies accross our estate.
However, Conftest provides the means to dynamically provide data in yaml files,
which we then provide to the program via. the --data
flag.
This is then merged into the data
object.
If you are requiring data to be provided, I advise to always add separate rules that verify the existence & shape of the data.
This safeguards against the event you forget to pass any data, or you provide the wrong data.
Without these rules to indicate bad data, the above rule would fail when
trying to access data.bad_keys
, meaning we do not see a failure in the Conftest
report
Merging data for more complex policies
But we don’t have to stop there. As already mentioned, Conftest isn’t limited to parsing Kubernetes yaml manifests.
For example, we might want to compare two versions of the same resource, or
validate multiple resources together (ie. a Service
and a Deployment
).
While we can build this data into a single document ourselves (ie. with some
jq
/yq
magic), Conftest actually provides a --combine
flag, which will merge
each provided document into a single input
array.
Then, the input
object is the following shape:
This extends the use-case of Conftest massively!
Some examples use cases might be…
-
Checking that a
Service
has a valid selector and is targetting the rightPod
s -
Auditing where specific
ConfigMap
values are used, across multipleDeployments
-
Validating that a Helm release has deployed the expected resources
-
Extracting env vars from a
Dockerfile
and ensuring these env vars are declared in aPod
spec
An example using combine
For example, if we also validate the following Pod
spec as well as our
existing Secret
:
With the following policy:
By running:
Conclusion
We’ve covered a lot of ground in this post, but this only scratches the surface as to how you can use Conftest.
Hopefully you can see the power provided by Conftest, Rego and OPA, and how you might use it in your workflow.
I’ll add some more specific examples in a an upcoming cheatsheet, so stay tuned!