A Kubernetes admission webhook, an Envoy sidecar in front of a microservice and a coding agent about to run a tool all stop at the same point before they act: is this allowed? Open Policy Agent (OPA) gives each of them one place to send that question. The caller sends the facts as JSON, OPA evaluates them against a policy written in Rego, and the answer comes back as data. Because the policy lives in files outside each service, teams version, review and test it like any other code, which is what policy as code means.
Open Policy Agent: a general-purpose policy engine
The project’s own definition: “The Open Policy Agent (OPA, pronounced “oh-pa”) is an open source, general-purpose policy engine that unifies policy enforcement across the stack” (OPA docs). General-purpose is the word that matters. OPA has no built-in idea of users, pods or HTTP methods; the docs’ own examples run from which roles a user needs for a resource to which registries container images may come from. It accepts any structured data as input, and a decision can be any structured data too: true or false, a list of violation messages, or an object naming an action and a reason.
OPA has been a Graduated project of the Cloud Native Computing Foundation (CNCF) since 2021 and is licensed under Apache 2.0. The docs name microservices, Kubernetes, CI/CD pipelines and API gateways as typical places to use it, and the project homepage also shows agent tool calling.
How does OPA work?
OPA works by separating a decision from its enforcement. The software that has to act, called the policy enforcement point (PEP), sends OPA a JSON document that describes the request. OPA, the policy decision point (PDP), evaluates that input against its Rego policies and the data it holds in memory, then returns a decision. The PEP enforces it: it admits the pod, forwards the HTTP request or refuses the tool call.
The same opa binary covers the usual deployments. opa eval evaluates a policy once from the command line, which is how CI jobs tend to use it. opa run --server starts a long-running server that listens on localhost:8181 by default and answers on the /v1/data HTTP API. Run it as a sidecar next to each service or as a daemon on each host, and the query never leaves the machine. Go programs can embed OPA as a library and skip the network hop entirely.
Two features matter once OPA runs in more than one place. Bundles are how policy and data reach each OPA: it downloads new versions from a server on a schedule and applies them to the next decision, with no restart. Decision logs go the other way. Each one records a decision with its input and result, which gives you an audit trail of what was asked and what OPA answered.
What is Rego?
Rego (pronounced “ray-go”) is the declarative language OPA policies are written in. The OPA docs describe it as “purpose-built for expressing policies over complex hierarchical data structures”, and that’s how it reads: a rule is a query over nested JSON. A rule holds, or produces a value, when every expression in its body is true, and two bodies for the same rule act as a logical OR. What people call an OPA policy is one or more Rego modules, each starting with a package line, plus the JSON data the rules refer to.
This is the complete example from the “Try opa eval” step of the OPA docs, as published. The input lists servers, ports and networks. The policy collects the IDs of servers that expose http on a public network or telnet anywhere, and allow is true only when there are none:
package example
default allow := false # unless otherwise defined, allow is false
allow if { # allow is true if...
count(violation) == 0 # there are zero violations.
}
violation contains server.id if { # a server is in the violation set if...
some server in public_servers # it exists in the 'public_servers' set and...
"http" in server.protocols # it contains the insecure "http" protocol.
}
violation contains server.id if { # a server is in the violation set if...
some server in input.servers # it exists in the input.servers collection and...
"telnet" in server.protocols # it contains the "telnet" protocol.
}
public_servers contains server if { # a server exists in the 'public_servers' set if...
some server in input.servers # it exists in the input.servers collection and...
some port in server.ports # it references a port in the input.ports collection and...
some input_port in input.ports
port == input_port.id
some input_network in input.networks # the port references a network in the input.networks collection and...
input_port.network == input_network.id # the network is public.
input_network.public
}
The docs then evaluate it with the input file:
./opa eval -i input.json -d example.rego "data.example.violation[x]"
The if before each rule body and the contains on the set rules are Rego v1 syntax, the default since OPA 1.0 shipped on December 20, 2024 after contributions from over 450 developers. Rego v1 makes if and contains mandatory, and every and in work without an import. Policies written for OPA 0.x can fail to parse under 1.0, so the release post points to opa check --v0-v1 to find parser and compiler errors in old code and opa fmt --write --v0-v1 to rewrite it. Regal, a linter and language server for Rego, catches the style and correctness problems the parser lets through. To try any of this without installing OPA, paste it into the Rego Playground.
What is policy as code?
Policy as code means writing a rule such as “servers reachable from the Internet must not expose http” in files that live in version control, instead of in a wiki or an admin console. The rule then goes through the same steps as application code: a pull request, a review, automated tests and a release that every enforcement point picks up. With OPA, the files are Rego modules, the tests are Rego too, and the release is a bundle.
The server policy above is a complete example. The requirement is two sentences, the code is the requirement, and a reviewer can read both. Tests are Rego rules whose names start with test_, run with opa test (policy testing docs), so a change that lets telnet through fails before anyone merges it.
Formal ships its own policies the same way, as Rego managed with a Terraform provider or a Kubernetes custom resource; Formal’s Rego policies covers that workflow.
Policy as code in CI/CD and Terraform
A pipeline already has the JSON, which is why CI is often where teams meet OPA first. Conftest runs Rego against structured configuration (Kubernetes manifests, Tekton pipelines, Terraform code, Serverless configs) and looks for deny, violation and warn rules. A deny fails the build; a warn reports without failing it.
For Terraform, OPA’s own Terraform guide works from the plan rather than the source files: save the plan, convert it to JSON and hand it to OPA as input.
terraform plan --out tfplan.binary
terraform show -json tfplan.binary > tfplan.json
opa exec --decision terraform/analysis/authz --bundle policy/ tfplan.json
Checking the plan means the policy judges the exact change Terraform is about to make. The same guide has a section on what a plan can’t tell a policy, worth reading before you make the check blocking.
Where OPA runs
Besides CI, two places account for most OPA deployments: the Kubernetes API server and the network path in front of services. Agent tool calls, the newest one, get their own section below.
Kubernetes admission control with OPA Gatekeeper
Every object created in a Kubernetes cluster passes the API server’s admission step, and that’s where OPA Kubernetes deployments usually sit. OPA Gatekeeper is “a validating and mutating webhook that enforces CRD-based policies executed by Open Policy Agent”. A validation rule can deny a resource, warn about it, run in dry-run, or audit resources that already exist and report violations without changing them. So a new rule can start in audit, and move to deny once the existing violations are fixed.
Gatekeeper isn’t the only option for clusters. Kyverno, which the CNCF lists as a Graduated project since March 16, 2026, is the other policy engine you’ll see there.
API and microservice authorization
For OPA authorization in front of services, the common pattern is Envoy. Envoy has an External Authorization filter that calls an authorization service to check each incoming request, and the OPA-Envoy plugin extends OPA with a gRPC server that implements that API. It runs as a second sidecar in each pod, next to Envoy, so the service code doesn’t change.
This is a hot path, and OPA holds up on it. In OPA’s adopters list, Pinterest reports its Kafka integration handling about 400K queries per second at peak without caching, and about 8.5M with caching.
OPA vs Cedar
Cedar comes up whenever someone asks for an OPA alternative, and the two overlap less than it seems. The Cedar policy language is “a language for writing authorization policies and making authorization decisions” (Cedar docs): a policy says which principal may take which action on which resource, in what context, and the engine answers allow or deny. OPA evaluates any JSON you give it and can return any structure. As of November 2026:
| Open Policy Agent | Cedar | |
|---|---|---|
| Built for | Policy across the stack: microservices, Kubernetes, CI/CD, API gateways | Authorization decisions in applications |
| Input | Any structured data (JSON) | Principal, action, resource and context, plus entity data |
| Output | Any structured data | Allow or deny |
| Schema | Not required | A schema validates policies when they’re created or updated |
| CNCF status | Graduated (2021) | Sandbox (accepted October 8, 2025) |
| License | Apache 2.0 | Apache 2.0 |
| Managed service | None from the project; third-party support listed | Amazon Verified Permissions |
Sources: OPA docs, Cedar docs, CNCF Cedar page, Cedar repository. The CNCF page also notes that Cedar’s language specification has been formally verified with a theorem prover.
We’d pick OPA when one engine has to judge many kinds of input: admission requests, HTTP calls, Terraform plans, agent tool calls. We’d look hard at Cedar for permissions inside one application, where a schema that rejects a policy referring to an attribute that doesn’t exist is worth more than flexibility.
Who maintains OPA now? Styra, Apple and the CNCF
Same maintainers, same foundation. On August 20, 2025, OPA’s creators announced that they, “along with many team members from Styra”, had joined Apple, and that OPA “remains a CNCF graduated open source project and there are no changes to the project governance or licensing” (OPA blog). The maintainer list stayed the same, apart from the employer change for those who joined Apple.
Apple wasn’t new to the project. The same post calls it “an enthusiastic OPA user” that relies on OPA as “a key component of its authorization infrastructure”. It also says the Rego Playground continues to be operated by Styra with no changes, and that Styra’s repositories, including EOPA, Styra’s commercial distribution of OPA, entered the community process to move into the CNCF OPA GitHub organization. Check GitHub for where a given repository lives today before you depend on it.
OPA for AI agent tool calls
A model that wants to use a tool doesn’t run anything. It returns a structured request that names the tool and its parameters, and the program around the model, which OPA’s docs call the harness, runs the tool. OPA’s page on agent tool calling makes the connection: “Tool calls are structured data, so they are a good fit for policy based approval with OPA and Rego.” That program is the PEP, and it asks OPA before it runs each call.
Two of the four rules in OPA’s example policy:
package coding.tools
deny contains $"Tool {tc.tool} is not allowed" if {
some tc in input.tool_calls
tc.tool in _disallowed_tools
}
deny contains $"WebFetch can only load from HTTPS URLs" if {
some tc in input.tool_calls
tc.tool == "WebFetch"
not startswith(tc.params.url, "https://")
}
_disallowed_tools := {"Bash", "Write", "Edit"}
The agent queries the deny rule over OPA’s REST API, and OPA answers with the reasons:
curl -s localhost:8181/v1/data/coding/tools/deny -d '{
"input": {
"tool_calls": [
{"tool": "WebFetch", "params": {"url": "http://example.com", "timeout": 5000}}
]
}
}'
{
"result": [
"WebFetch can only load from HTTPS URLs"
]
}
An empty result means the agent runs the calls. Any reason means it refuses them, and it can pass the reasons back to the model so the next call is better. Agents run for hours, so the bundles described above matter here: OPA picks up new rules on its schedule and the agent keeps running. Decision logs record every call the agent tried and which ones OPA denied.
Where that check sits sets its limit. A check inside the agent sees the tool call as the model wrote it, and only in agents that were set up to ask. It doesn’t see what the call does once it runs: the SQL a psql command sends, or the HTTP request a script makes. Those show up on the network, which is where we put the enforcement point (see agentic AI security for the wider set of controls).
Rego in an access proxy: how Formal runs OPA
Every Formal policy is a Rego module evaluated by OPA, so this is a setup we know from the inside. It’s one example of OPA in the access path. Formal isn’t an OPA distribution, and we don’t sell or support OPA itself. The enforcement points are the Formal Connector, a proxy in front of databases, SSH servers, Kubernetes clusters and HTTP APIs, and the Formal Endpoint, which runs on laptops, VMs and pods. Both evaluate Rego at up to three stages: when a session opens, before a request reaches the resource, and when the response comes back (policies docs).
Because the proxy parses the protocol, the response stage sees something a check inside the agent never does: the tool calls inside the LLM provider’s response, before the agent receives them. This policy from the policy evaluation docs blocks a response that contains a Bash tool call with a specific command:
package formal.v2
import future.keywords.if
import future.keywords.in
response := {
"action": "block",
"type": "block_with_formal_message"
} if {
some tool_call in input.llm.tool_calls
tool_call.name == "Bash"
tool_call.input.command == "echo hi"
}
It’s the same tool OPA’s example denies, checked before the agent gets the call rather than when the agent is about to run it.
Running OPA inside a proxy changes one thing about writing Rego. Formal policies can call 189 standard OPA built-ins, and Formal adds no custom ones, but a few are blocked. The docs list each with its reason; http.send, for example, makes network calls from the policy engine. External context comes from hooks instead: JavaScript or TypeScript functions that receive the policy input, with network access denied except to allowed hosts, a 5 second default timeout (configurable up to 60 seconds) and 1 MiB of output. Formal first evaluates the parts of a policy that don’t depend on a hook, then calls only the hooks the decision still needs, once each even when several policies reference the same one. The result lands in input.hooks:
package formal.v2
import future.keywords.if
request := {
"action": "block",
"type": "block_with_formal_message",
"reason": "Request risk is too high"
} if {
input.hooks.risk.score >= 5
}
The same engine masks columns per BI viewer in data masking, redacts SSNs from prompts in AI data loss prevention, and limits which tools an agent can call on the MCP gateway. It also reads input.agent, which is how a session policy can block Claude Code started with --dangerously-skip-permissions. New policies start in draft or dry-run, an impact report can backtest one against up to 31 days of past traffic when policy input retention is on, and every change creates a version; the platform page covers that lifecycle. When two policies disagree, Formal resolves it by least privilege, so a block beats an allow (enforcement docs). These policies apply only to traffic that goes through a Connector or the Endpoint.
ManoMano’s case study describes the policy as code side: “Using the Terraform provider and Open Policy Agent engine, ManoMano implemented security as code, ensuring consistent and auditable policy enforcement across all data stores.” And if you’re curious how the policies get edited, Faris Ashai wrote up how we built a visual editor for Rego on OPA’s AST parser and WebAssembly.
Whatever you run OPA in, the question from the diagram at the top decides what a policy can stop: where is the enforcement point, and what can it see? A Gatekeeper webhook sees every object the API server admits. An agent’s own check sees the tool calls its model writes. A proxy sees the queries and requests that actually leave the machine.