Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCXVI · Governance Without BureaucracyPolicyAsCode

Policy as code — OPA, Conftest, and Kyverno

Advanced⏱ ~26 mingitconftestopa

What you'll learn

  • Explain what policy as code is and why it is a structural control
  • Distinguish the three engines - OPA/Rego, Conftest, Kyverno - and the artefact each evaluates
  • Write a simple policy, run it locally with conftest test or opa eval, and read the verdict
  • Wire a policy-as-code check into CI as a structural gate on the merge button

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Policy as code is the structural control for mechanical governance losses. The three engines an infrastructure team will encounter are OPA (general-purpose, Rego), Conftest (OPA-backed, manifest-oriented), and Kyverno (Kubernetes-native, admission controller). This lesson teaches the three engines, the policy syntax for each, and the wiring into CI that turns a policy into a structural gate.

What policy as code is

Policy as code is the encoding of a governance rule as an executable artefact. The artefact is a file in the repository. The artefact is reviewed like any other code: it has an author, a reviewer, a CI check, a version, a changelog. The artefact’s behaviour is deterministic given an input; the verdict is a boolean.

The properties this gives the governance programme:

  • Testable. The policy can be evaluated against known inputs; the verdict can be asserted in a unit test. A policy without tests is a policy whose behaviour is unknown.
  • Reviewable. A change to the policy is a pull request. The change is read by a human who is not the author. The change is recorded in Git history.
  • Versioned. A policy at tag v1.4.0 is the policy that ran in March. A policy at tag v1.5.0 is the policy that ran in April. The change is reproducible.
  • Auditable. A verdict on a manifest at a SHA is stored in CI logs. The manifest, the policy, and the verdict can be reconstructed six months later.
flowchart LR
    P["Policy file:\nrego / conftest / kyverno yaml"] --> V["Versioned in repo:\ntag, signed"]
    I["Input:\nmanifest / request"] --> E["Engine:\nOPA / Conftest / Kyverno"]
    V --> E
    E --> R["Verdict:\nallow / deny / warn"]
    R --> CI["CI gate:\nmerge blocked on deny"]

A policy-as-code control is the canonical structural control: the system evaluates the rule; the engineer cannot bypass by forgetting; the bypass (an emergency exception) is auditable and rare.

OPA and Rego

OPA (Open Policy Agent) is the general-purpose engine. Its language is Rego. Rego is a declarative language over structured data: the input is a JSON document, the policy declares rules, and the engine evaluates a query. The canonical query is data.<package>.allow and the verdict is a boolean.

A Rego policy is a small file. Example:

package policy

default allow = false

allow {
    input.kind == "Deployment"
    input.spec.template.spec.containers[_].securityContext.privileged == false
}

Run the policy locally:

opa eval -d policy.rego -i input.json "data.policy.allow"

The command reads the policy from policy.rego, the input from input.json, evaluates the query, and prints the verdict. A true result means the input is allowed; a false (or undefined) result means denied.

OPA is general-purpose: it evaluates any structured input. The same engine can evaluate Kubernetes manifests, Terraform plans, CI workflow files, IAM policies, and admission requests. The cost of the generality is the verbosity of the policy: the engineer must encode the schema in Rego.

Conftest

Conftest is OPA-backed, oriented to configuration files. The input is a manifest (YAML, JSON, TOML, HCL). The policies are Rego. The command runs the policies against the inputs in one pass.

conftest test --policy policy/ manifests.yaml

The command reads the policies from the policy/ directory, the manifests from manifests.yaml, and prints the verdicts. A failure (deny rule, or missing allow rule) exits non-zero; CI fails.

Conftest is the right engine for the policy-as-code gate in a CI pipeline for non-Kubernetes configuration: Terraform plans (after terraform show -json), Ansible playbooks, GitHub Actions workflows, Dockerfiles parsed as structured data, IAM JSON. The orientation to files rather than admission requests keeps the engine narrow and the policies small.

Kyverno

