Enforcer
Open Policy Agent is a general purpose policy engine you run as a sidecar, a server, a CLI or a Go library. You write policies in Rego and OPA answers decision queries over JSON input plus whatever data you have loaded into it. It decouples the decision from the enforcement, and it holds no users, no tenants and no roles of its own: any identity or permission data has to be put there by you, inside a bundle, through the data API, or fetched during evaluation. That design is the reason people adopt it and also the reason some people go looking for something else. Every claim below is sourced from the vendor's own documentation, from CNCF, or from the vendor's own repository, and was checked on 15 September 2026, including the claims about us.
input, and pulling data during evaluation with http.send. The summary table scores that last one "Perfectly up to date. No size limit.", and the same page states the price just as plainly: "Latency and availability of decision-making are dependent on the network."off." Read that in context. The same page also says "By default, OPA binds to localhost, which prevents the OPA server from being exposed to services running outside of the same machine.", and --authentication and --authorization turn on bearer token or client TLS auth plus a Rego policy over OPA's own API. The point that stands is the narrow one: the defaults are off, and securing the management API is your job.Stay on OPA when policy has to cover more than one application. CNCF records that \"Open Policy Agent (OPA) was accepted to CNCF on March 29, 2018, moved to the Incubating maturity level on April 2, 2019, and then moved to the Graduated maturity level on January 29, 2021.\", and it is the same engine and the same language across Kubernetes admission control, Terraform plan checks, CI/CD gates, Envoy and SSH, so one team can own one policy corpus for all of them. Stay if you need decisions over arbitrary hierarchical JSON that a relationship model cannot express. Stay if you need answers to search questions rather than allow or deny questions: OPA's data filtering docs distinguish authorisation evaluation from authorisation search and show partial evaluation returning a SQL WHERE clause, and the REST API docs alone list the target formats: UCAST media types for all, minimal, linq and prisma, and SQL media types for SQL Server, MySQL, PostgreSQL and SQLite. Stay if you want policy embedded in a Go binary with no network hop, or running offline. And stay if the tooling matters: opa eval, opa test with coverage, opa bench, plus the Regal linter and language server give Rego a development loop that newer engines do not have.
| Option | Model | Strength | Trade-off |
|---|---|---|---|
| OpenFGA | Zanzibar-style relationship engine, Apache-2.0. CNCF records that "OpenFGA was accepted to CNCF on September 14, 2022 and moved to the Incubating maturity level on October 28, 2025." | It answers the question OPA cannot answer cheaply without partial evaluation. The relationship queries docs say "The ListObjects API is an API endpoint that returns the list of all the objects of a particular type that a specific user has a specific relationship with.", and ListUsers goes the other way. Relationship data lives in the service, so you do not replicate a permission table into the evaluator. | Those list queries are bounded by configuration. The config schema gives listObjectsDeadline a default of 3s and listObjectsMaxResults a default of 1000, described as "The maximum results to return in the non-streaming ListObjects API response. If 0, all results can be returned", so a broad list can time out or truncate unless you change those. The docs also scope the endpoint: "It provides a solution to the Search with Permissions (Option 3) use case for access-aware filtering on small object collections." Relationship tuples are written and stored in OpenFGA, so keeping them in sync with your database is your application's job. It does not authenticate end users. |
| SpiceDB | Zanzibar-inspired authorisation database, Apache-2.0, backed by a datastore such as PostgreSQL, CockroachDB or Spanner. | It makes the consistency trade-off explicit rather than hiding it. The consistency docs describe the New Enemy Problem and the per-request answer to it: "at_least_as_fresh will ensure that all data used for computing the response is at least as fresh as the point-in-time specified in the ZedToken." It also covers list filtering, through LookupResources and LookupSubjects. | Correctness costs latency and the same page is blunt about it: "This consistency mode explicitly bypasses caching, dramatically impacting latency." On one datastore the strongest mode is qualified: "fully_consistent does not guarantee read-after-write consistency on CockroachDB.", because a read can pick a timestamp older than a recent write on another node, up to the cluster's configured max_offset (the docs give a default of 500ms). Pinned reads expire: "Requests specifying at_exact_snapshot can fail with a Snapshot Expired error because SpiceDB eventually collects garbage over time." You are also running and operating a stateful database rather than a stateless sidecar, and it does no authentication. |
| Cerbos | Stateless policy decision point. Policies are YAML with CEL conditions, served over HTTP and gRPC. The PDP is Apache-2.0; Cerbos Hub is a separate commercial product. | Policies are readable by people who are not Rego programmers, the PDP holds no state so it scales like any stateless service, and PlanResources covers list endpoints. The query plan adapters page says "Cerbos provides reference implementations of query plan adapters that convert that response into native query filters for your data layer, so authorization conditions are evaluated at the database level rather than in application code.", with adapters listed for Prisma, Drizzle, Mongoose, Convex, LangChain / ChromaDB and SQLAlchemy. | It assumes something else already resolved the caller. The API reference annotates the principal's roles as "Static roles that are assigned to this principal by your identity management system. Required.", so every request carries identity data your application must fetch and be trusted to get right. Batch size is capped by default: "Up to 50 resources may be provided in a single request by default.", and the docs link to the setting that changes it. The adapters come with an explicit caveat, "Use these adapters as-is or as a starting point to build your own." And the wider platform is open core, so not everything is in the Apache-2.0 repository. |
| Cedar | Policy language plus evaluation engine, Apache-2.0. Embedded as a Rust or Java library, or consumed through the Amazon Verified Permissions service. | Cedar separates validation from evaluation, and states the benefit with its condition attached. The validation docs define validation soundness as "If your policies are deemed valid, they are sure not to exhibit most errors that could arise during request evaluation, for requests that adhere to the expectations defined by the schema.", and say the property was implemented in the Lean theorem prover, proved with automated reasoning, then differentially tested against the Rust implementation. For teams whose objection to Rego is that nobody can review a policy, that is the direct answer. | The guarantee is bounded, and the docs say so: "Validation soundness ensures the absence of most, but not all errors that could arise during policy evaluation." Validation is also something your application must remember to do: "Indeed, the Cedar authorization APIs do not perform validation at the same time that a request is evaluated. Rather, validation is an entirely separate API which can be invoked when policies are loaded or created." Request checking is optional too: "If an application writer is sure that requests will always match the schema's expectations by construction, they can elect to skip these steps." Cedar is a library, not a service, so there is no bundle distribution, no decision log and no management plane unless you build one or adopt Amazon Verified Permissions, which ties you to AWS. The application passes in the relevant entities and their attributes on each request, so the data-plumbing problem moves into your request path rather than disappearing. No identity. |
| Casbin | Embedded authorisation library at apache/casbin, described there as "Apache Casbin: an authorization library that supports access control models like ACL, RBAC, ABAC.", with ports across many languages. | The lowest-friction option on this list to adopt. It is an in-process library with adapters for common databases, so there is no extra service to run, no sidecar, no bundle server and no network hop. | You take on its model.conf and matcher expression syntax, which is small but idiosyncratic, and policy is data in a table rather than a reviewable artefact with a test framework around it. No server, no decision log, no policy distribution, no identity, and the multi-language ports do not all move at the same pace. |
| Keycloak | Full identity and access management server, Apache-2.0, with a fine-grained Authorization Services layer on top. | Identity and authorisation in the same product, which removes the data-loading problem for user and role data because the server already holds it. Its authorization services docs document user, role, group, client, client scope, time, regex, aggregated and JavaScript policy types, plus a Policy Evaluation Tool: "When designing your policies, you can simulate authorization requests to test how your policies are being evaluated." | It is a large Java server with a correspondingly large operational footprint, and the authorisation layer is the less travelled half of the product. Its own docs carry a caveat on the JavaScript policy type: "If your policy implementation is using Attribute based access control (ABAC) as in the examples below, then please make sure that users are not able to edit the protected attributes and the corresponding attributes are read-only." Those policies are also not editable from the console by default, "By default, JavaScript Policies can not be uploaded to the server.", so you package them as a JAR provider and deploy them. |
Enforcer is not the only engine here that combines identity with authorisation. Keycloak does the same thing, as the entry above says. What differs is the shape of the decision: because Enforcer holds the tenants, roles and groups and signs the caller in, POST /authz/check is handed an already-resolved caller, and the decision comes back with a reason and the policy id rather than a bare boolean. That is the whole of the claim.
Where Enforcer is worse, plainly.
List filtering only over Enforcer’s own resource types. The engine compiles the same policy into a SQL WHERE clause by partial evaluation, exposed over gRPC as CompileFilter, but only for the eleven types registered in Enforcer’s own process (account, group, tenant, ticket, wallet and six more). There is no REST endpoint for it and no way to register your own tables, so you cannot use it to filter your application’s data. Enforcer answers \"can this caller do this thing to this resource\" and nothing else. It cannot answer \"give me every resource this user can read\" over your tables. OPA can, through partial evaluation and the Compile API, and will return a SQL WHERE clause or a UCAST condition. Cerbos PlanResources, OpenFGA ListObjects and SpiceDB LookupResources also cover it, each with the limits quoted above. If your product has list endpoints that need row filtering, those four are ahead of us.
You do not escape Rego by choosing Enforcer. A named custom Enforcer policy is Rego in package enforcer.custom, compiled at write time, capped at 64KB with a 100ms evaluation budget. The Rego learning curve applies to us too, without OPA's tooling. OPA has opa eval, opa test with coverage, opa bench, the Regal linter and a language server. We have none of that.
No shadow or preview evaluation. An inactive Enforcer policy is stored and simply not evaluated, so there is no way to run a candidate policy against live traffic and compare before you turn it on. Cerbos and Keycloak both ship a way to test a policy against a simulated request before it takes effect.
No decision log out of the box. GET /audit-events is the identity and admin trail: logins, API keys, roles, tenants and terms. It never records authorisation decisions. You log the returned reason and policy_id yourself. OPA ships a decision log plugin; Cerbos and Keycloak both ship audit logging of their own.
The baseline is not blanket default-deny. Before any rule exists, Enforcer already allows any action other than manage on a row whose owner_id is the caller, plus group and group type reads in your tenant, your own audit events, and global policies. An admin with cross_tenant gets everything. A named policy replaces that built-in decision rather than intersecting with it, and contexts only ever reaches a named policy; the built-in engine ignores it.
Resource attributes are assertions. On /authz/check the resource is described by the caller and never looked up, so owner_id is an assertion your application has to resolve server side. OPA has the same property for input, and so does Cerbos, whose docs above require the caller to supply the principal's roles. A relationship engine such as SpiceDB or OpenFGA does not, because it stores the relationship itself.
Creating a policy requires tenant admin.
And we are small and new. OPA is a CNCF graduated project with years of production use across Kubernetes, Terraform and Envoy, and a linter, language server, playground and test framework around it. Keycloak, Casbin and the commercial identity vendors are larger still. We have no independent reviews and no large community. If your buying criterion is ecosystem maturity, do not pick us.
Sources checked 15 September 2026: www.openpolicyagent.org/docs/external-data · raw.githubusercontent.com/open-policy-agent/opa/ · www.openpolicyagent.org/docs/policy-performance · raw.githubusercontent.com/open-policy-agent/opa/ · www.openpolicyagent.org/docs/security · raw.githubusercontent.com/open-policy-agent/opa/ · www.openpolicyagent.org/docs/filtering · raw.githubusercontent.com/open-policy-agent/opa/ · www.openpolicyagent.org/docs/rest-api · raw.githubusercontent.com/open-policy-agent/opa/ · www.cncf.io/projects/open-policy-agent-opa/ · www.cncf.io/projects/openfga/