CoursesOPA & RegoWhat OPA is: decoupled policy

What OPA is: decoupled policy

One engine for all your rules.

Advanced12 min · lesson 1 of 12

A nightclub splits two jobs cleanly between two people. The bouncer at the door decides who gets in: checks IDs, checks the dress code, checks the guest list, then waves you through or turns you away. The DJ inside plays whatever they like and checks nobody. Open Policy Agent (OPA) is the bouncer, never the DJ. It is a general-purpose policy engine, which means you hand it JSON (JavaScript Object Notation, the plain-text data format nearly every tool already speaks) describing a situation, plus any extra facts you loaded ahead of time, and it hands back a decision. The situation might be a web request arriving at your service, a Kubernetes manifest, or a Terraform plan. Your application, your CI (continuous integration) pipeline, or your admission webhook plays the venue staff who actually has to act on what the bouncer said.

Three words are worth pinning down before you go further. Policy-as-code means you write your guardrails in a real language kept under version control, instead of sprinkling if-statements through a dozen services. Rego is OPA's declarative query language: you describe the conditions under which something is true, and OPA goes looking for values that satisfy them. Decoupled policy means the rule lives in one place, reviewed and tested on its own, while many different systems ask that one engine the same kinds of questions. That generality is why a single tool can gate API (application programming interface) authorization, block bad YAML (the indented text format Kubernetes and pipeline configs are written in) in CI, and reject a non-compliant pod at Kubernetes admission.

JSON in, decision out

Every OPA evaluation has the same shape. input is the document under judgement, and you supply it fresh with each query. It might be an HTTP request (the kind a browser or another service makes), a rendered Deployment manifest, or a Terraform plan converted to JSON. data is the background context OPA loaded earlier: allowlists, role maps, exception lists. Your Rego rules read both and produce a result. That result can be a plain allow or deny, a set of violation messages, or a structured object carrying reasons. OPA has no idea what the JSON actually represents. It only evaluates rules over documents, and that ignorance is exactly what lets you run one engine everywhere.

terminal
opa eval --format pretty 'true'
output
true

The path you query matters. Policies live under data in OPA's virtual document tree, so a rule named allow inside package authz is queried as data.authz.allow. Aim at the wrong path and you get undefined, which reads like "the policy did nothing" rather than like an error. Why undefined is dangerous comes up again later in this lesson. For now, hold on to this: OPA answers the exact question you asked, literally, and nothing beyond it.

How a policy decision is made
1input
JSON: the thing to judge
2data
JSON: external facts
3Rego policy
rules over documents
4decision
allow / deny / messages
OPA evaluates policy over input plus data. Something else still has to enforce the answer.

Why pull the rules out of the app

Teams feel the payoff from centralizing policy within a sprint or two. The rules live in one repository, go through code review, and ship with their own tests, instead of five microservices each reimplementing "no public S3 buckets" a slightly different way. Security and platform engineers can tighten a guardrail without asking every application team to redeploy. Policy-as-code is to guardrails what infrastructure-as-code is to servers: the rule is written down, versioned, and applied the same way every time.

The same Rego can sit at three different checkpoints. Conftest runs it in CI, catching bad config before a merge. Gatekeeper runs it at Kubernetes admission, catching it before anything gets scheduled. The Decision API runs it beside your service, catching it request by request. That is defense in depth: something that slips past CI because a developer skipped the pipeline still meets the same rule inside the cluster, because all three checkpoints read the same policy even though they enforce it in very different ways.

Where OPA sits in your stack

OPA does not ship with a pile of prewritten rules, even though plenty of teams wrap it so it looks that way. It is an engine you program. Conftest is a CLI (command-line interface) tool that parses YAML, JSON, HCL (HashiCorp Configuration Language, what Terraform files are written in), Dockerfiles and more into JSON, then runs your Rego against the result. Gatekeeper embeds OPA as a Kubernetes validating admission webhook and packages your Rego inside ConstraintTemplate CRDs (custom resource definitions, the way Kubernetes learns new object types). Run OPA as a sidecar and it exposes HTTP endpoints so a microservice can hand off authorization. The integration changes. The language stays put.

