Enforcer

Keycloak alternatives

Keycloak is an open source identity and access management server you run yourself. The Cloud Native Computing Foundation's project page records that Keycloak was accepted to CNCF on April 10, 2023 at the Incubating maturity level. Alongside sign-in it ships Authorization Services, a policy decision point that models resources, scopes and permissions, with nine built-in policy types: user, role, JavaScript, time, aggregated, client, group, client scope and regex. Applications obtain permissions through the UMA ticket grant at the token endpoint, which returns a requesting party token by default. The optional response_mode parameter changes that: decision returns only the overall decision, { "result": true }, and permissions returns the list of permissions granted. Most teams who go looking elsewhere are not unhappy with sign-in. They are unhappy with what the authorisation layer costs to operate and how little it tells them when a request is refused. Below: the reasons to leave, the reasons to stay, six alternatives, and where our own product, Enforcer, loses to Keycloak.

Why teams look past Keycloak's authorisation layer

When to stay

Stay on Keycloak if you need to run identity yourself, on your own infrastructure, with no vendor in the request path. It is a CNCF incubating project with years of production use behind it. Eight of its nine built-in policy types cover user, role, time, aggregated, client, group, client scope and regex conditions with no policy code at all, created from a list in the admin console. The console also has an Evaluate tab: When designing your policies, you can simulate authorization requests to test how your policies are being evaluated. That is a real safety net, and Enforcer does not have one. Keycloak also answers a question most check-only services cannot: an authorization request sent with no permission parameter returns an RPT of granted permissions. Take that scoped the way Keycloak scopes it. The note printed beside that exact example reads This will not evaluate the permissions for all resources. Instead, the permissions for resources owned by the resource server, owned by the requesting user, and explicitly granted to the requesting user by other owners are evaluated. It covers resources registered with Keycloak as a resource server, not arbitrary rows in your database. If you already operate realms and have the people to run and tune the server, replacing it is a large project with little upside.

The options

OptionModelStrengthTrade-off
OpenFGARelationship based, inspired by the Google Zanzibar paper. Run it as a server over HTTP or gRPC, or embed it as a Go library.Reverse lookups are a first class endpoint. From the concepts guide: The list objects endpoint responds with a list of objects for a given type that the user has the specified relationship with.Read that with the scope the docs attach. The relationship queries page says of ListObjects: It provides a solution to the Search with Permissions (Option 3) use case for access-aware filtering on small object collections. Under Caveats it adds ListObjects will return the results found within the time allotted (listObjectsDeadline, default: 3s) up to the maximum number of results configured (listObjectsMaxResults, default: 1000). Separately, it does authorisation only: no authentication and no user management, so you run an identity system beside it and keep the two in sync.
Ory KetoRelationship based, modelled on Google's Zanzibar, from the Ory project.Permissions are relationships written as tuples. The README quick start creates one with Document:secret#read@tom and checks it with ory check permission Document:secret read tom. Nested ownership and sharing, which flat role lists handle badly, fall out of that model rather than being worked around.Keto does authorisation only. Sign-in, sessions and user storage are separate Ory components, Kratos for identity and Hydra for OAuth2 and OpenID Connect, so you are assembling a stack rather than adopting one server.
CerbosPolicy as code. Access rules are YAML policies, evaluated by a policy decision point that the README describes as the stateless service where policies are executed and decisions are made, run as a service, a sidecar, a systemd unit or a Lambda.The PDP exposes two primary APIs, CheckResources for Can this principal access this resource? and PlanResources for Which of resource kind=X can this principal access?. The README adds: There are also a growing number of query plan adapters to convert the SDK PlanResources responses to a convenient query instance. It lists Prisma and SQLAlchemy, so the filter runs inside your own database query rather than becoming a second authorisation round trip.Stateless means it stores nothing about your users. The README's own example request carries the principal's roles and the resource's attributes in the request body, so gathering those attributes, and getting them right, stays in your application.
ZitadelFull identity platform: SSO, MFA, passkeys, OIDC, SAML and SCIM, with multi-tenancy in the core model rather than added later.Hosted and self-hosted are the same software. The README states: ZITADEL Cloud and self-hosted ZITADEL run the same codebase. Moving between them is not a re-platform.It is licensed AGPL-3.0, with Apache 2.0 and MIT exceptions for specific directories. Some organisations have to clear that with legal before they can adopt it.
authentikSelf-hosted identity provider. The README: authentik is an open-source Identity Provider (IdP) for modern SSO. It supports SAML, OAuth2/OIDC, LDAP, RADIUS, and more, designed for self-hosting from small labs to large production clusters.That protocol coverage, LDAP and RADIUS included, makes it the closest like-for-like swap when the objection is to Keycloak the software rather than to self-hosting at all. The migration is a swap, not a change of operating model.You are still running a server, so the operational burden does not go away, it moves. The code is MIT and the enterprise features sit under a separate authentik EE licence.
WorkOSHosted identity service. AuthKit covers user management, Enterprise SSO and Directory Sync are sold per connection, and Fine-Grained Authorization is a separate documented product.You stop operating a server, and an entry price is published. The pricing page today lists AuthKit as free up to 1 million users, with $2,500 per month for each additional 1M users, and lists Role-Based Access Control among the features marked available for all accounts. Its FGA resource discovery page answers the list question outright: Beyond permission checks, FGA provides endpoints to discover access relationships. Enforcer cannot do that at all.Enterprise connectors scale with your customer count rather than your usage: the pricing page lists SSO connections at $125 each for the first 1 to 15, with tiered discounts above that, and the same tiers for Directory Sync. FGA does not appear on the pricing page as a line item, free or paid, so confirm its cost with WorkOS before you plan around it.

