Production-ready guide covering policy as code with open policy agent with implementation patterns, code examples, and anti-patterns for enterprise engineering teams.
Open Policy Agent (OPA) is a general‑purpose engine for evaluating policies expressed in Rego, a declarative rule language. In compliance workflows, OPA sits between enforcement points—API gateways, CI/CD systems, Kubernetes admission controllers, or custom agents—and the data they protect. You attach OPA when policy decisions must be deterministic, auditable, and separable from application logic. It matters when policy changes need to propagate without service redeploys, when multiple teams require consistent rule evaluation, or when you must produce a verifiable trail of why a decision was made. This page covers what you can do with OPA day‑to‑day: writing policies, testing them, evaluating them against real data, integrating them into enforcement pipelines, and debugging the failures that catch people off guard.
A Rego policy lives in a text file with a package declaration. The policy body references data documents or input variables to produce a boolean or value result. Below is a minimal, real‑world‑shaped policy that checks whether a container image registry is approved.
policy.rego
package compliance.images
approved = ["ghcr.io", "registry.example.com"]
allowed := input.registry in approved
input.json
{"registry": "ghcr.io"}
Evaluate the policy against the input document with the opa eval command. The --input flag feeds the JSON; without it, OPA aborts.
opa eval --input input.json policy.rego
Output (truncated for brevity)
{
"result": [
{
"expressions": [
{
"value": true,
"text": "data.compliance.images.allowed",
"t": null
}
]
}
]
}
Change the registry to an unapproved value and re‑run; value flips to false. If you omit --input, OPA returns a plain error:
Error: evaluating data: evaluating input: input not found
OPA also supports a raw format that strips the JSON envelope and prints only the evaluated value:
opa eval --input input.json policy.rego --format raw
Output
true
A common silent failure occurs when the input document lacks the key the policy references. If input.json omits registry, OPA reports:
Error: evaluating data: evaluating input.registry: key not found in input
The policy evaluates but produces no expressions entry in the result array, and many enforcers interpret an empty result as “permit.” Always verify that the input document mirrors the shape your policy expects.
opa testPolicies should be exercised by a test suite before promotion. OPA’s test runner matches functions whose names begin with test_ within the policy file or imported test packages.
Create a test file policy.test.rego beside the policy:
policy.test.rego
package compliance.images.tests
import data.compliance.images
test_allowed_ghcr {
allowed = data.compliance.images.allowed
allowed == true
}
Run the test suite with:
opa test policy.rego policy.test.rego
Output
OK: 1 test(s) passed
If the test function name does not start with test_, or if the file lives outside the package hierarchy OPA expects, the runner reports:
No test functions found
OPA does not recurse into directories automatically; every test file must be passed explicitly on the command line, or placed under the package’s tests/ subdirectory if your workflow conventions include that layout.
Tests that reference data.compliance.images.allowed implicitly depend on the policy’s data document. If the policy file is modified and the data section changes shape—say, approved becomes a function call rather than a static array—tests that haven’t been updated will either fail to compile or produce unexpected results. Always run opa test after any policy edit as a gate in your CI pipeline.
A single eval call is fine for ad‑hoc checks, but production pipelines typically need a bundled, versioned policy artifact. The opa build command compiles a Rego file (and any imported packages) into a standalone data file.
opa build policy.rego
Output creates policy.data.json, a JSON document containing the compiled policy and all transitive data. This file can be checked into version control, shipped as a ConfigMap, or baked into container images.
For runtime deployment, OPA can serve policies over HTTP. Start a local server:
opa serve --addr 0.0.0.0:8181
The server listens on port 8181 by default. Query a decision via curl, posting the input document as the request body:
curl -s -X POST http://localhost:8181/v1/data/compliance/images/allowed \
-H "Content-Type: application/json" \
-d '{"registry":"ghcr.io"}'
Output
{"result":[{"expressions":[{"value":true,"text":"data.compliance.images.allowed","t":null}]}]}
To query an unapproved registry:
curl -s -X POST http://localhost:8181/v1/data/compliance/images/allowed \
-H "Content-Type: application/json" \
-d '{"registry":"malicious.io"}'
Output
{"result":[{"expressions":[{"value":false,"text":"data.compliance.images.allowed","t":null}]}]}
When the server is no longer needed, stop it with Ctrl‑C. In production, OPA runs as a long‑running process or sidecar, watching for file changes and hot‑reloading policies without restart—useful for compliance windows that must shift mid‑cycle.
Many teams embed OPA checks in shell scripts, Makefiles, or CI steps. The --format raw flag combined with --input yields a clean boolean exit code suitable for if statements or script logic.
Example Bash snippet:
#!/usr/bin/env bash
INPUT='{"registry":"registry.example.com"}'
RESULT=$(echo "$INPUT" | opa eval --stdin policy.rego --format raw)
if [ "$RESULT" = "true" ]; then
echo "Registry approved – proceeding"
exit 0
else
echo "Registry not approved – aborting"
exit 1
fi
opa eval --stdin reads the input from stdin, eliminating the need for a temporary input.json file. The script exits with the shell’s own exit code, which CI systems can gate on.
opa eval returns exit code 0 when the evaluation succeeds, regardless of the policy’s boolean result. If the policy contains a syntax error or the input is malformed, OPA exits with code 1 and prints the compilation error to stderr. A frequent pitfall is checking the boolean output while ignoring the exit code; a policy that returns false still yields exit code 0, so the surrounding script may continue past the check. Pair opa eval with explicit result comparison, as shown above, to avoid this class of silent passes.
| Symptom | Root Cause | What to Check |
|---|---|---|
Policy evaluates to no expressions in the result | Input document missing the key referenced in the policy (e.g., input.registry absent) | Verify input.json keys match every input.X reference in Rego |
opa test reports “No test functions found” | Test function name does not start with test_ or file is not passed on the command line | Rename functions to test_* and pass every *.test.rego file explicitly |
opa build fails with “package already defined” | Two policy files in the same build scope declare the same top‑level package | Consolidate declarations or split into separate build invocations with --output flags |
Decision API returns multiple expressions entries | Policy contains more than one top‑level rule that matches the queried path, or the queried attribute is a list rather than a scalar | Inspect the policy file for multiple default or allow rules at the same package path; consider consolidating with default statements |
Enforcer permits despite policy returning false | Enforcer interprets an empty result array as “allow” | Ensure the enforcement point inspects result[0].expressions[0].value; log the raw OPA response during debugging |
| Rego version incompatibility after upgrade | Policy uses rego 1.1 comprehension syntax on an OPA build still on 1.0.x | Check opa version; add import future.keywords as _ if experimental syntax is required, or pin OPA version in CI |
Newcomers frequently place configuration values directly in the data document and then reference them as input.something in policies, or vice‑versa. The data document is the static, often version‑controlled knowledge base (approved registries, allowed CIDRs, secret prefixes). The input document is the ephemeral, request‑scoped payload (the specific image being admitted, the user’s group, the CI job’s timestamp). Mixing them produces policies that either always evaluate to the same value (if input is omitted and the policy reads from data) or fail to compile (if input references a key that doesn’t exist in the request). Keep the separation explicit: data for what never changes per deployment; input for what changes per decision.
The most dangerous failure mode in OPA‑driven compliance is a policy that compiles without error but never produces a deny expression. This happens when:
allowed := … and never a complementary deny or default rule.result as “permit” (common in OAuth2/OIDC admission controllers).input, such as input.env == "production" when the CI job injects env: staging.Mitigation: include a catch‑all default in every policy you ship to OPA, e.g., default allow := false, and log the full OPA JSON response during integration tests. If the value field is false and the policy still permits, you’ve identified a mismatch between policy intent and enforcement logic.
Compliance regimes often require a persistent snapshot of which policies were active and what decisions they rendered. OPA’s opa build output is deterministic: given the same source files and Rego version, the produced policy.data.json hash is identical. Use sha256sum or opa hash to record the checksum in an audit log.
opa hash policy.data.json
Output (example)
sha256:3a7f5c2e9d8b...
Store that hash alongside the Rego source commit SHA and the OPA binary version in your change‑management database. When an auditor asks “what rule decided this admission?” you can replay the exact policy bundle and input, evaluate with opa eval, and produce a reproducible audit trail without needing live cluster access.
If your organization forwards OPA decisions to a centralized logging system, structure the curl POST body to include a request_id field; OPA passes through any top‑level keys it doesn’t interpret, making correlation straightforward:
curl -s -X POST http://opa:8181/v1/data/compliance/images/allowed \
-H "Content-Type: application/json" \
-d '{"registry":"ghcr.io","request_id":"c7e3a9f2-1b4d-4a9e-8c3a-5d2e9f1a2b3c"}'
OPA’s response preserves the request_id in the result array’s metadata (if your OPA version supports the metadata extension), enabling end‑to‑end traceability from the enforcement point back to the policy version that rendered the decision.
Policy as code with Open Policy Agent gives compliance teams a programmable, version‑controlled, and introspectable mechanism for make/allow decisions. The tooling—opa eval, opa test, opa build, and the OPA HTTP server—is small, stable, and deliberately generic. The sharp edges are not exotic bugs but ordinary mismatches between policy structure, input shape, and enforcement expectations. Treat the data/input boundary as a contract, keep test suites checked in alongside policy files, and always inspect the raw JSON OPA returns rather than relying on downstream interpreters to guess the outcome. With those habits, OPA becomes a reliable gate in your compliance pipeline rather than a source of silent exceptions.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy