Conftest: Expressing and Enforcing Policy for Configuration Systems
Anthony Critelli
You don't need a picture of me because I'm standing right in front of you, so here are my dogs: Kramer and Newman
hosts: all
tasks:
- name: Set file permissions
ansible.builtin.file:
path: /etc/passwd
mode: 0777
- name: Install very important package
ansible.builtin.package:
name: telnet
state: latest
Write config with policy violation
Perform code review. Human may or may not notice bad practice
Policy violation may or may not make it into production
# Scalar
admin_user := "bob"
# List
packages := [ "telnet", "netcat" ]
# Set
apps_set := { "telnet", "netcat", "ping", "tcpdump" }
# List of objects
users := [
{
"name": "alice",
"admin": true,
"account_enabled": false
},
{
"name": "bob",
"admin": true,
"account_enabled": true
},
{
"name": "trudy",
"admin": false,
"account_enabled": true
}
]
package main
import future.keywords.in
import future.keywords.if
# Constants are technically rules
apps := [ "telnet", "netcat", "ping", "tcpdump" ]
# The rules below are equivalent
# Uses newer keywords "if" and "in"
contains_netcat := true if {
some app in apps
app == "netcat"
}
# If the head doesn't define a value, then it defaults to true
contains_netcat {
apps[_] == "netcat"
}
package main
import future.keywords.in
import future.keywords.if
import future.keywords.contains
apps := [
{ "name": "apache", "latest": false },
{ "name": "java", "latest": true },
{ "name": "vim", "latest": false },
]
outdated_packages contains app.name if {
some app in apps
app.latest == false
}
outdated_packages[name] {
some app in apps
not app.latest
name := app.name
}
outdated_apache {
some app
outdated_packages[app]
app == "apache"
}
$ wget https://github.com/open-policy-agent/conftest/releases/download/v0.34.0/conftest_0.34.0_Linux_x86_64.tar.gz
$ tar -xf conftest_0.34.0_Linux_x86_64.tar.gz
$ sudo mv conftest /usr/local/bin/conftest
$ tree
.
├── playbook.yaml
├── policy
│ └── main.rego
└── README.md
1 directory, 3 files
2. Run tests against config
$ conftest test playbook.yaml
FAIL - playbook.yaml - main - Netcat is not a permitted package
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
package security.example.com
...
warn_sudo contains msg if {
some key,val in input.path
"sudo" in val.capabilities
msg := sprintf("Sudo capabilities granted on %v", [key])
}
$ conftest test docker-compose.yaml --data data.yaml
FAIL - docker-compose.yaml - main - Port 3306 is not allowed to be exposed.
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
import data.allowed_ports
deny_ports contains msg if {
some service in input.services
some published_port in service.ports
port := split(published_port, ":")[0]
not to_number(port) in allowed_ports
msg := sprintf("Port %v is not allowed to be exposed.", [port])
}
-o, --output string Output format for conftest results - valid options are: [stdout json tap table junit github] (default "stdout")
$ git commit -m "Add initial Dockerfile"
Running conftest pre-commit hook...
FAIL - Dockerfile - main - Base image 'ubuntu:12.04' is not in list of allowed base images
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
# Parsed config files can then be used in the Rego playground
$ conftest parse vault_policy.hcl
{
"path": {
"auth/token/*": {
"capabilities": [
"create",
"read",
"update",
"sudo"
]
},
"monolithic-database/*": {
"capabilities": [
"create",
"read",
"update"
]
}
}
}
Contact: critellia at gee mail dot com
Connect: https://www.linkedin.com/in/anthonycritelli/
Unused Slides
deny_telnet_with_package contains msg if {
some task in input.tasks
task["ansible.builtin.package"].name == "telnet"
task["ansible.builtin.package"].state != "absent"
msg := sprintf("Task '%v' contains illegal package telnet", [task["name"]])
}
❯ conftest test playbook.yaml
FAIL - playbook.yaml - main - Task 'Install telnet' contains illegal package telnet
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
violation_netcat contains complex_message if {
some task in input.tasks
task["ansible.builtin.package"].name == "netcat"
task["ansible.builtin.package"].state != "absent"
complex_message := {
"msg": "Netcat is not a permitted package",
"details": {
"wiki_article": "https://wiki.example.com/allowed_packages",
"security_policy": "SEC-12345"
}
}
}
❯ conftest test playbook.yaml
FAIL - playbook.yaml - main - Netcat is not a permitted package
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions
❯ conftest test playbook.yaml -o json | jq -r .[0].failures
[
{
"msg": "Netcat is not a permitted package",
"metadata": {
"details": {
"security_policy": "SEC-12345",
"wiki_article": "https://wiki.example.com/allowed_packages"
}
}
}
]
warn_sudo contains msg if {
some key, val in input.path
"sudo" in val.capabilities
msg := sprintf("Sudo capabilities granted on %v", [key])
}
❯ conftest test vault_policy.hcl --all-namespaces
WARN - vault_policy.hcl - security.example.com - Sudo capabilities granted on auth/token/*
1 test, 0 passed, 1 warning, 0 failures, 0 exceptions
Write config with policy violation
Automatically flag violation with Conftest
Remedy policy violation
Policy violations never have the chance to make it into production
Human review can focus on quality of code, not presence of violations