Kyverno is Kubernetes-native. Policies are YAML documents, not Rego. The engine runs as an admission controller in the cluster (mutating or validating webhook) and as a CLI for local CI evaluation.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged
spec:
  validationFailureAction: Enforce
  rules:
    - name: deny-privileged
      match:
        resources:
          kinds: ["Pod"]
      validate:
        message: "Privileged containers are forbidden"
        pattern:
          spec:
            containers:
              - securityContext:
                  privileged: "false|nil"

Kyverno’s narrowness is its strength: the policy is YAML, the engine speaks Kubernetes, the failure mode is the cluster refusing the manifest. The narrowness is also the limit: Kyverno evaluates Kubernetes objects, not Terraform plans or Ansible playbooks. The team needs Conftest (or OPA) for the non-Kubernetes inputs.

flowchart TB
    subgraph CI["CI pipeline"]
        CT["Conftest:\nmanifests / tfplan / workflows"]
        OP["OPA:\ngeneral-purpose eval"]
    end
    subgraph Cluster["Kubernetes cluster"]
        KY["Kyverno:\nadmission controller"]
    end
    CI -->|"merge gate"| MERGE["Merge to default branch"]
    Cluster -->|"admit gate"| DEPLOY["Deploy to cluster"]

Wiring policy as code into CI

A policy is not a structural control until it is wired into a gate. The wiring has four steps:

  1. The policy file is committed. Under policy/ or policies/. Reviewed like any other code.
  2. The policy is unit-tested. Known inputs produce known verdicts. The test runs in CI.
  3. The CI step runs the engine against the diff. conftest test --policy policy/ manifests.yaml against every changed file; kyverno apply against the manifest set. The step exits non-zero on deny.
  4. The branch-protection rule requires the step. The merge button is disabled until the policy check passes.
flowchart LR
    P["policy.rego\n(in repo, reviewed)"] --> U["unit tests"]
    U --> CI["CI step:\nconftest test / kyverno apply"]
    CI -->|"deny"| RED["Build red:\nmerge blocked"]
    CI -->|"allow"| GREEN["Merge allowed"]

The branch-protection rule is the structural part: the rule requires the policy step; the engineer cannot bypass the policy by clicking through. The policy is a control; the branch-protection rule makes the control structural.

Production discipline

  1. Pick the engine by the input. Kyverno for in-cluster admission; Conftest for manifest CI; OPA for general-purpose structured inputs.
  2. Test the policy. A policy without unit tests is a policy whose behaviour is unknown.
  3. Require the policy in branch protection. The structural gate is the branch-protection rule, not the policy file alone.
  4. Track verdicts. The false-positive rate and the exemption count are the leading indicators of a policy drifting into bureaucracy.

Cross-course references

  • This course, Part CIX-04 covers the tflint-and-fmt-deep-in-ci Terraform checks that pair with policy as code for Terraform plans.
  • Terraform for Production Sysadmins - Part XII (State) covers the OPA evaluation of Terraform plans for S3 bucket encryption and IAM least-privilege.
  • Kubernetes for Production Sysadmins - Part XXVI (AdmissionControl) covers Kyverno as the in-cluster admission controller.

Quiz

Knowledge check · 4 questions

  1. Q1. A team writes a Kyverno policy that refuses privileged containers, commits the YAML, but does not add a branch-protection rule. What does the policy prevent?

  2. Q2. Conftest is appropriate for evaluating Kubernetes manifests in a CI pipeline because it runs Rego policies against structured configuration files without requiring a running cluster.

  3. Q3. Write the shell command that runs a Conftest policy directory against a Kubernetes manifest file and exits non-zero on policy failure.

  4. Q4. Design the policy-as-code wiring for the team, choose the engine for each input, and identify the structural gaps.

    Team T operates 40 microservices. The team uses Kubernetes for workloads, Terraform for cloud infrastructure, and GitHub Actions for CI. Governance asks for four rules: (a) no privileged containers; (b) all S3 buckets encrypted; (c) all IAM roles have a description; (d) no `latest` tag on container images. The team has no policy as code today. The CI pipeline runs lint, test, build, and deploy steps for every pull request.

Passing score: 75%. Answers are checked in this browser.