OPA architecture and the Rego mental model
Queries over documents, not scripts.
In short. policy platforms focus on OPA architecture and the Rego mental model: Queries over documents, not scripts.
Every nightclub has one rulebook and many bouncers. If each bouncer memorizes their own version of it, the club's real policy is whatever each of them half-remembers tonight. Open Policy Agent (OPA) is that rulebook made executable: a small, general-purpose *policy engine* — a program whose only job is to answer questions like "may this request proceed?" — that any other system can consult before it acts. The rules are written in a purpose-built language called Rego and stored as plain text files. That is all *policy-as-code* means: authorization and compliance rules live in Git, get peer-reviewed like code, and run deterministically like code, instead of living in wikis, tickets, and tribal memory.
What OPA is, and the failure it prevents
OPA (CNCF-graduated, pronounced "oh-pa") exists to kill *policy sprawl*. Without it, a rule like "no container runs as root" gets re-implemented five times — in a Kubernetes admission webhook, a CI shell script, a Terraform review checklist, and twice in application code — and each copy drifts independently until one forgets an edge case and you have an incident. OPA splits the problem into two roles. The policy enforcement point (PEP) is whatever system does the blocking: your API gateway, your CI job, your microservice. The policy decision point (PDP) is OPA. The PEP sends OPA a JSON document describing what is about to happen (the input); OPA evaluates it against policy plus contextual facts (data) and returns a JSON decision. OPA itself never blocks, mutates, or enforces anything — it only answers. That separation is what lets one policy serve many enforcement points without being rewritten for each.
Architecture: everything is a document
Internally OPA has exactly one data structure: a single JSON document tree rooted at data. Every Rego file declares a package, and its rules are mounted into the tree at that path — package authz makes its rules addressable as data.authz.*. Static context (team rosters, image allowlists, CIDR ranges) is loaded alongside as *base documents*, and the per-request input is a separate ephemeral document. A query such as data.authz.allow is nothing more than a path into this tree; evaluation means computing the value at that path for the current input. OPA runs three ways — embedded as a Go library, as a daemon or sidecar exposing a REST API, or compiled to WebAssembly — and in production, policies and data usually arrive as versioned, signed bundles pulled from a remote endpoint (how bundles are built and promoted is the policy-lifecycle lesson's territory).
dataThe whole engine ships as one static binary, which is why the same artifact works as a CLI, a server, and a test runner:
curl -L -o opa https://openpolicyagent.org/downloads/latest/opa_linux_amd64_staticchmod +x opa && sudo mv opa /usr/local/bin/opa version# Version: 1.18.2# Go Version: go1.25.0# Platform: linux/amd64# WebAssembly: unavailable
The Rego mental model: queries, not programs
Rego descends from Datalog, and it is *declarative*: a rule does not execute top to bottom, it defines the conditions under which a name has a value. Three ideas carry almost the whole language. First, every expression inside a rule body must hold — a body is a logical AND. Second, defining the same rule name with multiple bodies means any one succeeding is enough — repetition is logical OR. Third, variables are not assigned but *unified*: OPA searches for values that satisfy every expression simultaneously, which is how you iterate without loops (some c in input.spec.containers simply tries each element). Rules are also pure — no mutation, no side effects — so the same input and data always yield the same decision.
package authzdefault allow := false# OR: any body that succeeds grants accessallow if input.user.role == "admin"allow if {# AND: every expression in this body must holdinput.method == "GET"input.resource.team in data.teams[input.user.name]}
Evaluate it from the command line: opa eval loads policies and base documents with -d, the input with -i, and takes the query as its argument.
cat > data.json <<'EOF'{"teams": {"alice": ["payments", "fraud"]}}EOFcat > input.json <<'EOF'{"method": "GET","user": {"name": "alice", "role": "developer"},"resource": {"team": "payments"}}EOFopa eval -d policy.rego -d data.json -i input.json --format pretty 'data.authz.allow'# true
Trace it: OPA tried the first allow body — role is developer, so it fails. The second body holds: the method matches, and unification finds "payments" inside data.teams["alice"], so allow is true. Had every body failed (say, a user missing from data.teams, which makes that reference undefined), allow would fall back to its declared default of false. Delete that default line and something more dangerous happens instead.
default is declared, the query result is undefined — not false. Over the REST API that surfaces as an empty object {} with no result key at all. Enforcement code written as "deny only when result is false" or "allow unless denied" fails open on undefined input paths — a typo in a field name silently authorizes everything. Always declare default allow := false, and write every caller to treat anything other than a literal true as a deny.From one-shot eval to a decision service
The same binary serves decisions over HTTP. opa run --server loads the files and exposes the document tree under /v1/data/... — the URL path mirrors the query path exactly, which is the single-document model paying off.
opa run --server --addr :8181 policy.rego data.json# {"addr":":8181","level":"info","msg":"Initializing server.","time":"..."}# in another terminal:curl -s localhost:8181/v1/data/authz/allow \-d '{"input": {"method": "GET","user": {"name": "alice", "role": "developer"},"resource": {"team": "payments"}}}'# {"result":true}
Trade-offs, hardening, and what breaks at scale
Rego's first cost is the mental switch: engineers used to imperative code fight unification for a week, and unreviewed "clever" Rego is write-only. The second is the *data problem*: OPA evaluates against local documents, so any context a rule needs must be replicated into the engine ahead of time via bundles or the data API. Multi-gigabyte base documents inflate memory and restart times, and they are usually the first thing to break at scale. Reaching out at decision time with the http.send built-in avoids replication but couples your request path's latency and availability to a remote service — treat it as a last resort and set strict timeouts. In-memory decisions typically land in well under a millisecond, but on a per-request authorization path you measure, you do not assume.
A default opa run --server listens unauthenticated: anyone who can reach the port can query decisions and — far worse — replace policies and data through the same REST API. In production, start it with --authentication=token --authorization=basic plus a system authorization policy so only intended callers reach the decision endpoints, serve TLS with --tls-cert-file and --tls-private-key-file, and bind to localhost when running as a sidecar. Turn on decision logging (--set decision_logs.console=true, or a remote sink) so every allow and deny is auditable, and verify bundle signatures so a compromised bundle server cannot ship a policy that allows everything.
Purity is the property to carry forward. Because a Rego rule is a deterministic function of input and data, a policy decision is the cheapest thing in your stack to test: feed it a JSON document, assert the output — no mocks, no cluster. The next lesson turns that into engineering practice: structuring packages, writing opa test suites, and measuring coverage so a refactor can never silently flip a production decision.
default allow := false, and the caller enforces the decision as "block the request only when the result is false." A field name in the input gets misspelled, so no allow rule body matches. What happens?false; this is the exact misconception the lesson warns about.default allow := false the result is undefined and enforcement can silently fail open.