Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Policy as Code with OPA and Conftest

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.

Policy as Code with OPA and ConftestConfig FilesTF, K8s, DockerConftestCLI runnerOPA EngineRego evalCI GatePass / FailRego Policy Repository (Git)SecurityNamingCost TagsVersioned, reviewed, tested like application code
Policy as Code with OPA and Conftest: static config flows through Rego rules in CI before anything reaches production.

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 .rego files, 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.

OPA Rego Evaluation ModelInput JSONTF plan, K8s YAMLRego Rulesdeny, warn, allowQuery ResultSet of messagesALLOW — empty deny setDENY — messages returnedConftest fails CI when any deny rule produces a message
OPA evaluates input against Rego deny rules; Conftest treats a non-empty deny set as a CI failure.

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.

CI Pipeline with Conftest GateLintUnit TestsConftestPolicy GateBuildDeployBlock MergePolicy runs before build artifacts reach production servers
Place Conftest after unit tests and before deploy so policy violations block merges without wasting build time.

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.

  1. Week 1: warn only, collect violation metrics.
  2. Week 2–3: fix existing repos or grant exceptions with expiry dates.
  3. 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.

ToolWhen it runsBest forLimitation
ConftestCI, local dev, pre-commitTerraform, Docker, YAML, JSON, HCLDoes not block live API calls
OPA GatekeeperKubernetes admission webhookCluster-wide enforcement, audit modeKubernetes only; needs cluster admin
KyvernoKubernetes admission + mutateKubernetes-native YAML policies, mutationsLess suited for Terraform plans
Embedded OPARuntime in your app or sidecarMicroservice authz, custom APIsRequires 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.

Where to Enforce Policy?What are you protecting?Static filesTF, Docker, YAMLK8s clusterLive API callsApp runtimeUser requestsConftestGatekeeperEmbedded OPAUse Conftest plus admission control for layered Policy as Code
Choose Conftest for static config in CI; use Gatekeeper or embedded OPA for runtime enforcement.

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 deny rule pattern in Conftest, export Terraform plans as JSON, and run opa test on every policy change.
  • Place Conftest in CI after unit tests and before deploy; use warn first, then promote rules to deny.
  • 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

Policy as Code means writing Rego rules that evaluate JSON or YAML config in CI. Conftest runs those rules against Terraform plans, Kubernetes manifests, and Dockerfiles, failing the build when a policy violation is found.

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 admission. Conftest is the simplest path for CI and local pre-commit checks on static config.

On Ubuntu 22 or 24, download the release binaries for your architecture and place them on PATH, or install via a package manager. Pin versions in your CI image so local and pipeline behaviour match. Verify with opa version and conftest --version before wiring either tool into GitLab CI or GitHub Actions. Matching versions across developer laptops and runners prevents policies that pass locally but fail mysteriously in the pipeline.

Start with one rule you already enforce manually—no latest image tags, no missing resource limits, or no public S3 ACLs. Write a failing test first, then the policy that makes it pass. Conftest expects policies under package main or a named namespace, using the deny rule set pattern where each deny[msg] block adds a violation message. Keep policies in a dedicated policies/ directory with subfolders per domain such as kubernetes/, terraform/, and docker/, plus testdata/ fixtures for good and bad examples.

Export the plan as JSON and point Conftest at that artifact. Run terraform plan -out=tfplan.binary, then terraform show -json tfplan.binary to produce tfplan.json. Run conftest test tfplan.json --policy policies/terraform/. Rego policies inspect input.resource_changes for fields like resource type and change.after values—for example, denying aws_s3_bucket resources with acl set to public-read. Validate the JSON structure while learning the plan schema, because provider upgrades can shift field names slightly between versions.

Add a dedicated policy stage before deploy, mirroring how code coverage gates work. Use the openpolicyagent/conftest:latest image with entrypoint cleared, then run conftest test against your manifest directory or Terraform plan JSON. Place Conftest after unit tests and before deploy so violations block merges without wasting deploy time. Use --output github or --output junit so violations appear as merge request annotations rather than buried log lines developers will ignore.

