Migrating from OPA and Rego to Cerbos

Published by Alex Olivier on November 20, 2025
Migrating from OPA and Rego to Cerbos

When we founded Cerbos in early 2021, we set out to solve a recurring problem we kept seeing across engineering teams: every service was reinventing authorization. Rules were tangled in code, duplicated across microservices, inconsistently tested, and difficult for product or security teams to review. We wanted a clean, scalable, maintainable way to manage “who can do what on which resource, under which conditions”.

Like many teams, we initially used Open Policy Agent as the foundation. It was mature, open source, flexible, and widely adopted. At the time it looked like the right platform on which to build application-level authorization.

Over time we learned that while OPA is excellent as a general-purpose policy engine (Kubernetes admission control, Terraform checks, CI/CD gates, network controls), that very generality becomes friction for application-level authorization.

Developers didn’t want to learn a new logic language - Rego. Input shapes became brittle. Rules became harder to reason about at scale. The evaluation model wasn’t optimized for app workloads. As our customers’ needs evolved - multi-tenant access control, per-resource context, derived roles, richer outputs - we needed an engine designed specifically for these patterns.

That led us to an important pivot: we moved away from OPA and built our own authorization engine tailored to application workloads. The result was a significant performance boost, a drastically simpler policy-authoring model, clearer separation of concerns, and an authorization experience developers actually enjoy using.

Migrating from OPA + Rego to Cerbos isn’t just replacing one engine with another, it’s adopting a model purpose-built for applications: readable YAML policies, CEL-based conditions, rich outputs, baked-in schemas, predictable evaluation, and an incremental migration path that lets you run Cerbos alongside OPA before fully cutting over.

This guide walks you through the differences, how to translate common patterns, how to update your integration layer, and how to roll out the migration safely.

 

Shifting your mindset: Key Cerbos principles

Before diving into code, it's crucial to understand the philosophical shift from OPA/Rego to Cerbos. Cerbos is built on a few core principles designed for safety, clarity, and maintainability in application authorization.

Resource-centric policies. In Cerbos, policies are organized around resources (invoice, dashboard, user_profile). All rules describing who can access a dashboard live in one file. This locality makes policies easier to find, audit, and reason about compared to scattering logic across role-centric or action-centric Rego files.

Default-deny. Cerbos operates on a "default-deny" principle. Access is only granted if a policy explicitly allows it. There are no ambiguous states; if a rule doesn't match to grant permission, the request is denied. This fail-safe approach eliminates a common class of bugs where a miswritten or incomplete Rego rule could inadvertently grant access.

Schema-driven validation. While Rego accepts arbitrary JSON, Cerbos encourages using JSON Schemas to define the expected structure of your principals and resources. This catches attribute errors early, enables better tooling, and ensures consistency across all your services.

Explicit and declarative. Cerbos policies are written in human-readable YAML. The structure is declarative, making it easy for developers, security teams, and even product managers to understand the logic without needing to be experts in a complex query language.

 

Solution overview

A typical OPA request looks like:

{
  "input": {
    "user": { "id": "u123", "roles": ["editor"], "department": "engineering" },
    "resource": { "id": "doc789", "type": "report", "department": "engineering", "owner": "u456" },
    "action": "approve"
  }
}

The application constructs this JSON, calls OPA, and receives a boolean or structured output.

With Cerbos, the interaction pattern is similar, but the request shape is explicit and designed for application authorization. Using an SDK-first example:

from cerbos.sdk.client import CerbosClient
from cerbos.sdk.model import Principal, Resource

client = CerbosClient("http://cerbos:3593")

principal = Principal(
    id="u123",
    roles=["editor"],
    attributes={"department": "engineering"}
)

resource = Resource(
    kind="report",
    id="doc789",
    attributes={
        "department": "engineering",
        "owner": "u456"
    }
)

decision = client.is_allowed(
    principal=principal,
    resource=resource,
    action="approve"
)

allowed = decision

This SDK call wraps the underlying CheckResources API. Cerbos evaluates the request against resource policies, derived roles, schemas and conditions, and returns a structured allow/deny decision.

 

Converting Rego logic to Cerbos

Below are the most common patterns we see in Rego and how to express them cleanly in Cerbos.

Pattern 1: Role based access control

Rego

allow {
  input.user.roles[_] == "admin"
}

allow {
  input.user.roles[_] == "manager"
  input.action == "approve_expense"
}

Cerbos

apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: expense
  version: default
  rules:
    - name: admin-all
      actions: ["*"]
      effect: EFFECT_ALLOW
      roles: ["admin"]

    - name: manager-approve
      actions: ["approve_expense"]
      effect: EFFECT_ALLOW
      roles: ["manager"]

Why this is better

  • Clarity and locality. All rules affecting the expense resource are colocated. The intent of each rule is immediately obvious.
  • Safety. The explicit actions: ["*"] for admins is clearer and less error-prone than an implicit wildcard.
  • Simplicity. Roles map cleanly without needing Rego iterators or complex set logic.

Pattern 2: Attribute based access control

Rego

allow {
  input.action == "edit"
  input.resource.owner == input.user.id
}

Cerbos

- name: edit-owner
  actions: ["edit"]
  effect: EFFECT_ALLOW
  roles: ["*"]
  condition:
    match:
      expr: request.resource.attr.owner == request.principal.id

Why this is better

  • Structure and validation. Attributes live in explicit namespaces (request.principal.attr, request.resource.attr), which can be validated by a JSON Schema. This prevents bugs from typos or missing fields.
  • Readability. The CEL expression is clean, readable, and directly expresses the business rule without boilerplate.

