Skip to main content

Conftest

Conftest is a very useful tool for testing policies using the Rego language.

We don't need to wait until the deploy process to confront these policies in the admission controller. Along with the project repository, or in another repository, we can have these policies configured and confront the manifests or Dockerfiles to avoid a complete pipeline and discover in the last second that we have errors.

By default, in the same directory where we're executing the conftest command, the rules will be searched in the policy directory, but we can point to a directory where we have the policies written in Rego that we'll confront.

Let's install the cli, but we could use it via docker obviously.

LATEST_VERSION=$(wget -O - "https://api.github.com/repos/open-policy-agent/conftest/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-)
ARCH=$(arch)
SYSTEM=$(uname)
wget "https://github.com/open-policy-agent/conftest/releases/download/v${LATEST_VERSION}/conftest_${LATEST_VERSION}_${SYSTEM}_${ARCH}.tar.gz"
tar xzf conftest_${LATEST_VERSION}_${SYSTEM}_${ARCH}.tar.gz
sudo mv conftest /usr/local/bin

root@cks-master:~# conftest --version
Conftest: 0.55.0
OPA: 0.67.0

Let's create two folders

# Creating the policy folder to avoid having to pass it as a parameter

root@cks-master:~# mkdir -p policy/kubernetes
root@cks-master:~# mkdir -p policy/dockerfiles

# Rules for deployment
root@cks-master:~# vim policy/kubernetes/deployment.rego
# NOTE: in this rule it's expecting securityContext at pod level and not at container level.
root@cks-master:~# cat policy/kubernetes/deployment.rego
# from https://www.conftest.dev
package main

deny[msg] {
input.kind = "Deployment"
not input.spec.template.spec.securityContext.runAsNonRoot = true
msg = "Containers must not run as root"
}

deny[msg] {
input.kind = "Deployment"
not input.spec.selector.matchLabels.app
msg = "Containers must provide app label for pod selectors"
}
root@cks-master:~# vim policy/dockerfiles/base.rego
root@cks-master:~# cat policy/dockerfiles/base.rego
# from https://www.conftest.dev
package main

denylist = [
"ubuntu"
]

deny[msg] {
input[i].Cmd == "from"
val := input[i].Value
contains(val[i], denylist[_])

msg = sprintf("unallowed image found %s", [val])
}
root@cks-master:~# vim policy/dockerfiles/commands.rego
root@cks-master:~# cat policy/dockerfiles/commands.rego
# from https://www.conftest.dev

package commands

denylist = [
"apk",
"apt",
"pip",
"curl",
"wget",
]

deny[msg] {
input[i].Cmd == "run"
val := input[i].Value
contains(val[_], denylist[_])

msg = sprintf("unallowed commands found %s", [val])
}

Now let's create a deployment and check with conftest.

root@cks-master:~# k create deployment httpd --image=httpd -oyaml --dry-run=client > httpd.yaml

root@cks-master:~# cat httpd.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: httpd
name: httpd
spec:
replicas: 1
selector:
matchLabels:
app: httpd
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: httpd
spec:
containers:
- image: httpd
name: httpd
resources: {}
status: {}

# Perfect, because we have the app label, but the runAsNonRoot at pod level is missing.
root@cks-master:~# conftest test httpd.yaml --policy ./policy/kubernetes/
FAIL - httpd.yaml - main - Containers must not run as root

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions


# Solving
root@cks-master:~# vim httpd.yaml
root@cks-master:~# cat httpd.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: httpd
name: httpd
spec:
replicas: 1
selector:
matchLabels:
app: httpd
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: httpd
spec:
securityContext:
runAsNonRoot: true
containers:
- image: httpd
name: httpd
resources: {}
status: {}
root@cks-master:~# conftest test httpd.yaml --policy ./policy/kubernetes/

2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions

We created rules for base (FROM) and commands (RUN) of the Dockerfile. We cannot have the ubuntu image and we cannot have the words apk, apt, pip, curl, and wget in RUN commands. Let's create a Dockerfile and test.

root@cks-master:~# vim Dockerfile

root@cks-master:~# cat Dockerfile
FROM ubuntu
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y golang-go
COPY app.go .
RUN go build app.go
CMD ["./app"]

# It's necessary to pass all namespaces because each package forms a namespace, if we don't pass it, only main will be analyzed which is the default namespace.

root@cks-master:~# conftest test Dockerfile --policy ./policy/dockerfiles/ --all-namespaces
FAIL - Dockerfile - main - unallowed image found ["ubuntu"]
FAIL - Dockerfile - commands - unallowed commands found ["apt-get update && apt-get install -y golang-go"]

2 tests, 0 passed, 0 warnings, 2 failures, 0 exceptions

# We changed the image to alpine
root@cks-master:~# vim Dockerfile
# We're having problems because it doesn't allow the word apt, let's change this policy and remove this word.
root@cks-master:~# conftest test Dockerfile --policy ./policy/dockerfiles/ --all-namespaces
FAIL - Dockerfile - commands - unallowed commands found ["apt-get update && apt-get install -y golang-go"]

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
root@cks-master:~# vim policy/dockerfiles/commands.rego
# apt removed...
root@cks-master:~# cat policy/dockerfiles/commands.rego
# from https://www.conftest.dev

package commands

denylist = [
"apk",
"pip",
"curl",
"wget",
]

deny[msg] {
input[i].Cmd == "run"
val := input[i].Value
contains(val[_], denylist[_])

msg = sprintf("unallowed commands found %s", [val])
}

# All good
root@cks-master:~# conftest test Dockerfile --policy ./policy/dockerfiles/ --all-namespaces

2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions
root@cks-master:~#

Well, there we have another tool that will help with local and pipeline testing.

If you want to run as docker the command will be...

# we map the volume where we have the policy folder
root@cks-master:~# docker run --rm -v $(pwd):/project openpolicyagent/conftest test Dockerfile --all-namespaces --policy policy/dockerfiles/

2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions
root@cks-master:~#