Safeguard
Infrastructure Security

Introduction to Open Policy Agent and Rego

A concrete walkthrough of Open Policy Agent and Rego — how OPA evaluates decisions, a runnable policy example, and where it fits in supply chain security.

Priya Mehta
DevSecOps Engineer
8 min read

Every Kubernetes admission controller, Terraform gate, and CI/CD policy check eventually asks the same question: should this action be allowed? Open Policy Agent (OPA) answers that question with code instead of tribal knowledge or wiki pages. Since donating to the CNCF in 2018 and graduating as an incubated project in February 2021, OPA has become the default way to enforce rules across Kubernetes (via Gatekeeper), Terraform (via Conftest), Envoy, Kafka, and custom CI pipelines — all using one policy language, Rego, and one decision API. Instead of writing bespoke validation logic in five different languages for five different tools, teams write a Rego rule once and evaluate it anywhere OPA runs, from an admission webhook to a docker build step.

This matters for supply chain security specifically because policy decisions increasingly depend on data that lives outside the deployment manifest — SBOM contents, vulnerability scan results, image provenance, license metadata. Rego was built to reason over exactly that kind of structured JSON data. This post walks through what OPA actually is, how Rego evaluates a decision, and how to write and test your first policy.

What is Open Policy Agent and why do security teams adopt it?

OPA is an open-source, general-purpose policy engine that separates decision-making logic from the application enforcing it. Rather than hardcoding "block privileged containers" or "deny public S3 buckets" inside application code, you push that logic into OPA as a standalone service or embedded library, and any system — a Kubernetes API server, a CI pipeline, a service mesh sidecar — sends OPA a JSON input and receives a JSON decision back. Styra, the company that created OPA in 2016, donated it to the CNCF in 2018, and it graduated in February 2021, putting it in the same tier as Kubernetes, Prometheus, and Envoy. Security teams adopt it because it lets one policy, written once, gate infrastructure-as-code, container admission, API authorization, and CI/CD approvals without rewriting logic per tool. Gatekeeper, the OPA-based Kubernetes admission controller, is used by default in GKE's Policy Controller and is a CNCF project in its own right.

What is Rego and how is it different from a typical scripting language?

Rego is a declarative query language modeled on Datalog, meaning you describe what a valid or invalid state looks like rather than writing imperative steps to check it. A Rego policy doesn't say "loop through each container, and if privileged is true, return false" — it says "a deny reason exists if any container has securityContext.privileged == true," and OPA's evaluation engine figures out how to answer that. This matters practically because Rego rules compose: you can define ten independent deny rules across ten files, and OPA aggregates every rule that matches into one decision, so teams can own separate policy files without coordinating merge order. In December 2024, OPA reached its 1.0.0 release, which made the modernized Rego v1 syntax — requiring the if and contains keywords explicitly, e.g. deny if { ... } instead of bare deny { ... } — the default, rather than something you had to opt into via import future.keywords. Existing v0 policies still run under compatibility mode, but new policies written today should target v1 syntax.

How does OPA actually evaluate a policy decision?

OPA evaluates a decision in three concrete steps: it takes a JSON input document, runs it against compiled Rego rules held in memory, and returns a JSON output at a named path. For example, a Kubernetes admission request arrives as a JSON AdmissionReview object containing the pod spec; OPA evaluates that input against the data.kubernetes.admission.deny rule set; and it returns either an empty set (allowed) or a set of deny messages (rejected), which Gatekeeper then turns into an admission failure with the message displayed to the kubectl apply caller. The same input-query-output loop applies to a Terraform plan JSON fed to Conftest before terraform apply, or an SBOM document fed into a CI gate before a container is pushed to a registry. Because the query path is just data.<package>.<rule>, you can test a policy locally with the OPA CLI (opa eval -i input.json -d policy.rego "data.kubernetes.deny") using the exact same input format the production system sends, which is why OPA policies are unusually easy to unit test compared to logic embedded in application code.

How do you write a first Rego policy from scratch?

You write a first Rego policy by defining a package, then one or more deny or allow rules that reference fields in the input document. Here's a complete, runnable example that blocks any Kubernetes pod running a privileged container:

package kubernetes.admission

import rego.v1

deny contains msg if {
    some container in input.request.object.spec.containers
    container.securityContext.privileged == true
    msg := sprintf("privileged container %q is not allowed", [container.name])
}

Save this as privileged.rego, then test it against a sample admission request with opa eval -i admission.json -d privileged.rego "data.kubernetes.admission.deny". If any container in the input has privileged: true, OPA returns a set containing the formatted message; if none do, it returns an empty set, which Gatekeeper interprets as "allow." A supply-chain-relevant variant of this same pattern — one that a team migrating off manual SBOM review would write in an afternoon — checks package versions against a known-bad list: deny contains msg if { some pkg in input.sbom.packages; pkg.name == "log4j-core"; semver.compare(pkg.version, "2.17.0") < 0; msg := sprintf("log4j-core %v is vulnerable to CVE-2021-44228", [pkg.version]) }. That five-line rule, run in CI against every SBOM before a build is promoted, catches Log4Shell-class exposures before the artifact ever ships.

Where does OPA fit inside a software supply chain security pipeline?

OPA fits at every gate where a machine-readable artifact needs a pass/fail decision before it moves to the next stage: SBOM validation after build, container admission at deploy, and infrastructure-as-code review before merge. A typical pipeline runs opa eval against the generated CycloneDX or SPDX SBOM immediately after docker build to reject images containing packages with critical CVEs or disallowed licenses, then runs Conftest against the Terraform plan JSON during the PR check to block public storage buckets or overly permissive IAM policies, then relies on Gatekeeper at the cluster level as a final backstop in case something slips through CI. Because all three stages consume the same Rego rule syntax, a security team can maintain one policy repository instead of three separate rule sets in three separate tool-specific formats (Sentinel for Terraform Cloud, Kyverno's YAML syntax for Kubernetes, custom scripts for SBOM checks), which is the main reason OPA adoption tends to expand once it clears the first gate.

What are the most common mistakes teams make adopting Rego at scale?

The most common mistake is writing monolithic policies with no unit tests, which turns every policy edit into a production risk. Rego ships with a built-in test framework (opa test) that lets you assert expected outputs against sample inputs in _test.rego files, and teams that skip this find out their policy was wrong only when it blocked — or failed to block — a real deployment. A second common mistake is ignoring evaluation performance: a Rego rule that iterates over every package in a large SBOM with nested loops instead of using indexed lookups can turn a sub-10ms admission check into a multi-second one, which matters when it's sitting in the hot path of every kubectl apply. A third is mixing Rego v0 and v1 syntax across a policy repo after the OPA 1.0.0 release, which causes silent parse differences; running opa fmt and pinning one syntax version repo-wide avoids it. Finally, teams often write policies that only check static configuration and never validate against runtime or exploitability context, producing alert volume without risk signal — a gap that policy engines by design don't solve on their own.

How Safeguard Helps

Safeguard extends what a Rego policy like the log4j example above can act on by feeding it real exploitability data instead of just version strings. Our reachability analysis determines whether a vulnerable function in a flagged package is actually called from your application's code paths, so policy gates can deny on reachable risk instead of every CVE match in an SBOM. Griffin AI, our security reasoning engine, correlates SBOM data — either generated automatically during your build or ingested from existing CycloneDX/SPDX sources — against live threat intelligence to prioritize which policy violations matter before a human ever reviews the CI failure. When a violation is confirmed and fixable, Safeguard opens an auto-fix pull request with the corrected dependency version or configuration change, so the Rego gate that caught the issue also drives the remediation, closing the loop between detection and fix without manual triage.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.