skip to content

Running the Engine

OPA is a process you place: linked into the caller as a Go library, beside it as a sidecar, or centrally over HTTP. Interviewers probe it because that placement fixes latency and blast radius.

on this pageshow

explore

questions

11

What does a POST to OPA's /v1/data endpoint send and return?

level: juniorimportance: must knowfreq 74%

answer

  1. the facts travel in one wrapper
  2. URL mirrors the package path
  3. the answer is wrapped too
  4. 200 even when it says no

basics

~20 s

You POST to /v1/data followed by the document path, with a body of the form {"input": {...}} carrying the facts. OPA evaluates that document and replies 200 with {"result": <value>} - the value the rule produced.

solid answer

~40 s

The URL is a document path: a rule named `allow` in `package db.policy` is the document `data.db.policy.allow`, so you POST to `/v1/data/db/policy/allow`. The request body wraps your facts in a single field, `{"input": {...}}`, and inside the policy that object is the `input` document. OPA replies `200` with the answer wrapped the same way: `{"result": false}`, `{"result": true}`, an object, whatever the document evaluated to. The most important thing to say out loud is that the HTTP status is about evaluation, not about the verdict - a denial is a perfectly ordinary `200`, and a client that branches on the status code enforces nothing. A `GET` to the same URL evaluates with no input, which is fine for documents that depend only on data OPA already holds.

code

json · 9 lines
json
{
  "input": {
    "action": "create",
    "resource": {
      "type": "managed_database",
      "backup_retention_days": 7
    }
  }
}

go deeper

for a junior

Be ready to name the pieces without hesitating: a POST to /v1/data plus the document path, the facts wrapped in an input object, the answer wrapped in a result object. Say out loud that a denial still comes back as HTTP 200.

for a middle

Explain how the URL is built from the package and rule names, why the input you send and the data OPA already stores are different things, and what changes in the response when you query a whole package instead of one rule.

for a senior

Show that you design the calling service around the payload rather than the status code: state exactly what your enforcement point does on a 4xx, on a 5xx, and on a 200 whose body has no result in it.

for a principal

Own the question of which document path is the published interface for callers. Decide who is allowed to change its shape, and how a shape change reaches every caller without a window in which the gate quietly stops answering.

## Asking OPA a question over HTTP OPA answers one thing at a time: *what is the value of this document, given these facts?* Over HTTP that question is a request to the Data API, and every integration detail follows from its two envelopes - one around what you send, one around what comes back. ### The URL is a document path, not a file name Rego rules live in packages. A rule named `allow` inside `package db.policy` is the document `data.db.policy.allow`. On the wire the `data` root becomes the `/v1/data` prefix and the dots become slashes, so the URL is `/v1/data/db/policy/allow`. Several `.rego` files can contribute rules to the same package; the file names never appear in the URL and are irrelevant to the caller. You also choose how deep to point. `/v1/data/db/policy/allow` asks for one value. `/v1/data/db/policy` asks for the whole package document - an object with a key for every rule in it that is defined. Pointing at a single named decision is almost always the better client contract: the response stays one value with one meaning, instead of a snapshot of whatever the policy currently happens to contain, including helper rules a policy author is free to rename tomorrow. ### The facts go under `input` The POST body is `{"input": { ... }}`. Whatever you put inside that field is the `input` document the policy sees. Two nearby things it is not: - **`data`** is OPA's own stored state - documents loaded from a bundle or pushed with `PUT /v1/data/...`. That is the standing information the engine already has, like a list of approved database engines. It is not part of your request. - **The URL path** selects *what to evaluate*, not *what to evaluate it against*. Putting a resource id in the path does not make it visible to the rule. A `GET` on the same path evaluates the document with no input supplied, which only makes sense for documents that depend purely on stored data. A concrete example. Suppose the rule enforces that a managed database may not be created with a backup retention window under thirty days. The provisioning service, before it calls the cloud API, posts the spec it is about to submit: ``` POST /v1/data/db/policy/allow {"input": {"action": "create", "resource": {"type": "managed_database", "backup_retention_days": 7}}} ``` and gets `{"result": false}` back. It then refuses to make the call. ### The answer comes back under `result` A successful evaluation is `200` with `{"result": <value>}`. The value is whatever the document evaluated to - a boolean, an object, an array of messages. Two consequences get probed constantly: 1. **Status is about evaluation, verdict is in the body.** `200` means OPA ran the query without an error. `400` means it could not read your request, `500` means evaluation itself failed, and a connection error means the engine is not there. None of those are denials, and a denial is not any of those - it is a `200` whose body says `false`. Enforcement points that check `response.ok` and proceed are not enforcing anything. 2. **An undefined document has no `result` field at all.** You get `200` and the body `{}`. This is the single most expensive detail in the whole API, because the shape of a 'no decision' is indistinguishable at the transport layer from a healthy call. Requests can also ask for extras alongside the result - timing metrics, or an evaluation trace - which arrive as sibling fields in the same object. They are diagnostics; the decision is always the `result` field. ### v0 versus v1 shapes OPA also exposes `/v0/data/<path>`, kept for simple webhook-style callers that cannot reshape their payload. There, the **request body is the input document itself** (no `input` wrapper) and the **response body is the value itself** (no `result` wrapper). Same evaluation, different envelope. Mixing them up is a classic integration bug and it fails quietly in both directions. Point a v0-shaped caller at a v1 URL and there is no `input` field in the body, so the rule sees nothing and evaluates against an empty input. Point a v1-shaped caller at a v0 URL and the whole `{"input": ...}` object becomes the input document, so every reference in the policy is off by one level - the rule looks for `input.resource` and the actual value is at `input.input.resource`. Neither produces an error status; both produce wrong decisions. ### What to carry away The wire contract is small: path names the document, `input` carries the facts, `result` carries the answer, and the status code tells you only whether OPA could answer at all. Build the client around the body, decide up front what it does when the body has no `result` in it, and pin the exact path you query in a test so that a package rename does not silently turn your gate off.

  • The rule is named allow in package db.policy - what URL do you POST to?
    `/v1/data/db/policy/allow`. The package path becomes URL segments under the `/v1/data` prefix and the rule name is the last segment. Posting to `/v1/data/db/policy` instead returns the whole package document - an object holding every defined rule in the package - which couples your client to the policy's internal rule names.
  • What does a 200 status tell you about whether the action is allowed?
    Nothing at all. `200` means OPA evaluated the query without erroring. Allowed or denied lives entirely in the body's `result` value, and a denial is an ordinary `200`. Error statuses mean the engine could not answer - a malformed request, an evaluation error, an unreachable server - and none of them are a verdict.
  • How does the /v0/data shape differ on the wire?
    It has no envelopes. The request body *is* the input document and the response body *is* the value, which suits webhook callers that cannot reshape their payload. Sending a v1-shaped body there makes the policy see `input.input.*`, and every reference in the rule silently misses.

saying these in an interview costs you the question

  • Expects a 403 status when the policy denies
  • Puts the facts at the top level instead of under input
  • Thinks the URL path names the .rego file
  • Sends no input and assumes OPA already knows the request

context

open as a page

OPA can run as an embedded Go library, a per-pod sidecar or a shared server — what changes between them?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Where the decision is computed. Embedded is an in-process function call with no network. A sidecar keeps the call on the pod's loopback. A shared server puts one OPA in every caller's path over the network.

open as a page

In an OPA Envoy ext_authz policy, where in input do the request method, path and body live?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Under input.attributes.request.http: method, path, headers with lowercased keys, and body as a string. The OPA Envoy plugin also adds three top-level conveniences: input.parsed_path, input.parsed_query and input.parsed_body.

open as a page

Why does OPA's /v1/data return 200 with an empty JSON body?

level: middleimportance: should knowfreq 52%

basics

~20 s

Because the document you queried is undefined: nothing produced a value, so there is no result field to return. Evaluation itself succeeded, which is why the status is still 200. An empty body is no decision - neither an allow nor a deny.

open as a page

OPA is embedded in ten Go services with the policy baked into each binary — how does a new rule roll out?

level: middleimportance: should knowfreq 44%

basics

~20 s

One rebuild and release per service. With Rego compiled into the binary there is nothing to push, so enforcement coverage follows ten release trains, and for a while some services enforce the new rule and some the old one.

open as a page

An OPA ext_authz rule matched a URL path exactly; adding ?page=2 now returns 403. Why?

level: middleimportance: should knowfreq 55%

basics

~10 s

Because input.attributes.request.http.path is the raw request target and still carries the query string, so the equality test against "/api/v1/orders" fails. Match input.parsed_path instead, which is query-free and percent-decoded.

open as a page

An OPA sidecar in 3,000 pods versus one shared decision service — what do you size before choosing?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Memory multiplied by replica count, and policy-fetch fan-out. Each sidecar holds its own copy of the policy and any base data, and polls for updates independently, so 3,000 replicas mean 3,000 copies and 3,000 pollers.

open as a page

In an OPA ext_authz policy, how do you allow writes to /admin only from one mTLS identity?

level: seniorimportance: should knowfreq 48%

basics

~10 s

Write a positive allow over an explicit default deny: match the first parsed_path segment, check the method is a write, and compare input.attributes.source.principal, the peer identity the proxy authenticated, against the one permitted value.

open as a page

When should you call OPA's /v1/compile instead of /v1/data?

level: seniorimportance: nice to knowfreq 27%

basics

~20 s

When you cannot supply every fact at query time. /v1/data needs a complete input and answers with a value; /v1/compile takes a query plus the references you name as unknown and answers with the conditions that are still left to check.

open as a page

Centralizing OPA as one shared decision service — what leaves each caller that never left before?

level: seniorimportance: nice to knowfreq 33%

basics

~20 s

Every query's input document. A rule can only judge facts it is given, so the input carries account, tenant and requester identifiers, and centralizing sends all of it across a boundary on every call and usually into a decision-log store.

open as a page

An OPA ext_authz result returns allowed true plus a resolved-subject header. What must hold for the upstream to trust it?

level: seniorimportance: nice to knowfreq 38%

basics

~10 s

The proxy must be the only way in, and the caller must not be able to set that header themselves. Any route around the gate hands the upstream a subject nobody authenticated.

open as a page