Policy as Code with Open Policy Agent and Conftest

Did you ever make a change in a Kubernetes manifest only to find out upon deployment that you’ve messed up a piece of configuration. I know I have. Wouldn’t it be nice to catch these mistakes before deploying? Open Policy Agent (OPA) allows us to do so.

OPA is a policy engine which enables us to write our own policies and run them against our manifests. We will write these policies in Rego, a declarative language that is provided by OPA.
We will use a utility tool called Conftest to run the policies we’ve made against our configuration files.

Starting off

First off, let’s go ahead and install Conftest. OPA will be included in this installation.

brew tap instrumenta/instrumenta
brew install conftest

Now that we’ve got everything installed we’ll need a manifest that we can run against our policy check. For this I’ve created an example deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: example-app-deployment
 labels:
   app: example-app
 namespace: example
spec:
 replicas: 1
 selector:
   matchLabels:
     app: example-app
 template:
   metadata:
     labels:
       app: example-app
   spec:
     imagePullSecrets:
       - name: example-pull-secret
     containers:
     - name: example-app
       image: <registry>/example-app
       ports:
       - containerPort: 80

Writing the first policy

Our deployment currently has a single replica. Imagine our company requires all of our Deployments to have at least 2 replicas. We’ll be able to guarantee this by writing a policy for it.

package main

deny_replicas[msg] {
    input.kind == "Deployment"                          # If it is a Deployment
    input.spec.replicas < 2                             # And the replicas are < 2
    msg := "Deployments must have 2 or more replicas"   # Set the return variable to the error message and fail the test
}

The policy is fairly straightforward. There are two noteworthy things. First is the input variable that is used. Input is a variable that Conftest creates for you. It takes the input we give it, parses it to JSON format and makes it available as an input variable in our policies.
The second noteworthy thing is something that might feel a bit unnatural. The name of this policy starts with deny. This means that if this test reaches the end, it will fail. If any of the statements in the test resolve to false, the test will abort and it will be considered a success.

Running the policy

Conftest by default requires our policies to be placed in a directory called policy. Therefore our directory structure should look something like this.

dir-structure

Directory structure

We can now run our policy check by executing the following command in our project directory.

Failing testrun

As expected our test failed, since we did not meet our policy by having 2 replicas in our Deployment. Let’s go ahead and fix that and rerun our test.

Succeeding testrun

This time the test passed with flying colors!

Create some more!

Testing our example deployment against our policy is fun, but in any realistic scenario you’d have a lot more configuration files and policies. Therefore I’ve added two more configurations, one for a Service and one for a Secret:

dir-structure2

Directory structure

apiVersion: v1
data:
  .dockerconfigjson: supersecret
kind: Secret
metadata:
  name: example-pull-secret
  namespace: example
type: kubernetes.io/dockerconfigjson
apiVersion: v1
kind: Service
metadata:
  name: example-app
  labels:
    app: example-app
  namespace: example
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app: example-app

Now that we’ve added those, we also want a new policy. Let’s say we want every manifest to explicitly declare its namespace. We can write a policy for this:

package main

deny_namespace[msg] {
   not input.metadata.namespace               # There is no namespace
   msg := "Resources must have a namespace"   # Set the return variable to the error message; The test has reached the end thus has failed
}

Just like the other policy we will deny any input for which all the statements resolve to true. If there is no namespace, input.metadata.namespace will resolve to false. By using the not operator we can resolve this to true if the namespace is missing.In order to run the tests we need to use a wildcard in our conftest command.

We now have 6 tests that have been executed. That’s because we run 2 policies for each of our 3 configuration files.

Unit testing our policies

OPA provides us with an easy way to unit test the policies we’ve written.

test_deny_replicas {
    deny_namespace[msg] with input as {
        "kind": "Deployment",
        "metadata": {
         "name": "example-app-deployment"
        }
    }
}

test_deny_namespace {
    deny_namespace[msg] with input as {
        "kind": "Deployment",
        "spec": {
            "replicas": 2
        }
    }
}

Writing the tests is pretty straightforward. The name of the test must begin with test_. In the test itself all we need to do is call the policy we’d like to test, and provide it with our own input. After this we can run all our unit tests by performing the following command in our policy directory.

What’s next?

We’ve now learned how we can create policies and test our yml files against them. This is only the beginning. Conftest supports many more configuration files like JSON, XML and Dockerfile. Writing policies for all these and running them in your pipeline is a strong way to help guarantee certain standards for your configuration files and could prevent issues that would otherwise stay hidden until deployment.

For more information on Conftest reference their documentation. Make sure to check out the documentation for Open Policy Agent as well. There you will find their syntax, Rego, explained in detail.