Git, CI/CD & GitOpsCXVI · Governance Without BureaucracyPolicyAsCode
Policy as code — OPA, Conftest, and Kyverno
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
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.0is the policy that ran in March. A policy at tagv1.5.0is 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:
- The policy file is committed. Under
policy/orpolicies/. Reviewed like any other code. - The policy is unit-tested. Known inputs produce known verdicts. The test runs in CI.
- The CI step runs the engine against the diff.
conftest test --policy policy/ manifests.yamlagainst every changed file;kyverno applyagainst the manifest set. The step exits non-zero on deny. - 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
- Pick the engine by the input. Kyverno for in-cluster admission; Conftest for manifest CI; OPA for general-purpose structured inputs.
- Test the policy. A policy without unit tests is a policy whose behaviour is unknown.
- Require the policy in branch protection. The structural gate is the branch-protection rule, not the policy file alone.
- 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-ciTerraform 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
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?
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.
Q3. Write the shell command that runs a Conftest policy directory against a Kubernetes manifest file and exits non-zero on policy failure.
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.