Where Enforcer fits, and where it does not

What is actually different. In Enforcer, identity and authorisation are one service, so POST /authz/check is handed a caller that the same service already signed in, and the response carries a reason and the policy_id that produced the decision. Round trips are not the difference: Keycloak can return a bare decision too, with response_mode=decision giving { "result": true } and no RPT to unpack. The reason on a denial is. Custom policies are Rego in package enforcer.custom, compiled when you write them, capped at 64KB with a 100ms evaluation budget, so there is no JAR, no bin/kc.[sh|bat] build and no preview feature flag. Creating one needs tenant admin. Where Keycloak is better. 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. It cannot answer "which resources can this user read". Keycloak answers a version of that in two places: the RPT returned when no permission is named, scoped to resources owned by the resource server, owned by the requesting user or explicitly granted to them by other owners, and fine grained admin permissions, which partially evaluate permissions to filter and paginate results when a realm administrator queries realm resources. OpenFGA, Keto, Cerbos and WorkOS FGA all answer it in their own domains. Enforcer answers none of it. Enforcer also has no shadow or preview evaluation: an inactive policy is stored and simply not evaluated, whereas Keycloak's Evaluate tab simulates a request against your policies before they go live. And there is no decision log out of the box. GET /audit-events is the identity and admin trail, covering logins, API keys, roles, tenants and terms, and it never records an authorisation decision, so you log the returned reason and policy_id yourself. Keycloak also composes where Enforcer replaces: it has a documented Decision Strategy across evaluated permissions, where Affirmative means that at least one permission must evaluate to a positive decision in order grant access to a resource and its scopes. and Unanimous means that all permissions must evaluate to a positive decision in order for the final decision to be also positive. Enforcer has no equivalent. Two behaviours that are easy to get wrong. With no policy_id named, a built-in baseline runs, and it is not blanket default-deny. Before any rule exists it 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, with a cross tenant admin getting everything. A named policy replaces that built-in decision rather than intersecting with it, and contexts only reaches a named policy. 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 before it trusts the answer. Size. Enforcer is small and new. There are no independent reviews and no large community, against a CNCF incubating project with years of production use behind it.

Sources checked 15 September 2026: www.cncf.io/projects/keycloak/ · www.keycloak.org/docs/latest/authorization_servi · www.keycloak.org/docs/latest/server_development/ · www.keycloak.org/docs/latest/server_admin/index. · openfga.dev/docs/concepts · openfga.dev/docs/interacting/relationship-querie · github.com/openfga/openfga · github.com/ory/keto · github.com/cerbos/cerbos · github.com/zitadel/zitadel · github.com/goauthentik/authentik · workos.com/pricing

Enforcer is identity and authorization in one system, for applications and AI agents. Docs at docs.instruxi.dev. Related: Authorization tools compared · Why was this request denied? · OpenFGA alternatives · Cerbos alternatives · Permit.io alternatives