Git, CI/CD & GitOpsLII · Kubernetes CIPolicy
Policy with Conftest and Kyverno — Rego and CEL, and what policy catches
What you'll learn
- Run conftest test against rendered manifests and read the output
- Distinguish Rego policies (OPA, Conftest) from CEL policies (Kyverno)
- Identify the categories of misconfiguration policy catches that schema validation misses
- Recognise the boundary between CI policy and admission-time policy
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 validation catches the class of error that schema validation cannot: organisational rules that are not part of the Kubernetes API contract. A Deployment with apiVersion: apps/v1, three replicas, and a valid selector passes kubeconform; the same Deployment with no app.kubernetes.io/owner label, no resource limits, and a latest image tag is organisationally wrong but schema-correct. Policy is the layer that fails the second Deployment. Conftest and Kyverno are the two canonical tools, and they implement policy in two different languages: Rego and CEL.
What policy catches
The Kubernetes OpenAPI schema encodes the API server’s contract: field types, required fields, enum values. Anything not in the schema is outside the schema’s authority. The categories of organisational rule the schema does not encode:
- Required labels.
app.kubernetes.io/name,app.kubernetes.io/owner,app.kubernetes.io/part-of. The schema does not require them; the organisation does. - Forbidden fields.
hostNetwork: true,hostPID: true,imagePullPolicy: Always,image: ${NAME}:latest. The schema accepts them; the organisation forbids them. - Wildcard RBAC.
verbs: ["*"],resources: ["*"],apiGroups: ["*"]. The schema accepts them; the organisation forbids them because they violate least privilege. - Resource bounds. No CPU limits, no memory limits, no
requests. The schema accepts them; the organisation requires them because unbounded workloads are a noisy-neighbour risk. - Image provenance. References to images from unapproved registries, references to images without digest pinning. The schema accepts them; the organisation requires provenance.
These are not schema errors; they are policy errors. A tool that reads the schema cannot catch them. A tool that reads policy can.
Conftest with Rego
Conftest is the canonical CI-side policy tool. It reads Rego policies (the same language OPA uses) and applies them to a structured input — most commonly rendered Kubernetes manifests:
conftest test --policy policy/ manifests.yaml
The policies are Rego files in a policy/ directory. A policy that fails any resource produces a non-zero exit code and a list of violations:
package main
deny[msg] {
input.kind == "Deployment"
not input.metadata.labels["app.kubernetes.io/owner"]
msg := sprintf("Deployment %s is missing label app.kubernetes.io/owner", [input.metadata.name])
}
The output, when a Deployment is missing the label:
FAIL - manifests.yaml - Deployment my-app - Deployment my-app is missing label app.kubernetes.io/owner
1 test, 0 passed, 1 failed
Conftest’s strength is the policy language. Rego is a purpose-built DSL for declarative rules over structured data; the policies are easy to read, easy to test, and easy to version-control. The cost is the learning curve: Rego is not a general-purpose language, and the policy author must learn the data model OPA uses for Kubernetes resources.
Kyverno with CEL
Kyverno is the canonical admission-time policy tool. It runs inside the cluster as a webhook and applies policies to every resource that the API server is about to admit. The policies are written in CEL (Common Expression Language), the same expression language the API server uses for validation rules:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-owner-label
spec:
validationFailureAction: Enforce
rules:
- name: require-owner-label
match:
resources:
kinds:
- Deployment
validate:
message: "Deployment must have label app.kubernetes.io/owner"
cel:
expressions:
- expression: "has(object.metadata.labels['app.kubernetes.io/owner'])"
The policy is a Kubernetes resource itself. The validate.cel.expressions field is the CEL expression that must evaluate to true for the resource to be admitted. The validationFailureAction: Enforce field means a failure blocks the admission; Audit means the failure is logged but the admission proceeds.
Kyverno can also run in CI with kyverno apply, which evaluates the policies against rendered manifests without contacting the cluster:
kustomize build overlays/prod | kyverno apply policies/
The CI-side application produces the same violations as the admission-time application for the same input. The CI check fails the pipeline before the admission check has a chance to run.
Rego versus CEL
The two languages serve the same purpose but differ in shape:
| Aspect | Rego (Conftest, OPA) | CEL (Kyverno, API server) |
|---|---|---|
| Where it runs | CI (Conftest) or admission (OPA Gatekeeper) | Admission (Kyverno) or CI (kyverno apply) |
| Author surface | Dedicated DSL with rules and helpers | Expression language, embedded in YAML |
| Data model | OPA’s input model | Kubernetes object model |
| Best for | Complex multi-resource rules, reusable policy libraries | Single-resource rules, parity with admission constraints |
| Cost | Learning curve for Rego | CEL is simpler; fewer constructs than Rego |
The choice is rarely either-or. Most production pipelines run Conftest at CI for organisational policy and Kyverno at admission for cluster-level constraints. The two compose because the input is the same parsed manifest.
flowchart LR
A["Rendered manifests"] --> B["kubeconform\n(schema)"]
A --> C["conftest\n(Rego)"]
A --> D["kyverno apply\n(CEL)"]
B --> E["CI pipeline"]
C --> E
D --> E
E --> F["GitOps controller"]
F --> G["Kyverno admission"]
Production discipline
- Run conftest and kubeconform in the same pipeline. Schema validation and policy validation are independent; both must pass.
- Use
kyverno applyfor CI parity with admission. The same policies that run at admission can run at CI, so the pipeline catches what admission would catch. - Version-control policies in the repository. A policy file in
policy/is reviewable, testable, and auditable; a policy file in a wiki is none of these. - Test policies. Conftest ships with a test harness (
conftest verify); Kyverno ships with akyverno testsubcommand. A policy that has never been tested is a policy that has never been run.
Cross-course references
- Kubernetes for Production Sysadmins - Part XXXIII (Admission) covers OPA Gatekeeper and Kyverno at admission time; this lesson covers the CI-side application of the same policies.
- Kubernetes for Production Sysadmins - Part X (Manifests) covers the resource model the policies read.
- This course, Part LII-03 (TemplateValidation) - the render step that produces the input to the policy check.
Quiz
Knowledge check · 4 questions
Q1. A Deployment with three replicas and a valid selector passes kubeconform but is missing the `app.kubernetes.io/owner` label required by the team. Which tool catches this?
Q2. Running Kyverno as an admission controller in the cluster makes CI-side policy with Conftest redundant, because every admission will be checked.
Q3. Name three categories of organisational misconfiguration that policy validation catches and schema validation does not.
Q4. Diagnose a CI pipeline where policy validation is missing and a Deployment with wildcard RBAC reaches the cluster, and recommend the policy fix.
A team runs `kubeconform -strict` in CI but no policy tool. A contributor adds a ClusterRoleBinding that grants the `default` ServiceAccount in the `prod` namespace `verbs: ['*'], resources: ['*']`. kubeconform accepts the manifest because the schema permits wildcard fields. The merge proceeds; admission accepts the binding because no admission policy is configured; a compromised pod in the `prod` namespace now has cluster-wide RBAC.
Passing score: 75%. Answers are checked in this browser.