Pular para o conteúdo principal

Conftest

Conftest é uma ferramenta muito útil para testar políticas usando a linguagem Rego.

Não precisamos esperar chegar no processo de deploy para confrontar essas políticas no admission controller. Junto ao repositório do projeto, ou em outro repositório, podemos ter essas políticas configuradas e confrontar os manifestos do Dockerfiles para evitar um pipeline completo e no último segundo descobrir que temos erros.

Por padrão, no mesmo diretório que estamos executando o comando conftest serão buscadas as regras no diretório policy, mas podemos apontar um diretório onde temos as políticas escritas em Rego que iremos confrontar.

Vamos instalar o cli, mas poderíamos usar via docker obviamente.

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

Vamos criar duas pastas

# Criando a pasta policy para não precisar ficar passando como parâmetro

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

# Regras para deployment
root@cks-master:~# vim policy/kubernetes/deployment.rego
# OBS: nesta regra ele está esperando securityContext em nível de pod e não de contêineres.
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])
}

Agora vamos criar um deployment e conferir com o 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: {}

# Certinho, pois temos a label app, mas está faltando o runAsNonRoot em nível de pod.
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


# Resolvendo
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

Criamos regras para base (FROM) e commands (RUN) do Dockerfile. Não podemos ter a imagem ubuntu e não podemos ter as palavras apk, apt, pip, curl e wget nos comandos RUN. Vamos criar um Dockerfile e testar.

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"]

# É necessário passar todos os namespaces pois cada package forma um namespace, se não passar será analisado apenas o main que é o namespace padrão.

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

# Mudamos a imagem para alpine
root@cks-master:~# vim Dockerfile
# Estamos tendo problemas, pois ele não permite a palavra apt, vamos mudar essa política e remover essa palavra.
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 removido...
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])
}

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

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

Bom, temos aí mais uma ferramenta que ajudará nos testes locais e de pipeline.

Se quiser rodar como docker o comando será...

# mapeamos o volume onde temos a pasta policy
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:~#