Featured image of post Enforcing Policy-as-Code Compliance in CI with OPA and Conftest

Enforcing Policy-as-Code Compliance in CI with OPA and Conftest

Implement a Policy-as-Code stage in your CI pipeline using Conftest and Open Policy Agent (OPA) to enforce compliance rules on headers, metadata, or config files, blocking merges on policy violations.

This post is part of the DevSecOps Principles series.

Introduction

In the first article, we built a modern CI pipeline for a Hugo site using GitHub Actions. Then, in the second article, we enhanced it with dynamic security testing using OWASP ZAP, adding a runtime security gate to prevent high-severity vulnerabilities from reaching production.

Now, we take the next step in our DevSecOps journey by introducing Policy-as-Code: a method to enforce compliance and governance rules directly within our pipeline using Open Policy Agent (OPA) and Conftest.

Enterprise CI/CD systems often need more than just security scans. They enforce internal policies, like checking for mandatory headers, metadata, or configuration patterns. With Conftest, we codify those rules and run them locally in CI, without relying on external services.

In this article, we:

  • Define lightweight .rego rules (e.g. “Referrer-Policy must exist”)

  • Extract headers or site metadata using curl or httpie

  • Convert the data to JSON and run conftest against it

  • Fail the pipeline if any policy is violated

This approach ensures that compliance is not just a checklist, but an automated and enforced part of your deployment lifecycle, a core principle of DevSecOps, and reflects how real-world engineering teams enforce internal technical standards early and automatically in their software lifecycle.

This article continues from the previous two. Make sure you have a working CI pipeline and dynamic scan stage before proceeding. No external OPA installation is required, everything runs as a local CI job.

What is Conftest?

Conftest is a lightweight command-line tool that uses OPA to test structured files (JSON/YAML) against policies. Unlike OWASP ZAP, it doesn’t scan the live application but rather enforces structure-level compliance.

It is often used to validate:

  • Kubernetes YAMLs

  • Terraform configs

  • Dockerfiles

  • And in this case, static site headers or metadata

It runs completely locally and therefore doesn’t make network request, making it ideal for early-stage CI enforcement.

Pipeline Enhacement: Compliance Check Stage in the Pipeline (Post-ZAP)

To operationalize our Policy-as-Code strategy, we’ll now extend the existing GitHub Actions workflow by adding a new compliance job. This stage runs after the live preview is built, and performs the following tasks:

Step Description
Define Policies Use .rego files to declare simple rules like: “The Referrer-Policy header must exist”, or “Homepage must have a <title> tag”.
Extract Input Data Use tools like curl or httpie to make a request to the preview site and extract the relevant data (headers, body).
Convert to JSON or YAML Save the data as site.json, which becomes input for Conftest.
Run Conftest Locally Use conftest to test the input against your .rego policies, locally and without hitting any external services.
Fail on Policy Violation If any policy fails, the pipeline blocks the merge, just like a governance gate in enterprise CI pipelines.

This stage acts as a compliance gate, complementing the quality and security gates already implemented in previous articles.

In the next section, we’ll walk through the actual implementation in GitHub Actions using a dedicated job to fetch headers, validate them, and stop the pipeline if any check fails. This completes the full DevSecOps flow:

Build Quality → Security Scan → Compliance Enforcement

GitHub Actions Implementation

Step 1 — Extend Your ci.yml with a Compliance Job

