
September 10, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Your Terraform plan passes. Your Kubernetes manifest applies cleanly. Then production breaks because someone opened port 22 to the world or tagged a public S3 bucket wrong. Policy as Code with OPA and Conftest fixes that gap. You encode rules in the Open Policy Agent (OPA) Rego language and run them in CI with Conftest before merge. I use this pattern on Linux CI runners that deploy Laravel and infrastructure for client projects. The rest of this guide shows exactly how to set it up.
What is Policy as Code with OPA and Conftest?
Policy as Code treats compliance rules like application code. You version them in Git, review them in pull requests, and test them automatically. OPA is the general-purpose policy engine. Conftest is the CLI wrapper that makes OPA easy to use against static files.
OPA evaluates structured input—Terraform JSON plans, Kubernetes YAML, Dockerfile layers—and returns allow or deny decisions. Rego is OPA's declarative query language. Conftest bundles Rego policies into a folder, runs them against files you point at, and exits with a non-zero code on failure. That exit code is what your CI job reads.
This is different from application authorization. Laravel policies and gates decide whether a user can edit a record. OPA decides whether a deployment artifact meets organisational standards. Both are "policy," but they operate at different layers.
Core components you need to know
- OPA — the policy engine that evaluates Rego against JSON input. It runs as a binary, sidecar, or embedded library.
- Rego — the policy language. It uses partial definitions and set comprehensions rather than imperative if/else chains.
- Conftest — a thin CLI that loads policies from a directory and tests files or directories against them.
- Policy bundle — a folder of
.regofiles, often one package per domain (security, naming, tags).
The official OPA documentation is the authoritative reference for Rego syntax and built-in functions. Conftest adds convenience flags like --namespace, --output, and --policy that map directly to CI use cases.
How do you write your first Rego policy for Conftest?
Start small. Pick one rule you already enforce manually—no latest image tags, no missing resource limits, no public S3 ACLs. Write a failing test first, then the policy that makes it pass. That TDD loop works well for Rego.
Install OPA and Conftest
On Ubuntu 22 or 24, download the release binaries or use a package manager. Pin versions in your CI image so local and pipeline behaviour match.
# Install OPA (example: Linux amd64)
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64
chmod +x opa && sudo mv opa /usr/local/bin/
# Install Conftest
curl -L -o conftest.tar.gz \
https://github.com/open-policy-agent/conftest/releases/latest/download/conftest_Linux_x86_64.tar.gz
tar xzf conftest.tar.gz && sudo mv conftest /usr/local/bin/
opa version
conftest --version Project layout
Keep policies in a dedicated directory at the repo root or in a shared policy repo consumed by multiple teams.
policies/
kubernetes/
deployment.rego
terraform/
s3.rego
tags.rego
docker/
dockerfile.rego
testdata/
bad-deployment.yaml
good-deployment.yaml A Kubernetes deployment policy
Conftest expects policies under package main or a named namespace. Violations use the deny rule set pattern.
# policies/kubernetes/deployment.rego
package main
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
not container.resources.limits
msg := sprintf("Container '%s' must set resource limits", [container.name])
}
deny[msg] {
input.kind == "Deployment"
container := input.spec.template.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container '%s' must not use the latest tag", [container.name])
} Run Conftest against a manifest:
conftest test deploy/app.yaml --policy policies/kubernetes/ Conftest parses YAML into JSON before evaluation. OPA sees a single document as input. For multi-document YAML files, use conftest test with the --combine flag or split files.
Terraform plan policies
For Terraform infrastructure as code, export the plan as JSON and test that artifact. This catches misconfigurations before terraform apply.
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json --policy policies/terraform/ Example S3 public access denial:
# policies/terraform/s3.rego
package main
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket"
change := resource.change.after
change.acl == "public-read"
msg := sprintf("S3 bucket %s must not be public-read", [resource.address])
} Validate JSON structure with a JSON formatter while you learn the Terraform plan schema. The shape varies slightly between provider versions, so write policies against fields that are stable.
Testing policies locally
OPA includes a test runner. Put tests in _test.rego files alongside your policies.
# policies/kubernetes/deployment_test.rego
package main
test_deny_missing_limits {
deny["Container 'web' must set resource limits"] with input as {
"kind": "Deployment",
"spec": {"template": {"spec": {"containers": [{"name": "web"}]}}}
}
} opa test policies/kubernetes/ -v Run opa test on every policy change. It is faster than spinning up Conftest against fixture files and catches regressions early.
How do you integrate OPA and Conftest into a CI pipeline?
The value of Policy as Code with OPA and Conftest is enforcement at merge time. A policy that only runs on a laptop does not protect production. Wire Conftest into the same stage where you run linters and unit tests.
GitLab CI example
On projects I maintain with Deployer 7 and GitLab CI, I add a dedicated policy stage before deploy. The pattern mirrors code coverage gates in CI—fail fast, print clear messages, block merge.
# .gitlab-ci.yml
stages:
- test
- policy
- deploy
conftest-kubernetes:
stage: policy
image:
name: openpolicyagent/conftest:latest
entrypoint: [""]
script:
- conftest test k8s/ --policy policies/kubernetes/ --output github
conftest-terraform:
stage: policy
image:
name: openpolicyagent/conftest:latest
entrypoint: [""]
script:
- terraform init -backend=false
- terraform plan -out=tfplan.binary
- terraform show -json tfplan.binary > tfplan.json
- conftest test tfplan.json --policy policies/terraform/ Use --output github or --output junit so violations appear as annotations in GitHub Actions or GitLab merge request widgets. That visibility matters. Developers ignore logs they cannot find.
GitHub Actions example
- name: Run Conftest on Kubernetes manifests
uses: openpolicyagent/conftest-action@v0.56.0
with:
files: k8s/
policy: policies/kubernetes/
fail-on-warn: true Shared policy libraries
Large teams centralise policies in one repository and pin a Git submodule or copy step in CI. That mirrors Jenkins shared libraries for pipeline code. One team owns security baselines. Application repos consume them without duplicating Rego.
For multi-repo setups, publish policy bundles as OPA bundles (opa build) and pull them in CI. The Conftest project documentation covers remote policy sources and combine modes for monorepos.
Warn vs deny
Conftest supports warn rules in addition to deny. Use warnings during policy rollouts so teams see violations without blocked merges. Flip warnings to denials once adoption is high. This staged approach reduces backlash on day one.
- Week 1:
warnonly, collect violation metrics. - Week 2–3: fix existing repos or grant exceptions with expiry dates.
- Week 4: convert high-confidence rules to
deny.
When should you choose Conftest over OPA Gatekeeper or Kyverno?
Conftest is for pre-deploy static analysis. OPA Gatekeeper and Kyverno enforce policies at the Kubernetes API admission layer. You need both layers for defence in depth, but they solve different problems.
| Tool | When it runs | Best for | Limitation |
|---|---|---|---|
| Conftest | CI, local dev, pre-commit | Terraform, Docker, YAML, JSON, HCL | Does not block live API calls |
| OPA Gatekeeper | Kubernetes admission webhook | Cluster-wide enforcement, audit mode | Kubernetes only; needs cluster admin |
| Kyverno | Kubernetes admission + mutate | Kubernetes-native YAML policies, mutations | Less suited for Terraform plans |
| Embedded OPA | Runtime in your app or sidecar | Microservice authz, custom APIs | Requires application integration |
Read the full comparison in Kyverno vs OPA Gatekeeper for policy. My rule: Conftest in CI catches mistakes early. Gatekeeper catches anything that bypasses CI—manual kubectl, compromised tokens, emergency hotfixes.
For multi-cloud governance and policy as code, Conftest shines because Terraform and Pulumi outputs are JSON-shaped. One Rego library can cover AWS, Azure, and GCP resources if you normalise tags and naming in the plan JSON.
What are common mistakes when implementing Policy as Code with OPA and Conftest?
I've seen teams abandon OPA after a bad first rollout. The engine works. The failure is usually process, not Rego syntax.
Writing policies without fixture tests
Rego looks unfamiliar. Without opa test, every change becomes manual trial and error. Build a library of good and bad fixtures under testdata/. Run them in CI alongside application tests. Treat policy regressions as production bugs.
Policies that are too broad on day one
Blocking every non-standard label on week one creates merge queue chaos. Start with three high-impact rules: no public storage, no privileged containers, required cost tags. Expand from measured violation data, not a 200-line policy wish list.
Ignoring Terraform plan format drift
Provider upgrades change JSON field names. Pin provider versions in Terraform and re-run opa test after upgrades. For CloudFormation infrastructure as code on AWS or Bicep workflows, convert templates to JSON or use Conftest's HCL parser where supported.
Skipping admission-layer enforcement
CI-only policy is necessary but not sufficient. Anyone with cluster credentials can bypass it. Pair Conftest with Kubernetes security and network policies for runtime controls.
Duplicating logic across repos
Copy-pasting Rego into fifteen repositories guarantees drift. Centralise policies. Version them semantically. Let application repos pin policy-lib@v2.3.0 the same way they pin Composer packages.
Weak error messages
A deny message like "violation found" wastes engineer time. Include the resource name, field path, and remediation hint in every msg string. Good messages turn policy from friction into documentation.
On a production Laravel deployment pipeline, I treat Conftest like static code analysis in CI with Sonarqube. Same psychology applies. Developers accept gates when feedback is fast, accurate, and actionable.
Key Takeaways
- Policy as Code with OPA and Conftest encodes compliance rules in versioned Rego files tested against JSON and YAML config in CI.
- Use the
denyrule pattern in Conftest, export Terraform plans as JSON, and runopa teston every policy change. - Place Conftest in CI after unit tests and before deploy; use
warnfirst, then promote rules todeny. - Conftest covers static files; pair it with OPA Gatekeeper or Kyverno for Kubernetes admission enforcement.
- Centralise policies in a shared repo, write clear violation messages, and start with a small set of high-impact rules.
- Defence in depth beats any single tool—CI gates plus runtime admission plus app-level checks.
People Also Ask
Is Conftest the same as OPA?
No. OPA is the policy engine and Rego runtime. Conftest is a CLI built on OPA that tests files and directories against Rego policies. You can run OPA directly, embed it in services, or use Gatekeeper for Kubernetes. Conftest is the simplest path for CI and local pre-commit checks on static config.
Can Conftest test Dockerfiles and Helm charts?
Yes. Conftest parses Dockerfiles natively and evaluates each instruction as input. For Helm, render charts to YAML with helm template first, then run Conftest on the output directory. This catches misconfigurations before charts reach a cluster.
Do I need to learn Rego to use Conftest?
Yes, but you can start with community policy libraries. The Rego syntax becomes natural after a few policies. OPA's playground and opa test accelerate learning. Many teams assign one engineer to own the policy repo while others consume it through CI.
How does Policy as Code relate to Sentinel or Kyverno?
Sentinel is HashiCorp's policy language for Terraform Cloud and Enterprise. Kyverno uses Kubernetes-style YAML for cluster policies. OPA and Rego are open source and tool-agnostic. Conftest applies the same Rego policies to Terraform, Kubernetes, Docker, and more without vendor lock-in.
Ship policy gates before your next production incident
Policy as Code with OPA and Conftest turns "we should never deploy that" into an automated gate every merge request passes through. Start with one Terraform rule and one Kubernetes manifest check. Wire Conftest into CI this week. Expand from real violation data, not theoretical compliance decks.
If you want help hardening CI pipelines, deployment workflows, or infrastructure review for a Nepal or remote project, see testing and optimization services or review how we run production pipelines on Adventure Third Pole Trek. For broader platform work, explore custom software development or support and maintenance. Ready to talk through your stack? Contact us with your repo layout and CI tool—we will map the first three policies worth enforcing.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

