Introduction to enterprise authorization
Enterprise authorization is a capability of Stacklok Enterprise. For a full comparison of ToolHive Community and Stacklok Enterprise capabilities, see Stacklok Enterprise.
At small scale, hand-writing a Cedar policy for each MCP server works fine. At fleet scale, with many servers, many teams, and an identity-provider-driven identity model, that approach falls apart. Enterprise authorization turns access control into a set of declarative custom resources that the operator compiles into the Cedar policies ToolHive enforces at runtime.
The problem
Open source ToolHive supports rich Cedar policies on every MCP server, and that flexibility is exactly what you want when you are securing one or two servers. Once you are running a fleet, the same flexibility becomes a liability. Each server carries its own policy file. The policies drift. There is no clean place to express something as simple as "every engineer can read every server."
The model also conflates two things that should be separate. A Cedar policy defines what a role can do and who holds that role, all in one document. That works for a single team that owns a single server. It does not work when a platform team needs to define a reusable role and a namespace owner needs to grant that role to their own users without rewriting the platform team's policy.
The CRDs introduced by enterprise authorization separate these concerns. Cluster admins define roles once. Cluster admins or namespace owners bind those roles to identity-provider groups and roles. A separate attachment object decides which MCP servers a binding applies to. The operator compiles the combination into Cedar.
The model
Enterprise authorization is built on a small set of custom resources. If you have used Kubernetes RBAC, the shape will feel familiar.
ClusterPlatformRole(reference) defines what a role can do. It is product-agnostic. Per-product action vocabularies live underspec.productActions[], keyed by API group. The only supported product API group istoolhive.enterprise.stacklok.com. Wildcard"*"is allowed, which is how the built-inwriterrole grants every action.ClusterPlatformRoleBinding(reference) defines who gets a role, cluster-wide. It maps identity-provider groups and roles (as carried in the JWT) to aClusterPlatformRole.PlatformRoleBinding(reference) is the namespaced sibling ofClusterPlatformRoleBinding. It lets a namespace owner author grants for resources in their own namespace without involving the cluster admin. Cluster-scoped and namespace-scoped bindings combine with union semantics: a subject is granted a role if any matching binding says so.ToolhiveAuthorizationPolicy(reference) defines where access applies. It attaches one or more role bindings to a specific MCP target: anMCPServerorMCPRemoteProxy. It also supports optionalruleRestrictions(narrow the bound role to a specific tool, prompt, or resource), atoolHintFilter(gate by thereadOnlyHintordestructiveHintannotations the server advertises), and adeny[]list that compiles to a Cedarforbidrule and overrides every grant. MultipleToolhiveAuthorizationPolicyobjects can attach to the same target; their effective principal sets and deny lists are unioned.
Claim mapping
Bindings refer to identity-provider groups and roles. Those values come from
claims in the JWT that ToolHive validates on every request. By default, the
platform derives the claim names from the IdP type it detects, so a cluster
admin typically does not need to configure them. For Microsoft Entra ID, the
defaults are the roles claim (the app-role claim) and the groups claim.
To override the defaults, set the groupsClaim and rolesClaim keys on the
platform identity-provider ConfigMap. The enterprise installer creates this
ConfigMap with a release-specific name in the platform namespace, for example
stacklok-enterprise-platform-enterprise-manager-idp-config. Find it with
kubectl get configmap -A | grep idp-config, then edit the keys to match the
claims your IdP emits. For the identity-provider side of this setup, where you
configure the IdP to emit those group and role claims, see
Configure platform identity.
Inside each binding, the from[] field is a list of PrincipalCondition
objects. Each condition has two arrays: groups[] and roles[]. The two arrays
inside one condition are AND-ed: the subject must satisfy both the groups list
and the roles list to match. Across multiple conditions in the same from[],
the conditions are OR-ed: a subject matches the binding if any one of them
matches. At least one of groups[] or roles[] must be non-empty in every
condition, which the CRD enforces with a CEL validation rule.
For example, this binding matches users who are in the mcp-engineers group and
hold the mcp-viewer role, or anyone who holds the mcp-admin role:
from:
- groups: [mcp-engineers]
roles: [mcp-viewer]
- roles: [mcp-admin]
For the broader picture of how ToolHive verifies tokens and integrates with an OIDC provider, see Authentication and authorization.
Built-in roles
The operator ships two ClusterPlatformRole objects out of the box.
readergrants the MCP read operations andcall_tool. Thecall_toolsurface can be narrowed at bind time withtoolHintFilter.readOnlyHint: true, which restricts the binding to tools the MCP server itself annotates as read-only.writergrants every action via wildcard.
You can list them with kubectl get clusterplatformroles (or the short form,
kubectl get cpr). Build a custom ClusterPlatformRole only when you need a
narrower action vocabulary than reader and writer offer.
Next steps
- Quickstart - GitHub MCP with Entra ID for a hands-on walkthrough against a real identity provider
- Namespace self-service authorization to see how a namespace owner authors their own bindings
- ToolhiveAuthorizationPolicy for the full schema of the attachment object that ties everything together
- Cedar policies and the authorization policy reference for the underlying policy language and the full vocabulary the operator emits