# Conftest Cheat Sheet

> **Tool:** Conftest
> **Category:** Security & Compliance
> **Verified against:** Conftest 0.62.0 (bundles OPA 1.6.0), flags verified via `conftest --help` /
> `conftest test --help` run locally, 2026-08-29
> **Official docs:** https://www.conftest.dev/

## What it is and where it fits 🎯

Conftest is the practical, config-testing-focused front end to OPA/Rego — "does this YAML/JSON/HCL/Dockerfile
pass our policies" without hand-writing `opa eval` invocations or managing input-loading boilerplate. This is
what most teams actually put in a CI pipeline for policy-as-code checks against Kubernetes manifests,
Terraform plans, and Dockerfiles. If OPA is the engine, Conftest is the steering wheel and dashboard built for
one specific, extremely common job.

## Installation

```bash
brew install conftest                          # macOS/Linux Homebrew
# or download a release tarball for your OS/arch from the GitHub releases page and extract the binary onto PATH
docker pull openpolicyagent/conftest            # containerized, no local install

conftest --version
```

## Testing config files against policies

```bash
conftest test deployment.yaml                          # policies default to a `policy/` directory in the cwd
conftest test --policy my-policies/ deployment.yaml     # explicit policy directory
conftest test k8s/                                       # test every file in a directory
conftest test --all-namespaces deployment.yaml            # evaluate every Rego namespace, not just `main`
```

## Writing a policy

```rego
# policy/deployment.rego
package main

deny[msg] if {
    input.kind == "Deployment"
    not input.spec.template.spec.securityContext.runAsNonRoot
    msg := sprintf("Deployment %s must set runAsNonRoot", [input.metadata.name])
}

warn[msg] if {
    input.kind == "Deployment"
    not input.spec.template.spec.containers[_].resources.limits
    msg := sprintf("Deployment %s has a container with no resource limits", [input.metadata.name])
}
```

`deny` rules are hard failures; `warn` rules are reported but don't fail the build unless `--fail-on-warn` is
set — this split is the mechanism most Conftest policy sets use to separate "must block the merge" from
"flag for review."

## Loading supporting data

```bash
conftest test --data policy/exceptions deployment.yaml
# a policy/exceptions/allowed_images.yaml file becomes available in Rego as `data.exceptions`
```

```yaml
# policy/exceptions/allowed_images.yaml
allowed_registries:
  - "registry.internal.example.com"
```

```rego
deny[msg] if {
    input.kind == "Pod"
    image := input.spec.containers[_].image
    not startswith_any(image, data.exceptions.allowed_registries)
    msg := sprintf("image %s is not from an approved registry", [image])
}
```

## Output formats

```bash
conftest test -o table k8s/deployment.yaml
conftest test -o json k8s/
conftest test -o junit k8s/ > results.xml
conftest test -o sarif k8s/                     # for GitHub code scanning
```

Sample table output (real shape, from the tool's own documentation example):

```
+---------+----------------------------------+--------------------------------+
| RESULT  |               FILE               |            MESSAGE             |
+---------+----------------------------------+--------------------------------+
| success | examples/kubernetes/service.yaml |                                |
| warning | examples/kubernetes/service.yaml | Found service hello-kubernetes |
|         |                                  | but services are not allowed   |
+---------+----------------------------------+--------------------------------+
```

## CI gating behavior

```bash
conftest test k8s/                              # default: non-zero exit on any failure/warning
conftest test k8s/ --fail-on-warn                # also fail on warnings (deny is already a hard fail)
conftest test k8s/ --no-fail                     # always exit 0 — reporting-only mode
```

> [!NOTE]
> Rego policies distinguish `deny` rules (hard fail, always non-zero exit) from `warn` rules (reported, but
> doesn't fail the build unless `--fail-on-warn` is set). This deny/warn split is the standard way most real
> Conftest policy bundles express severity, and is a cleaner mental model than remembering per-tool severity
> flag conventions the way you would for tfsec/Checkov/Trivy.

## Debugging a policy 🔍

```bash
conftest test --trace k8s/deployment.yaml                              # verbose Rego evaluation trace to stderr
conftest test --trace --output=json k8s/deployment.yaml 2>trace.log     # keep trace and formatted output separate
conftest parse k8s/deployment.yaml                                       # show the structured data Conftest extracted from the file, before any policy runs
```

> [!TIP]
> **`conftest parse` is the first thing to run when a policy "isn't matching" anything.** It shows exactly
> what structured data Conftest handed to Rego — a policy written against `input.spec.containers` fails
> silently (not with an error) if the actual parsed structure nests things differently than expected, and
> `parse` is how you find that out in seconds instead of guessing.

## Sharing policy bundles

```bash
conftest pull oci://ghcr.io/org/policies:latest      # download policies from an OCI registry
conftest push oci://ghcr.io/org/policies:latest       # publish your policy/ directory as an OCI artifact
conftest test --update oci://ghcr.io/org/policies k8s/    # fetch the latest policy bundle, then test in one step
```

This is the mechanism a platform team uses to centrally publish and version an org-wide policy set that every
application repo's CI pulls the latest version of, rather than each repo vendoring its own copy of the Rego
files (which drifts).

## Real-world scenario: enforcing a platform-wide "no privileged containers" rule

A platform team wants every Kubernetes manifest across dozens of repos to fail CI if it requests a privileged
container, with the policy centrally owned and updated in one place:

```rego
# policy/security.rego
package main

deny[msg] if {
    input.kind == "Pod"
    input.spec.containers[_].securityContext.privileged == true
    msg := "privileged containers are not permitted on this platform"
}
```

```bash
conftest push oci://ghcr.io/platform-team/k8s-policies:latest
# in every application repo's CI:
conftest test --update oci://ghcr.io/platform-team/k8s-policies k8s/
```

A policy update the platform team makes (tightening or relaxing a rule) takes effect on every consuming
repo's next CI run automatically — no PR needed against each individual repo.

## Real-world scenario: GitHub Actions CI recipe

```yaml
# .github/workflows/conftest.yml
name: Conftest
on: [pull_request]
jobs:
  policy-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: instrumenta/conftest-action@master
        with:
          files: k8s/
          policy: policy/
```

## Common pitfalls

- **A policy silently matching nothing** — see the `conftest parse` TIP above; this is by far the most common
  "why isn't my policy firing" report.
- **Forgetting `--fail-on-warn`** when a `warn` rule was actually meant to block merges.
- **Vendoring policy files per-repo instead of using `push`/`pull`** — makes a centrally-owned policy change
  require touching every consuming repo instead of taking effect automatically on next scan.

## Exit codes

`0` all tests pass (or `--no-fail` set) · non-zero when a `deny` fires, or a `warn` fires with
`--fail-on-warn`.

## When to reach for something else

For hand-authoring or unit-testing the underlying Rego itself (not just running it against config files), use
`opa test`/`opa eval` directly — see the OPA cheat sheet. For Kubernetes-native admission control (rejecting a
resource at `kubectl apply` time, not just in CI), the same Rego policies typically get wrapped in a
Gatekeeper `ConstraintTemplate` rather than run through Conftest.
