This guide is for teams that run Axiomatics today and are moving authorization to Cerbos, or want to know what the move involves before a renewal. It is written for the architect or engineer doing the work, and for the identity and security leads who need to sign off on the plan.
Most of what you built around Axiomatics carries over. Your enforcement points stay where they are, and the access rules your business agreed stay the same. Your identity provider and identity governance tools keep doing their jobs. What changes is the language the rules are written in and who can read them, how a change is tested and released, where attribute data comes from, and which component each enforcement point asks for a decision.
Why migrating from Axiomatics is mostly an authoring and attribute question
Calling a different decision service is the smallest part of the work. The real work is in two places.
The first is authoring. Your rules exist today as ALFA, compiled to XACML 3.0, and often only a few people can read them with confidence. There is no converter from ALFA or XACML to Cerbos, and a line by line translation would carry XACML's machinery, attribute categories, bags and combining algorithms, into a format that does not need it. Writing each rule from its intent, with a test that proves it, produces policy your engineers, security reviewers and auditors can all read, and the tests become the definition of a correct migration.
The second is attributes. In Axiomatics, attribute connectors inside the decision service fetch values from directories, databases and HTTP services, so a request can look small while the decision depends on data nobody sees in it. In Cerbos, the application sends what it already knows, and Cerbos Synapse fetches the rest at decision time from the system that owns it. Listing every attribute your connectors resolve today, and deciding who supplies each one, is what makes the decisions match.
What replaces Axiomatics after you migrate
Axiomatics' Authorization Hub, Access Decision Service, attribute connectors, enforcement SDKs, Policy DevOps tests and reverse query become parts of one Cerbos platform.
- Authorization Hub becomes policy files in Git or Cerbos Hub policy stores. Changes go through code review and tests, and Cerbos Hub builds, versions and distributes them to every decision point. Cerbos Hub runs hosted, or on-premise and air-gapped for enterprise customers.
- The Access Decision Service becomes Cerbos decision points running in your infrastructure, as a service, sidecar, DaemonSet or serverless function. They hold policy only, and keep working on the last policy they received if Cerbos Hub is unreachable.
- Attribute connectors become Cerbos Synapse data sources, which fetch identity, resource and relationship data from identity providers, databases and APIs at decision time, with caching you configure.
- Enforcement SDKs and gateway integrations become Cerbos SDKs in your applications and Cerbos Synapse at the gateway, mesh and data layers (Envoy, Istio, Kafka, Trino, Kubernetes), plus MCP servers and AI coding agents.
- Policy DevOps tests become Cerbos test suites, YAML files next to the policies that Cerbos Hub runs before it builds each deployment.
- The reverse query becomes query plans for "which records can this person see".
- Audit logs become Cerbos decision logs, collected by Cerbos Hub from every decision point and linked to the policy version that made each decision.