terminal
conftest test deployment.yaml --policy policy/
kubectl apply -f pod.yaml
curl -s localhost:8181/v1/data/authz/allow -d '{"input":{"user":"bob"}}'
output
FAIL - deployment.yaml - main - container app runs privileged
Error from server: admission webhook "validation.gatekeeper.sh" denied the request
{"result": false}

OPA decides. Something else enforces

This split is easy to forget, and forgetting it gets expensive. A deny from OPA means nothing if the caller logs it and carries on, if your Conftest policy uses warn where it should use deny, or if Gatekeeper is still sitting in dryrun mode because the "trial period" never ended. A policy that gets evaluated and then ignored is theatre. Every integration has to fail closed on a deny, and you have to prove that in a test rather than assume it.

Undefined is not deny: the fail-open trap
When no Rego rule matches, the result is undefined, not false. An authorization policy written without default allow := false leaves most requests sitting in that undefined state. A service that checks if (result === true) and otherwise lets the request through has fail-open behavior, so undefined sails straight past as an allow. The Decision API makes this worse: it returns HTTP 200 with an empty body when a rule is undefined, and a naive client reads that as approval. Default every boolean decision to false, and treat a missing result as deny at the integration layer.

One tiny policy, end to end

Rego syntax gets its own lesson next. Before that, watch the whole loop run once. A package holds the rules. input carries the facts about this one request. You query a path under data to get the answer back. The policy below lets anyone through for a GET on /health and denies everything else by default. Look closely at that explicit default: without it, an unmatched path would come back undefined instead of false.

terminal
cat > policy.rego <<'EOF'
package example
default allow := false
allow if {
input.method == "GET"
input.path == "/health"
}
EOF
echo '{"method":"POST","path":"/api"}' > request.json
opa eval -d policy.rego -i request.json --format pretty 'data.example.allow'
output
false
# swap input for a health check:
echo '{"method":"GET","path":"/health"}' > request.json
opa eval -d policy.rego -i request.json --format pretty 'data.example.allow'
true

What you will build in this course

The order of the course is deliberate. The beginner lessons install OPA, teach you how evaluation works in Rego, and wire up opa test so policy gets tested like any other code. The intermediate lessons cover deny-set idioms, the split between input and data, Conftest inside CI, and coverage gates. The advanced lessons put Gatekeeper on a real cluster, run OPA as a service with signed bundles, profile Rego that sits on a hot path, and ship policy through a pipeline that enforces at every layer. Every lesson gives you the commands, the output you should see, and the specific failure the control exists to stop.

OPA graduated from the CNCF (Cloud Native Computing Foundation, the body that also hosts Kubernetes) in 2021 and sits alongside the rest of the cloud-native tooling. Unlike Kubernetes, though, it is not tied to containers at all. That independence is the whole point. A platform team standardizes on Rego once, then reuses the same tests, the same bundle pipeline, and often the same rule bodies across microservice authorization, Terraform checks in CI, Kubernetes admission, and API gateway plugins. A new hire learns one decision model instead of four home-grown scripting dialects buried in four different tools.

Who writes the policy, who writes the app

Decoupling splits the audience in two. Application developers emit structured JSON and ask OPA for a verdict. They do not bury security rules inside business logic. Platform and security engineers write the Rego, publish the bundles, and keep the allowlists in data current. The day "no privileged containers" grows an exception process, the policy repository gets a pull request, not twelve services getting redeployed. None of that works unless enforcement asks OPA on every path. A bouncer on a smoke break stops nobody.

terminal
opa eval --format pretty '{"a":1}' 'input.a'
output
1

Set that against authorization hard-coded into application code. A product manager asks for "editors may write to docs/, but only on weekdays." It starts life as a five-line if-statement in one service. Six months later the mobile API, the admin console, and the batch importer each carry their own version: one forgot about weekends, one treats an empty role list as admin. A single tested Rego document replaces four drifting implementations, evaluated identically everywhere. Moving to it is real work. Living with the drift costs more.

OPA does not replace identity either. It assumes something upstream already authenticated the caller and handed it JSON describing who they are. OPA answers the question that comes next: given this identity, this action, and this resource, permit or deny? Or, given this manifest, list what is wrong with it. Keeping authentication upstream is what keeps OPA stateless, and stateless is why you can run as many copies of it as your traffic needs. There are no sessions for them to keep in sync.

terminal
opa eval --format pretty '1 + 2 * 3'
output
7

