What OPA is: decoupled policy
One engine for all your rules.
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.
opa eval --format pretty 'true'
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.
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.
conftest test deployment.yaml --policy policy/kubectl apply -f pod.yamlcurl -s localhost:8181/v1/data/authz/allow -d '{"input":{"user":"bob"}}'
FAIL - deployment.yaml - main - container app runs privilegedError 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.
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.
cat > policy.rego <<'EOF'package exampledefault allow := falseallow if {input.method == "GET"input.path == "/health"}EOFecho '{"method":"POST","path":"/api"}' > request.jsonopa eval -d policy.rego -i request.json --format pretty 'data.example.allow'
false# swap input for a health check:echo '{"method":"GET","path":"/health"}' > request.jsonopa 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.
opa eval --format pretty '{"a":1}' 'input.a'
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.
opa eval --format pretty '1 + 2 * 3'
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.
cat > policy.rego <<'EOF'package exampledefault allow := falseallow if {input.method == "GET"input.path == "/health"}EOFecho '{"method":"GET","path":"/health"}' > ok.jsonopa eval -d policy.rego -i ok.json --format pretty 'data.example.allow'echo '{"method":"POST","path":"/api"}' > bad.jsonopa eval -d policy.rego -i bad.json --format pretty 'data.example.allow'
truefalse
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.