First, you’ll define a new job called conftest-check after the OWASP ZAP stage to your existing ci.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
conftest-check:
  name: Run Policy-as-Code Checks with Conftest
  runs-on: ubuntu-latest
  needs: owasp-zap  # Ensure ZAP has completed before running

  steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Start Hugo preview with Nginx
      run: |
        docker run -d --rm -p 8080:80 -v $(pwd)/public:/usr/share/nginx/html nginx
        sleep 5

    - name: Install Conftest
      run: |
        wget https://github.com/open-policy-agent/conftest/releases/download/v0.45.0/conftest_0.45.0_Linux_x86_64.tar.gz
        tar -xzf conftest_0.45.0_Linux_x86_64.tar.gz
        sudo mv conftest /usr/local/bin/

    - name: Extract headers using curl
      run: |
        curl -s -D - http://localhost:8080 -o /dev/null \
        | awk 'NF' \
        | awk -F: 'NR>1{gsub(/^[ \t]+/, "", $2); printf "\"%s\": \"%s\",\n", $1, $2}' \
        | sed '$ s/,$//' \
        | awk 'BEGIN { print "{\n\"headers\": {" } { print } END { print "}\n}" }' \
        > site_headers.json

    - name: Run Conftest policies
      run: conftest test site_headers.json
  • checkout code

    • Retrieves the repository so we can access the Hugo output and .rego policy files
  • Start Hugo preview with Nginx

    • Serves the static site locally at http://localhost:8080 so we can inspect headers
  • Install Conftest

    • Downloads the latest Conftest binary and installs it into the runner environment
  • Extract headers using curl

    • Uses curl and awk to convert HTTP response headers into structured JSON.
  • Run Conftest policies

    • Validates the extracted headers against defined .rego rules. Fails on violations.

With the compliance job running in CI, let’s now look at how to organize and maintain your .rego policies for clarity, scalability, and reuse across projects.

Step 2 — Write Your .rego Policies

Policies live under a policy/ directory (default for Conftest).

Example: Check for Referrer-Policy header policy/headers.rego:

1
2
3
4
5
6
package site.headers

deny[msg] {
  not input["Referrer-Policy"]
  msg = "Missing Referrer-Policy header"
}

Command in CI:

1
conftest test site_headers.json

➡️ This checks that the required header is present in the JSON structure extracted earlier.

If the header is present, the test passes. If not, the pipeline fails and the PR is blocked.

Step 3 — Input File Generation (Handled Automatically in CI)

In our workflow, we use curl and some lightweight shell scripting to fetch the site’s headers and convert them into a structured file called site_headers.json. This file wraps the headers under a headers key and serves as input for Conftest policy checks.

1
2
3
4
# Simplified representation of what happens in CI:
curl -s -D - http://localhost:8080 -o /dev/null \
| ... # format into headers JSON
> site_headers.json

You don’t need to generate this file manually as it’s created automatically in your GitHub Actions pipeline before the policy check step.

Step 4 — Test Your Policies Locally Before CI

To ensure your .rego works properly, you can write unit tests using OPA’s native test framework.

Create a separate headers_test.rego file next to your policy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package site.headers

test_referrer_policy_missing {
  not deny with input as {
    "headers": {
      "Content-Type": "text/html"
    }
  } == []
}

test_referrer_policy_present {
  deny with input as {
    "headers": {
      "Referrer-Policy": "no-referrer"
    }
  } == []
}

Then run:

1
opa test policy/

Benefits of Testing .rego Policies

  • Validates that rules catch what they should (and only what they should)
  • Prevents accidental rule breakage as you update logic
  • Enables test-driven development (TDD) for compliance rules

Step 5 — Customize Policy Output (Optional)

You can customize the failure message for easier CI debugging:

1
msg = sprintf("Policy violation: missing %q header", ["Referrer-Policy"])

Step 6 — Upload Results as GitHub Artifacts (Optional)

Just like ZAP, you can upload the site_headers.json used for debugging.

Just add this at the end of your conftest-check job:

uses: actions/upload-artifact@v4 with: name: site-headers-json path: site_headers.json

1
2
3
4
5
6
7
8
9

# Conclusion


I highly encourage you to integrate Conftest into your CI pipeline. It’s lightweight, fast, and brings governance rules into code, where they belong.

By automating policy checks, you reduce the risk of human error and align your workflow with how compliance is enforced in real-world engineering teams. It’s a small addition that makes a big difference in delivering secure and standards-compliant software.

Thanks for reading, and here’s to making DevOps not just faster, but more accountable.