OPA
.mdVerified against OPA 1.20.1, flags verified via `opa --help` / `opa eval --help` / `opa test --help` run · official docs
What it is and where it fits 🎯#
Open Policy Agent is the general-purpose policy engine underneath Kubernetes admission control (Gatekeeper),
API authorization layers, and config-validation pipelines across the industry — policies are written in a
purpose-built language called Rego. The opa binary itself is the low-level tool: author/test/format Rego,
run OPA as a standalone server for real-time authorization decisions, or evaluate a query by hand. For the
common "test my config files against Rego policies" job most teams reach for day to day, Conftest (its own
cheat sheet in this series) is the far more ergonomic front end — most engineers touch opa directly only
when writing or debugging the underlying Rego, not for routine CI checks.
Where OPA sits in a policy-as-code pipeline#
The same Rego policy logic can, in principle, back all four of these consumption paths — which is exactly why OPA became the de facto standard rather than each ecosystem inventing its own policy language.
Installation#
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_static
chmod 755 opa
sudo mv opa /usr/local/bin/ # or anywhere on your own PATH without sudo
opa versionSingle static binary, no runtime dependencies. The download URL always points at the latest release — pin a
specific version by using a versioned URL instead (.../v1.x.y/opa_linux_amd64_static) if you need
reproducible CI.
Core concepts: Rego in one page#
package authz
default allow := false
allow if {
input.method == "GET"
input.path == ["users"]
}
allow if {
input.method == "GET"
input.path == ["users", input.user_id]
input.requester_id == input.user_id
}package— every Rego file declares a namespace; rules are addressed asdata.<package>.<rule>.default allow := false— the deny-by-default posture almost every real authorization policy wants; without it, an undefined result (no matching rule) isundefined, notfalse, which most consumers treat differently than an explicit denial.- Rules can be defined multiple times (like the two
allowblocks above) — Rego evaluates them as an OR: the rule is true if any of its definitions match.
Evaluating a query#
opa eval 'x := 1; y := 2; x < y' # ad-hoc expression, no files needed
opa eval --data policy.rego --input input.json 'data' # evaluate a policy against an input document
opa eval --data policy.rego 'data.authz.allow' # evaluate a specific rule
opa eval --bundle ./bundle 'data' # load data + Rego from a bundle directory or tarballOutput formats#
opa eval --format pretty 'data.authz.allow' # human-readable (default for `run`, not `eval`)
opa eval --format json 'data.authz.allow' # default for eval — raw JSON result
opa eval --format values 'data.authz.allow'Sample --format json output for the allow rule above with a matching input:
{
"result": [
{
"expressions": [
{ "value": true, "text": "data.authz.allow", "location": {"row": 1, "col": 1} }
]
}
]
}Testing policies 🧪#
opa test ./policy/ # runs every rule prefixed test_ as a test case
opa test ./policy/ -v # verbose, shows each test case
opa test ./policy/ --coverage # report which lines of Rego were actually exercised
opa test ./policy/ -r test_allow_admin # run only test cases matching this regexpackage authz_test
import data.authz.allow
test_get_own_profile_allowed if {
allow with input as {"method": "GET", "path": ["users", "bob"], "requester_id": "bob", "user_id": "bob"}
}
test_get_other_profile_denied if {
not allow with input as {"method": "GET", "path": ["users", "bob"], "requester_id": "alice", "user_id": "bob"}
}Tip
Write the "denied" test case, not just the "allowed" one. A policy engine that never explicitly tests its deny path is the single most common way an authorization bug ships — a rule that's too permissive passes every "should allow" test trivially, and only a "should deny" test catches it.
Formatting and linting Rego source#
opa fmt -w ./policy/ # rewrite files in canonical Rego style, in place
opa check ./policy/ # parse + type-check without evaluatingRunning OPA as a server#
opa run --server --addr localhost:8181 ./policy/ # for sidecar/API-authorization use cases
curl -X POST localhost:8181/v1/data/authz/allow -d '{"input": {"method": "GET", "path": ["users", "bob"]}}'Building a distributable bundle#
opa build -b ./policy/ -o bundle.tar.gz # package policies + data into a versioned bundle for distribution
opa sign --signing-key key.pem -b ./policy/ # sign a bundle for supply-chain integrityReal-world scenario: deny-by-default RBAC for an internal platform API#
A platform team building an internal service-to-service authorization layer wants a hard "nothing is allowed unless explicitly permitted" posture that's independently testable outside the application it protects:
package authz
import future.keywords.in
default allow := false
allow if {
input.role in {"admin", "platform-owner"}
}
allow if {
input.role == "developer"
input.action == "read"
}Running this as a sidecar (opa run --server) means the authorization logic is a separate, independently
versioned, independently testable artifact from the application code — a security review of "what can a
developer-role token actually do" is a Rego-file review, not an application-code audit.
Common pitfalls#
- Missing
default allow := false— without it, a query with no matching rule returnsundefined, which some integrations treat as truthy/permissive rather than as an explicit deny. Always set an explicit default. - Testing only the happy path — see the TIP above.
- Forgetting
opa fmt -wbefore committing — Rego, like Go, has one canonical formatting; skipping it creates unnecessary diff noise across a team.
When to reach for something else#
For "test my config files against policies" (the far more common day-to-day job than hand-authoring opa eval calls), reach for Conftest — see its own cheat sheet, which wraps this same Rego engine in a much more
ergonomic config-testing CLI.