Pattern 3: Contextual logic via derived roles

Complex rules often check a combination of a user's role and their relationship to a resource (e.g., "a tenant_admin can only manage resources within their own tenant").

Rego

allow {
  input.user.roles[_] == "tenant_admin"
  input.resource.tenant_id == input.user.tenant_id
}

In Rego, this logic must be repeated everywhere it's needed, leading to brittle, copy-pasted rules. Cerbos solves this with derived roles.

Cerbos derived role

apiVersion: api.cerbos.dev/v1
derivedRoles:
  name: tenant_context
  definitions:
    - name: tenant_admin_of_resource # A descriptive name for the role
      parentRoles: ["tenant_admin"]
      condition:
        match:
          expr: request.principal.attr.tenant_id == request.resource.attr.tenant_id

Cerbos resource policy

# In a policy for a resource like "invoice" or "project"
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: project
  version: default
  importDerivedRoles:
    - tenant_context
  rules:
    - name: tenant-admin-actions
      actions: ["manage", "delete"]
      effect: EFFECT_ALLOW
      derivedRoles: ["tenant_admin_of_resource"] # Use the derived role here

Why this is better

  • Don't repeat yourself. Complex contextual logic is defined once and reused across many resource policies.
  • Separation of concerns. The base role (tenant_admin) is separated from the contextual condition. This makes policies cleaner and easier to maintain.
  • Readability. The resource policy becomes incredibly simple, stating that tenant_admin_of_resource can perform manage actions. The complexity is neatly abstracted away.

Pattern 4: Using structured outputs

If your Rego policies emit structured data for auditing or UI purposes, Cerbos handles this with the output block.

Cerbos

- name: confidential-access-rule
  actions: ["view"]
  effect: EFFECT_ALLOW
  roles: ["auditor"]
  condition:
    match:
      expr: request.resource.attr.classification == "confidential"
  output:
    when:
      ruleActivated: |
        {
          "message": "Access granted to confidential document.",
          "auditId": request.auxData.jwt.claims.auditId
        }

Why this is better

  • Declarative outputs. Outputs are explicitly part of the rule definition.
  • Rich context. Outputs can be constructed using any data from the request, including principal attributes, resource attributes, or even JWT claims from auxData.

 

The migration process

A safe migration follows a clear, phased process. Instead of a "big bang" cutover, we recommend an incremental strategy.

1. Discover and design

  • Inventory policies. Document all your existing Rego policies and the decision points in your application that rely on them.
  • Define your schema. Model your principals and resources in JSON Schema. This is a great opportunity to clean up inconsistent attribute names and enforce data structures. This step alone often uncovers hidden bugs in the existing system.
  • Map to Cerbos. Translate your Rego policies into Cerbos's resource-centric model. Focus on creating one resourcePolicy per resource kind and identifying opportunities for derivedRoles.

2. Implement and validate

  • Build a test matrix. For each policy, create a set of test cases covering allow, deny, edge cases, and malformed inputs.
  • Run in parallel. In your application, call both OPA and Cerbos for each authorization request. Log both decisions and compare the results. This "shadow mode" allows you to validate your new Cerbos policies with real production traffic without any risk.
  • Compare and align. Use your logs to find any discrepancies between OPA and Cerbos decisions. Investigate and adjust your Cerbos policies until the results are fully aligned.

3. Deploy and cut over

  • Audit mode. Run Cerbos in production but continue to enforce decisions from OPA. Log any differences to catch final edge cases.
  • Controlled rollout. Use feature flags to start enforcing decisions from Cerbos for a small subset of users or low-risk endpoints.
  • Progressive cut-over. Gradually increase the traffic being enforced by Cerbos, monitoring logs and performance closely.
  • Decommission OPA. Once you have confirmed parity and performance, you can safely remove the OPA integration.

 

Potential migration challenges and solutions

Large monolithic policies.
Break them down. A giant Rego file often maps to many small, focused resourcePolicy files in Cerbos, which are much easier to manage.

Complex Rego logic.
Logic involving loops, comprehensions, or virtual documents can often be simplified by restructuring the input data or using Cerbos derivedRoles and CEL expressions.

Inconsistent input data.
Rego's flexibility can hide inconsistencies in the data sent by different services. Cerbos's JSON Schemas force you to fix these inconsistencies, resulting in a more robust system.

Embedded business logic.
If your Rego contains business logic beyond just authorization, refactor that logic back into the application layer. Let your application gather and prepare attributes, and let Cerbos make the clean, final authorization decision.

 

Conclusion

Migrating from OPA + Rego to Cerbos is an opportunity to adopt a simpler, more transparent, and more scalable authorization architecture. Cerbos gives you a developer-friendly policy format, strong attribute validation, contextual roles, declarative conditions, and rich outputs, all designed specifically for the challenges of application-level authorization.

By converting your Rego logic into resource-centric Cerbos policies, running both systems in parallel to validate parity, and gradually cutting over, you can modernize your authorization stack with confidence and reduce long-term complexity. You'll be left with a system that is not only faster and more secure, but also far easier for your entire team to understand and maintain.

For a comparison of Cerbos and OPA across policy language, developer experience, architecture, performance, and policy management - refer to our deep-dive.

If you’re looking to enforce fine-grained, contextual, and continuous authorization across apps, APIs, AI agents, MCPs, services and workloads - give Cerbos a try.

If you’re curious how Cerbos could fit into your architecture or have specific requirements to discuss, feel free to book a call with a Cerbos engineer for a free 1:1 session.

Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team