Policy as Code¶
Policy as Code (PaC) expresses security, compliance, and operational rules as machine-readable, version-controlled code that is automatically evaluated and enforced — rather than as documents humans are expected to remember and follow. This makes policy consistent, testable, auditable, and impossible to silently skip, which is exactly what a high-velocity DevSecOps pipeline needs.
Why policy as code¶
- Consistency — the same rule is enforced identically everywhere, every time, without relying on individual judgment or memory.
- Automation — policies run in CI and at admission without manual gatekeeping or review bottlenecks.
- Version control and review — policy changes go through pull requests with history, diff, and accountability. Rolling back a bad policy is a revert.
- Testability — policies can be unit-tested against example inputs before rollout, preventing both false positives and missed cases.
- Auditability — every decision is logged with the input, the policy version, and the outcome — producing structured evidence for Compliance Auditing.
- Scalability — one centrally maintained policy library can be distributed to thousands of pipelines, services, and clusters simultaneously.
Where policy as code is applied¶
- CI/CD gates — block merges or builds that violate policy (e.g., "no critical vulnerabilities", "SBOM required", "image must be signed"). Conftest tests structured config files (Kubernetes manifests, Terraform plans, Helm values) against Rego policies before they are applied.
- Admission control — Kubernetes admission controllers (OPA Gatekeeper, Kyverno, or the built-in CEL-based
ValidatingAdmissionPolicy, GA since Kubernetes 1.30) reject non-compliant or unsigned workloads at the point of deploy (see Deploy). - Infrastructure as Code — enforce guardrails on Terraform/Kubernetes before provisioning (see IaC Scanning).
- Cloud guardrails — preventive controls (Service Control Policies in AWS, Azure Policy, GCP Organization Policy) prevent forbidden actions from being performed at all.
- API authorization — OPA is commonly used as an external authorization engine to evaluate fine-grained access-control policies separately from application code.
Policy examples¶
Block :latest image tags¶
# deny_latest_tag.rego
package main
import rego.v1
deny contains msg if {
input.kind == "Deployment"
some container in input.spec.template.spec.containers
endswith(container.image, ":latest")
msg := sprintf("Container '%v' must not use the :latest tag — pin to a digest or versioned tag", [container.name])
}
Require runAsNonRoot¶
# require_non_root.rego
package main
import rego.v1
deny contains msg if {
input.kind == "Deployment"
some container in input.spec.template.spec.containers
not container.securityContext.runAsNonRoot
msg := sprintf("Container '%v' must set securityContext.runAsNonRoot: true", [container.name])
}
Require labels¶
# require_labels.rego
package main
import rego.v1
required_labels := {"app", "team", "env"}
deny contains msg if {
input.kind == "Deployment"
some label in required_labels
not input.metadata.labels[label]
msg := sprintf("Deployment is missing required label: '%v'", [label])
}
Require runAsNonRoot (Kyverno YAML alternative)¶
# kyverno-require-non-root.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-run-as-non-root
spec:
validationFailureAction: Enforce # use Audit first; newer Kyverno versions also support per-rule validate.failureAction
rules:
- name: check-runAsNonRoot
match:
resources:
kinds: ["Pod"]
validate:
message: "Containers must not run as root. Set runAsNonRoot: true in securityContext."
pattern:
spec:
containers:
- securityContext:
runAsNonRoot: true
OPA integration in CI — step by step¶
- Install Conftest in your CI image or as a binary download step (pin the version and verify the checksum published with the release):
CONFTEST_VERSION=<pinned-version> # see github.com/open-policy-agent/conftest/releases
wget https://github.com/open-policy-agent/conftest/releases/download/v${CONFTEST_VERSION}/conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz
tar xzf conftest_${CONFTEST_VERSION}_Linux_x86_64.tar.gz && mv conftest /usr/local/bin/
- Store policies in a
policy/directory in your repo (or reference a shared OCI bundle):
- Run in CI against generated Kubernetes manifests before applying:
-
Interpret results — conftest exits non-zero if any
denyrule fires. CI blocks the job and surfaces the message to the developer. -
Promote to enforcement — roll out first in non-blocking mode (
conftest test --no-fail, or usewarnrules instead ofdeny). Once the policy has run for two sprints without significant false positives, remove--no-fail(or promotewarnrules todeny) so violations block the pipeline.
Testing policies¶
A policy that is not tested is a policy that may silently fail. Use OPA's built-in test framework:
# deny_latest_tag_test.rego
package main
import rego.v1
test_deny_latest if {
deny["Container 'app' must not use the :latest tag — pin to a digest or versioned tag"] with input as {
"kind": "Deployment",
"spec": {"template": {"spec": {"containers": [{"name": "app", "image": "myrepo/myimage:latest"}]}}}
}
}
test_allow_pinned if {
count(deny) == 0 with input as {
"kind": "Deployment",
"spec": {"template": {"spec": {"containers": [{"name": "app", "image": "myrepo/myimage:sha256-abc123"}]}}}
}
}
Run with: opa test ./policy/
Require 100% test coverage for all deny rules before promoting to enforcement mode (opa test --coverage ./policy/ reports it). Lint policies with Regal in CI.
Rego syntax note: since OPA 1.0 (and recent Conftest/Gatekeeper releases built on it), the v1 syntax is the default and requires the
ifandcontainskeywords, as used above (theimport rego.v1line keeps the files valid on both old and new versions). Olderdeny[msg] { ... }rules needimport rego.v1or migration (opa fmt --v0-v1) to work on current versions.
Policy library management¶
At scale, individual teams should consume policies — not author them. A platform team model:
- Central policy repository — all organizational policies live in a single, reviewed repo. Changes require at least two approvals.
- OCI distribution — package and publish policies as OCI artifacts to your container registry. Conftest can pull directly:
conftest pull oci://myregistry/policies:v1.2. - Versioned releases — tag policy releases. Pipelines pin to a specific version. Breaking policy changes are pre-announced and coordinated.
- Policy changelog — maintain a changelog. Teams need to understand what changed between policy versions and why, so they can prepare before upgrading.
- Exemption process — define how teams can request a policy exemption for a specific resource, with a named owner and expiry date. Exemptions are stored as OPA annotations or Kyverno exceptions, not as silently disabled checks.
Good practice¶
- Start with guardrails, not handcuffs — begin in warn/audit mode, then move high-confidence rules to enforce after validating false-positive rates. This builds developer trust rather than creating immediate friction.
- Codify organization-specific rules — encode the policies unique to your risk appetite and regulatory context, not just generic benchmarks.
- Provide clear, actionable messages — when a policy blocks something, tell the developer exactly what to change and why. A cryptic OPA error kills adoption.
- Unit-test every policy — use OPA's built-in test framework or Kyverno's
testcommand to validate that policies correctly allow and deny the right inputs before deploying them. - Gate policy changes like code changes — a policy change that silently widens permissions is as dangerous as a code change that introduces a vulnerability.
Maturity progression¶
Starter — a few ad-hoc Conftest or OPA checks run in CI for the most critical rules (e.g., no hardcoded secrets, no public buckets). Policies live in individual repos with no central coordination.
Intermediate — a centrally maintained policy library distributed to all pipelines. Kubernetes admission control enforced via Gatekeeper or Kyverno. Policies are version-controlled, reviewed, and unit-tested. Warn vs. enforce distinction managed deliberately.
Advanced — full policy lifecycle management via OPA bundles served from an OCI registry or a custom bundle server (the commercial Styra DAS management plane has reportedly been discontinued; see Tools). Every compliance control from Compliance Auditing has a corresponding policy check. Policy decisions are logged to SIEM for audit evidence. SLO on policy coverage: 100% of production workloads governed by at least one admission policy.
Common pitfalls¶
- Policies that are always in warn mode — a policy that never enforces is theater. Build a path from warn to enforce for every rule with a defined timeline.
- No tests for the policy itself — untested policies silently fail to catch what they were meant to catch, discovered only after an incident.
- Overly broad deny rules — a policy that blocks too much gets disabled by teams under pressure. Tune aggressively on false positive rate before enforcing.
- Policy debt — policies accumulate without pruning. Stale rules that no longer apply create confusion and noise. Review the policy library quarterly.
- No exemption process — teams under deadline that cannot get an exemption will delete the policy check. A defined exemption path keeps the policy in place.
Metrics¶
| Metric | What it tells you |
|---|---|
| % of pipelines with at least one enforcement-mode policy | Governance coverage across the estate |
| Policy violation rate by rule | Which policies are blocking most; candidates for tuning or developer education |
| Warn-to-enforce migration rate | Maturity of the policy program; stagnation in warn mode is a risk signal |
| Policy test coverage | How thoroughly policies are validated before rollout |
| Open policy exemptions by age | Exemptions that have not been closed indicate growing residual risk |
Tools1¶
Open-source¶
- Checkov — policy-as-code scanning for IaC (Terraform, CloudFormation, Kubernetes, Helm) with hundreds of built-in rules and support for custom Rego or Python policies. Integrates natively with most CI systems.
- Conftest — tests any structured configuration (YAML, JSON, HCL, Dockerfile, etc.) against Rego policies. Ideal for pre-deploy validation in CI without a running cluster.
- Gatekeeper — OPA-based Kubernetes admission controller. Enforces policies at the Kubernetes API level, preventing non-compliant resources from ever being admitted. Supports audit mode for existing resources.
- Kyverno — Kubernetes-native policy engine using YAML-based policies (no Rego). Easier adoption for teams unfamiliar with Rego; supports mutating policies (auto-remediation) as well as validation.
- Open Policy Agent (OPA) — general-purpose policy engine using the Rego language. The foundation for Conftest, Gatekeeper, and many other tools. Use directly for API authorization and custom enforcement contexts.
Commercial¶
- No single dominant vendor-neutral commercial option. Styra DAS (the commercial OPA management plane) was reportedly discontinued after Styra's OPA maintainers joined Apple in 2025; its former components were slated for community maintenance. Teams needing centralized policy governance typically build it on OPA bundles and a CI/CD-driven policy repository, or use the policy modules of CNAPP and cloud-governance platforms.
Links¶
-
Listed in alphabetical order. ↩