Key Cerbos principles to know before translating
Policies are organized by resource. A resource policy holds every rule for one kind of resource, such as purchase_order, with the actions it supports. There are no policy sets to nest. Shared logic lives in derived roles, variables and scoped policies.
Everything is denied unless a rule allows it, and there is no NotApplicable or Indeterminate in the answer. Every action comes back as allow or deny.
A deny wins over an allow for the same role. If a person has several roles and one of them is allowed with no deny for that role, the action is allowed. To make a deny apply to everyone, write it for every role, roles: ["*"].
Conditions are CEL expressions over the principal (P), the resource (R) and the request. Attributes are typed JSON values, single values or lists, rather than bags.
Missing data fails closed. A condition that hits a missing attribute does not match, so an allow rule does not fire. Strict evaluation mode goes further and denies an action whenever a condition or variable it depends on raises an error, which closes the gap for deny rules too. Schemas reject a request that is missing a required attribute before any rule runs.
Rules can return outputs. A rule can return a value computed in CEL alongside the decision, which is where obligations and advice go.
XACML and ALFA to Cerbos concept mapping
| XACML or ALFA | Cerbos | Notes |
|---|---|---|
| PolicySet | A resource policy per resource kind, with scoped policies for hierarchy | No nesting. Group by resource, not by organizational chart |
| Policy | A resource policy, or a scoped policy for a department, region or tenant | Scopes replace policy sets that exist only to separate business units |
| Rule with Effect Permit or Deny | A rule with EFFECT_ALLOW or EFFECT_DENY | |
| Target | The rule's actions and roles, or derivedRoles | Anything that compares two attributes goes in the condition, as in XACML |
| Condition | condition.match.expr, or all, any and none blocks | CEL, for example R.attr.amount <= P.attr.approvalLimit |
| Rule and policy combining algorithms | Cerbos' fixed semantics, plus scoped policy settings | See the next section |
| AttributeDesignator, subject category | P.attr.<name> | |
| AttributeDesignator, resource category | R.attr.<name> | |
| Action category | The requested action, matched by the rule's actions | |
| Environment category (time, channel) | now(), attributes on the request, or globals set from environment variables | Time functions are built into CEL. Globals hold values fixed per deployment, such as the region |
| Bags and *-one-and-only functions | Single values, or lists checked with in, exists and all | A schema can require an attribute to be a single string |
| MustBePresent="true" | A required field in a schema with enforcement set to reject | Rejects the request before any rule runs |
| Indeterminate | Deny, through default deny, and strict evaluation for errors | The answer is always allow or deny |
| Obligations and advice | Rule outputs, on match or on condition not met, and query plans where an obligation carried a data filter | The enforcement point acts on outputs, and the data layer applies a query planâs filter |
| Attribute connectors (PIPs) | Request attributes, or Cerbos Synapse data sources | The data stays in the system that owns it |
| Authorization Hub (PAP) | Git and Cerbos Hub policy stores, builds and deployments | Every deployment is a tested, versioned build |
| Access Decision Service (PDP) | Cerbos decision points | Stateless, scale by adding instances |
| Policy DevOps (Gradle and JUnit) | Cerbos test suites in YAML | Run by cerbos compile and by Cerbos Hub before every build |
| XACML JSON request to /authorize | CheckResources, or the AuthZEN evaluation API | Several resources and actions in one call |
| Contextual Authorization Query | PlanResources query plans | Returns a filter for your data layer |
| Audit logs | Decision logs from every decision point, collected by Cerbos Hub | Linked to the policy version, searchable and exportable |
XACML combining algorithms in Cerbos
XACML lets every policy and policy set choose how its children combine. Cerbos has one evaluation model and no combining algorithms to choose from. Most estates use a small number of algorithms in recurring patterns, so each one maps to a way of writing the policy that gives the same result under that one model.
| XACML algorithm | How to get the same result in Cerbos |
|---|---|
| deny-overrides, ordered-deny-overrides | The default. A deny wins over an allow for the same role. Write cross-role denies with roles: ["*"] |
| deny-unless-permit | The default. No allow means deny, and there is no NotApplicable |
| permit-overrides, ordered-permit-overrides | Write the exceptions into the allow rule's condition instead of a separate deny, or split the roles, since an allow from any role wins across roles |
| first-applicable | Cerbos rules have no order. Make the conditions mutually exclusive, or move each layer into a scoped policy, where the most specific scope that decides wins |
| permit-unless-deny | Write an allow for every role, roles: ["*"], and the exceptions as deny rules |
| only-one-applicable | No equivalent. Merge the overlapping policies, which usually removes an ambiguity rather than losing a behavior |
| Policy sets where a business unit may only narrow the corporate rules | Scoped policies with SCOPE_PERMISSIONS_REQUIRE_PARENTAL_CONSENT_FOR_ALLOWS, so a child scope can restrict but not grant beyond its parent. This is the closest pattern, not an exact equivalent |
The ordered variants differ from the unordered ones only in the order children are evaluated, which can change the obligations returned but not the decision, so they map the same way.
A worked example of translating an Axiomatics policy to Cerbos
Here is a rule typical of a finance estate on Axiomatics. Managers can approve purchase orders up to their own approval limit, in their own department, and every approval carries an obligation to log it. The ALFA has the shape most estates use, with denyUnlessPermit and one-and-only functions so that a missing or multi-valued attribute does not let a request through. The ALFA below follows Axiomatics' published syntax.
namespace acme {
import System.*
namespace attrs {
attribute role { id = "acme:subject:role" type = string category = subjectCat }
attribute department { id = "acme:subject:department" type = string category = subjectCat }
attribute approvalLimit { id = "acme:subject:approval-limit" type = double category = subjectCat }
attribute resourceType { id = "acme:resource:type" type = string category = resourceCat }
attribute poDepartment { id = "acme:resource:po-department" type = string category = resourceCat }
attribute poAmount { id = "acme:resource:po-amount" type = double category = resourceCat }
attribute actionId { id = "acme:action:id" type = string category = actionCat }
}
obligation logApproval = "acme:obligation:log-approval"
policy purchaseOrders {
target clause attrs.resourceType == "purchase-order"
apply denyUnlessPermit
rule managerApprovesWithinLimit {
target
clause attrs.actionId == "approve"
clause attrs.role == "manager"
permit
condition stringOneAndOnly(attrs.department) == stringOneAndOnly(attrs.poDepartment)
&& doubleOneAndOnly(attrs.poAmount) <= doubleOneAndOnly(attrs.approvalLimit)
on permit {
obligation logApproval
}
}
}
}
The decisions you know must hold today are the definition of a correct migration, so write them as a Cerbos test suite first, then make the policy pass.
# tests/purchase_order_test.yaml
name: PurchaseOrderTestSuite
options:
strictEvaluation: true
principals:
maria:
id: maria
roles: ["manager"]
attr:
department: finance
approvalLimit: 5000
lee:
id: lee
roles: ["manager"]
attr:
department: finance
sam:
id: sam
roles: ["employee"]
attr:
department: finance
approvalLimit: 5000
resources:
small_order:
kind: purchase_order
id: po-1
attr:
department: finance
amount: 3200
large_order:
kind: purchase_order
id: po-2
attr:
department: finance
amount: 12000
marketing_order:
kind: purchase_order
id: po-3
attr:
department: marketing
amount: 800
tests:
- name: Managers approve orders within their limit in their own department
input:
principals: ["maria"]
resources: ["small_order", "large_order", "marketing_order"]
actions: ["approve"]
expected:
- principal: maria
resource: small_order
actions:
approve: EFFECT_ALLOW
outputs:
- action: approve
expected:
- src: resource.purchase_order.vdefault#manager-approves-within-limit
val:
obligation: log-approval
purchaseOrder: po-1
approver: maria
amount: 3200
- principal: maria
resource: large_order
actions:
approve: EFFECT_DENY
- principal: maria
resource: marketing_order
actions:
approve: EFFECT_DENY
- name: A manager with no approval limit cannot approve anything
input:
principals: ["lee"]
resources: ["small_order"]
actions: ["approve"]
expected:
- principal: lee
resource: small_order
actions:
approve: EFFECT_DENY
- name: People who are not managers cannot approve
input:
principals: ["sam"]
resources: ["small_order"]
actions: ["approve"]
expected:
- principal: sam
resource: small_order
actions:
approve: EFFECT_DENY
The second test covers missing data. In the ALFA, doubleOneAndOnly turns a missing approval limit into Indeterminate, and denyUnlessPermit turns that into a deny. In Cerbos the condition cannot be satisfied without the attribute, so the allow never fires, and strict evaluation turns the error into an explicit deny.
Now the policy.
# policies/purchase_order.yaml
apiVersion: api.cerbos.dev/v1
resourcePolicy:
version: default
resource: purchase_order
rules:
# Managers approve orders up to their own limit, in their own department
- name: manager-approves-within-limit
actions: ["approve"]
effect: EFFECT_ALLOW
roles: ["manager"]
condition:
match:
all:
of:
- expr: P.attr.department == R.attr.department
- expr: R.attr.amount <= P.attr.approvalLimit
output:
when:
ruleActivated: |-
{"obligation": "log-approval", "purchaseOrder": R.id, "approver": P.id, "amount": R.attr.amount}
A few things changed on the way across. The target on resource type became the policy's resource, the targets on action and role became actions and roles, and the condition reads the same in plain CEL. The denyUnlessPermit algorithm is simply how Cerbos works. The obligation became an output that carries the approver and the amount, so the code that logs the approval no longer has to look them up. Run cerbos compile locally, or let Cerbos Hub run the suite before it builds the deployment, and the policy only ships when all three tests pass.
Once the attribute shapes are settled, attach a schema to the policy that makes department and approvalLimit required, and set schema enforcement to reject. A request without them is then refused before any rule runs, which is the Cerbos equivalent of MustBePresent="true".
If your team works with an AI coding agent, the Cerbos agent skills help with the rewrite. Install them with npx skills add https://www.cerbos.dev, give the agent your rules written in plain words, and it writes each one as a Cerbos policy with a test suite and runs the tests until they pass. Your reviewers still approve every policy before it ships.

Getting your rules out of Axiomatics
Start with an inventory, not a conversion. Export your ALFA sources, or the XACML your Authorization Hub deploys, and list every policy set, policy and rule with the attributes each one reads.
Use it as a reference for what the rules are. Write the rules in plain words first, one resource type at a time, agree them with the person who owns that resource, then write the Cerbos policy and its tests. Large XACML estates often hold rules nobody has needed for years, and policy sets that exist only because of how the estate grew. The inventory is the moment to retire them rather than carry them across.
Where attributes from Axiomatics attribute connectors go
List every attribute your policies read, and for each one write down whether the request supplies it or an attribute connector resolves it, and from which source.
- Attributes the enforcement point already sends. Keep sending them, as principal or resource attributes on the Cerbos request.
- Identity attributes resolved from a directory. Department, clearance, manager and group memberships come from the identity token where it is fresh enough, or from a Cerbos Synapse data source that reads the identity provider at decision time, as we show in looking up identity attributes at decision time.
- Resource attributes resolved from a database. Send them from the record the application already loaded, or let a Cerbos Synapse data source read them from the database that owns them.
- Environment attributes. Time comes from now() in the condition. Channel, location and device go on the request. Values fixed for a deployment, such as its region or environment, can be set as globals in the decision pointâs configuration, including from environment variables, and read in any condition.
- Hierarchies. Department, region and organization paths are checked with hierarchy functions in the condition, so "a manager for EMEA can approve orders in EMEA.UK.London" is one expression.
For each item, also write down how stale it is allowed to be. That list is the Synapse cache configuration, and it is what your audit team will want to see.
XACML obligations and advice as Cerbos outputs
Every obligation and piece of advice in your estate becomes an output on the rule that produced it. Use ruleActivated for what XACML returned on Permit, and conditionNotMet for what it returned when a rule did not apply, such as a message explaining why an approval needs a second person. Where an obligation or advice carried a filter, such as âonly return orders in the userâs regionâ, use a query plan instead. Cerbos returns the conditions as a filter, and your data layer applies it.
Two things change for the enforcement point. Outputs arrive in the response next to the decision, as structured values, so it reads them instead of parsing obligation identifiers and attribute assignments. And Cerbos does not refuse a decision because the enforcement point cannot carry out an output, so if an obligation was a hard requirement, such as "only release this record if the access is logged", make the enforcement point treat that output as required before acting on the allow.
Replacing XACML requests with Cerbos calls
An XACML JSON request to Axiomatics looks like this.
{"Request":{
"AccessSubject":[{"Attribute":[
{"AttributeId":"acme:subject:role","Value":"manager"},
{"AttributeId":"acme:subject:department","Value":"finance"},
{"AttributeId":"acme:subject:approval-limit","Value":5000.0}]}],
"Action":[{"Attribute":[{"AttributeId":"acme:action:id","Value":"approve"}]}],
"Resource":[{"Attribute":[
{"AttributeId":"acme:resource:type","Value":"purchase-order"},
{"AttributeId":"acme:resource:po-department","Value":"finance"},
{"AttributeId":"acme:resource:po-amount","Value":3200.0}]}]}}
The same check through the Cerbos JavaScript SDK reads like this.
const decision = await cerbos.checkResource({
principal: {
id: "maria",
roles: ["manager"],
attr: { department: "finance", approvalLimit: 5000 },
},
resource: {
kind: "purchase_order",
id: "po-1",
attr: { department: "finance", amount: 3200 },
},
actions: ["approve"],
});
if (decision.isAllowed("approve")) {
const approval = decision.output(
"resource.purchase_order.vdefault#manager-approves-within-limit",
);
// log the approval with approval.approver and approval.amount
}
Cerbos SDKs exist for eight languages, plus Laravel, and one CheckResources call can ask about several resources and actions at once, which replaces XACML multiple decision requests. If you would rather keep a standard request format, Cerbos PDP implements the AuthZEN Authorization API, the OpenID Foundation standard for how enforcement points ask for decisions. Put the call behind one function in each service, so the swap is one change per service.
For lists and searches, replace reverse queries with query plans. Ask which purchase orders Maria may approve, and Cerbos returns the conditions as a filter your data layer applies, with documented adapters for Prisma, Drizzle, Mongoose, Convex, SQLAlchemy and LangChain with ChromaDB.
Porting tests from Axiomatics Policy DevOps
If you have Policy DevOps tests, each JUnit case that asserts a decision becomes an entry in a Cerbos test suite, a principal, a resource, an action and the expected effect, plus any expected outputs. Most suites shrink, because fixtures in Cerbos are YAML rather than request builders.
If you have few tests, build them from production. Take a sample of real requests and their Axiomatics decisions from your decision logs, and turn them into fixtures. That gives you the decisions that actually happen, not the ones someone remembered to test.
Running Cerbos beside Axiomatics in shadow mode
Before Cerbos enforces anything, run it beside Axiomatics on real traffic.
- Send every check to both. At each enforcement point you are migrating, call Cerbos alongside Axiomatics, but keep enforcing Axiomatics' answer.
- Log both decisions. Cerbos Hub collects every Cerbos decision with its inputs, linked to the policy version that made it, so a mismatch can be traced to a rule or a missing attribute. Turn on audit logging on both sides for the services in scope.
- Fix any mismatch. Most mismatches are attributes an Axiomatics connector resolved that the Cerbos request did not carry. The rest are rules that were broader or narrower than anyone remembered, often through a combining algorithm. Each fix becomes a test.
- Agree the exit bar with your security team before you start, for example zero unexplained mismatches over two weeks of production traffic for the service.

Switching over and decommissioning Axiomatics
Move enforcement one service at a time, once that service meets the exit bar. Keep Axiomatics running for the services not yet moved, then for a short rollback window after the last one. Export Axiomatics' audit logs for the retention period your regulators require before you shut it down, and retire its attribute connectors once nothing reads them.
Common challenges when migrating from Axiomatics, and how to solve them
Rules only one person understands. Some policies were written and extended over years by one specialist. Rebuild those rules from their business intent, not from the XACML. Write the rule in plain words, agree it with its owner, then write the policy and the test.
Deeply nested policy sets. Estates that grew by business unit often nest policy sets several levels deep with different combining algorithms at each level. Flatten them by resource kind, and use scoped policies where a business unit genuinely needs its own rules.
Attributes nobody knew a rule depended on. An attribute connector resolves something behind the scenes and nobody listed it. Shadow mode finds these as mismatches. Add each one to the attribute list, then to the request or to a Synapse data source.
Multi-valued attributes. XACML bags let an attribute hold several values, and comparisons use "any" semantics. In Cerbos, make the shape explicit. A list attribute is checked with in or exists, and a schema can require a single value where the rule assumes one.
Indeterminate results your enforcement points handle. Some PEPs treat Indeterminate differently from Deny, for example by retrying. Cerbos always returns allow or deny, so remove that branch, and turn on strict evaluation so an error in a condition always denies.
Obligations the enforcement point must fulfill. List them, return each as an output, and make the enforcement point check for it before acting on an allow.
Estate-wide "who can access" reports. Cerbos answers "who can access this record" by checking each candidate person, which suits reviews of a record or a team. If your entitlement reporting runs a reverse query across the whole estate, keep that report where it runs and move the runtime decisions first.
Planning your migration from Axiomatics
Start with the service that has the fewest attribute sources and the simplest combining, not the most important one. Settle the request shape and the attribute list there, then reuse them for the harder services. Start early enough that you go into your next Axiomatics renewal with at least one service running on Cerbos against your own traffic, and a mismatch log that shows how close the rest is.
Book a call and bring a few of your Axiomatics policies and the attributes they use. We will model them in Cerbos with you, write tests that check they give the same decisions as today, and help you build a migration plan, timeline and risk view your security and audit teams can review. To start on your own, try Cerbos with your translated policies.
Go deeper:
FAQ
Tagged in