Someone senior will eventually ask why IAM (identity and access management) policies or Kubernetes RBAC (role-based access control) are not enough on their own. The honest answer is that both are excellent inside their own borders and blind outside them. AWS IAM will never look at your Helm chart in GitHub Actions. Kubernetes RBAC has never seen a Terraform plan. OPA is the shared format for rules that have to hold everywhere. It does not replace cloud IAM. It gives you one testable language for the standards your organization sets, then pushes enforcement out to whichever surface is able to act.

How this goes wrong in real life

From a defender's seat, four OPA failures come up again and again. Policy that was never deployed, so OPA runs and nothing ever calls it. Policy deployed in warn or dryrun mode and left there forever. undefined quietly treated as allow inside an authorization integration. And someone widening an allowlist by editing data without touching a line of Rego. Test for all four every quarter. The question is not "is Gatekeeper running." The question is "does a known-bad pod actually get rejected, with exit code 1 in CI and HTTP 403 in the cluster."

Write your first policy against a real incident or a real audit finding. "Public LoadBalancer with no annotation" beats any abstract tutorial, because the test writes itself from the bad manifest that already slipped through. Keep that manifest as a Conftest fixture forever. It is evidence the control fixes something that actually happened to you, which is the argument you will want on hand when a developer complains that CI got slower.

Service meshes and API gateways complement OPA rather than compete with it. Those tools route traffic; OPA answers whether this traffic, or this manifest, should exist in the first place. Istio and Linkerd can call OPA for authorization decisions. Kong and Envoy support external auth filters that POST to /v1/data. The pattern repeats every time: serialize the context to JSON, ask OPA, act on the answer. That repetition is why learning Rego once keeps paying you back.

When stakeholders want numbers, count two things: duplicated authorization checks you deleted, and incidents your CI policy caught before they shipped. Both are rough, and both are honest. Inventing one tidy metric like "policy coverage percent" and presenting it as a security outcome is neither.

Try this

Run the small health-check policy twice: once with a GET /health request, once with a POST /api request. Watch default allow := false turn the second case into an explicit false rather than undefined. That one habit separates a bouncer who fails closed from a door that drifts open whenever nobody wrote a matching rule.

terminal
cat > policy.rego <<'EOF'
package example
default allow := false
allow if {
input.method == "GET"
input.path == "/health"
}
EOF
echo '{"method":"GET","path":"/health"}' > ok.json
opa eval -d policy.rego -i ok.json --format pretty 'data.example.allow'
echo '{"method":"POST","path":"/api"}' > bad.json
opa eval -d policy.rego -i bad.json --format pretty 'data.example.allow'
output
true
false

Takeaway

OPA is the bouncer, not the DJ. It reads JSON, evaluates your Rego, and returns a decision. Your app, Conftest, or Gatekeeper still has to act on that decision, because a deny that gets logged and ignored stops exactly nothing.

Next up: install the binary and get comfortable with opa eval and the REPL (read-eval-print loop, an interactive prompt where you type a query and see the answer straight away), so you can ask the engine questions without building a whole pipeline first.

Quick check
01Which description of Open Policy Agent (OPA) is accurate?
Incorrect — Gatekeeper is the Kubernetes integration. OPA itself works over any JSON you send it.
Correct — Anything that can send JSON and act on the answer can use it: CI, APIs, admission control, and more.
Incorrect — OPA is programmable. A scanner might embed it, but OPA is the engine, not the rule pack.
Incorrect — OPA evaluates policy. It does not hold your application secrets.
02No Rego rule matches and you wrote no default. What comes back from the query?
Correct — Undefined is not false. Write default allow := false and treat a missing result as deny.
Incorrect — With no default and no matching rule, Rego gives you undefined, not false.
Incorrect — You get a normal evaluation response. Errors are for malformed queries or engine problems.
Incorrect — Undefined carries its own meaning in Rego. It is neither false nor null.
03What is the one OPA limitation to keep in your head?
Incorrect — Convert the plan to JSON and Conftest or opa eval will read it happily.
Incorrect — Kubernetes admission is one integration. CI and service sidecars are equally common.
Correct — A deny that gets logged and never acted on stops nothing.
Incorrect — OPA is a standalone open-source binary you can run on a laptop.

Related