Use the openpolicyagent/conftest-action, pinning a specific version such as v0.56.0 rather than floating latest. Pass the files directory and policy directory as inputs, and set fail-on-warn to true if warnings should also fail the job. This mirrors the GitLab CI pattern: evaluate Kubernetes YAML or rendered Helm output against your Rego bundle and block the pull request when deny rules fire. Pinning the action version keeps policy evaluation consistent across branches and forks.

Conftest runs in CI, local dev, and pre-commit—it is best for Terraform, Docker, YAML, and JSON static analysis but does not block live Kubernetes API calls. OPA Gatekeeper enforces at the admission webhook layer cluster-wide. Kyverno handles Kubernetes-native YAML policies and mutations at admission time. Use Conftest to catch mistakes before merge; pair it with Gatekeeper or Kyverno to catch manual kubectl, compromised tokens, or emergency hotfixes that bypass CI. For multi-cloud Terraform governance, Conftest shines because plan output is JSON-shaped across AWS, Azure, and GCP.

Yes. Conftest parses Dockerfiles natively and evaluates each instruction as input, so policies under policies/docker/ can flag risky patterns before images are built. For Helm, render charts to YAML with helm template first, then run conftest test on the output directory against policies/kubernetes/. Multi-document YAML files may need the --combine flag or splitting into separate files. This catches misconfigurations in container definitions and rendered manifests before anything reaches a cluster.

Yes, but you can start with community policy libraries and grow from there. Rego syntax becomes natural after a few policies, especially if you use OPA's playground and opa test on every change. Many teams assign one engineer to own the shared policy repo while application teams consume it through CI without writing Rego daily.

Sentinel is HashiCorp's policy language tied to Terraform Cloud and Enterprise. Kyverno uses Kubernetes-style YAML for cluster admission policies and supports mutations. OPA and Rego are open source and tool-agnostic—Conftest applies the same Rego policies to Terraform plans, Kubernetes manifests, Dockerfiles, and more without vendor lock-in. Choose Sentinel if your organisation is all-in on HashiCorp's paid stack; choose Kyverno for Kubernetes-only admission with YAML-native authoring; choose OPA and Conftest when you need one policy language across CI and multiple config formats.

Laravel policies and gates decide whether an authenticated user can edit a specific database record at the application layer. OPA decides whether a deployment artifact—Terraform plan, Kubernetes manifest, Dockerfile—meets organisational standards before it reaches production. Both are called policy, but they operate at different layers. On production Laravel deployment pipelines I maintain with Deployer 7 and GitLab CI, Conftest sits alongside linters and unit tests as a static gate on infrastructure config, not on user permissions inside the app.

OPA includes a test runner—put tests in _test.rego files alongside your policies and run opa test policies/kubernetes/ -v. Tests use the with input as syntax to assert specific deny messages fire on bad fixtures. Build a library of good and bad examples under testdata/ and run opa test on every policy change. It is faster than spinning up Conftest against fixture files and catches regressions before they reach the merge queue. Treat a broken opa test like a broken unit test.

Teams often write policies without fixture tests, making every Rego change manual trial and error. Launching too many deny rules on day one blocks the merge queue—start with three high-impact checks such as no public storage, no privileged containers, and required cost tags. Ignoring Terraform plan format drift after provider upgrades breaks policies silently. Copy-pasting Rego across fifteen repos guarantees drift; centralise and version policies instead. Weak deny messages like violation found waste engineer time—include resource name, field path, and a remediation hint in every msg string.

Conftest supports warn rules in addition to deny. Use warnings during initial rollouts so teams see violations without blocked merges, then convert high-confidence rules to deny once adoption is high. A practical staged approach: week one warn only and collect violation metrics; weeks two and three fix existing repos or grant exceptions with expiry dates; week four flip proven rules to deny. This reduces backlash while still surfacing policy gaps. Pair CI-only Conftest with Kubernetes admission enforcement, because anyone with cluster credentials can bypass a CI gate entirely.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: