Set up Cedar authorization in Kubernetes
This guide shows you how to attach Cedar authorization policies to your MCP servers in Kubernetes. Authorization works alongside any of the authentication approaches in Authentication and authorization: once a client is authenticated, Cedar policies decide what it's allowed to do. When using the embedded authorization server, Cedar policies can also evaluate upstream identity provider claims such as group membership. See Upstream identity provider claims for details.
For the Cedar policy language itself, including syntax, entity types, and policy-writing patterns, see Cedar policies. This guide covers the Kubernetes-specific mechanics of attaching those policies to a workload.
Prerequisites
You'll need an MCPServer, MCPRemoteProxy, or VirtualMCPServer with authentication already configured (see Authentication and authorization).
Ways to provide Cedar policies
You can provide Cedar policies to an MCPServer in three ways:
- ConfigMap (
authzConfig.type: configMap): store policies in a separate ConfigMap and reference it from the MCPServer. This keeps policies decoupled from the server spec and lets you share one policy set across multiple servers. Prefer this for larger or shared policy sets. - Inline (
authzConfig.type: inline): define policies directly in the MCPServer spec. This is the simplest option for small, server-specific policy sets, with no separate resource to manage. - Shared MCPAuthzConfig (
authzConfigRef): reference a separateMCPAuthzConfigresource that holds the policies. The sameMCPAuthzConfigcan be referenced from multiple MCPServer, MCPRemoteProxy, and VirtualMCPServer resources, and the operator blocks deletion while references exist. Prefer this for shared policy sets that you also want to manage as first-class Kubernetes resources.
All three approaches use the same Cedar policy language. authzConfig and
authzConfigRef are mutually exclusive at admission.
Step 1: Create authorization configuration
Create a JSON or YAML file with Cedar policies. This example demonstrates several policy patterns:
- Allow everyone to use the weather tool
- Restrict the admin_tool to a specific user (alice123)
- Role-based access: only users with the "premium" role can call any tool
- Attribute-based: allow the calculator tool only for add/subtract operations
Here's an example in JSON format:
{
"version": "1.0",
"type": "cedarv1",
"cedar": {
"policies": [
"permit(principal, action == Action::\"call_tool\", resource == Tool::\"weather\");",
"permit(principal == Client::\"alice123\", action == Action::\"call_tool\", resource == Tool::\"admin_tool\");",
"permit(principal, action == Action::\"call_tool\", resource) when { principal.claim_roles.contains(\"premium\") };",
"permit(principal, action == Action::\"call_tool\", resource == Tool::\"calculator\") when { resource.arg_operation == \"add\" || resource.arg_operation == \"subtract\" };"
],
"entities_json": "[]",
"group_claim_name": "",
"role_claim_name": ""
}
}
Set group_claim_name to extract group membership from a custom JWT claim, and
role_claim_name to extract role membership from a separate claim (for example,
Entra ID roles). Leave both empty to use the
default claim resolution order.
You can also define custom resource attributes in entities_json for per-tool
ownership or sensitivity labels.
For more policy examples and advanced usage, see Cedar policies.
- ConfigMap
- Inline
Step 2: Create a ConfigMap with policies
Store your authorization configuration in a ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: authz-config
namespace: toolhive-system
data:
authz-config.json: |
{
"version": "1.0",
"type": "cedarv1",
"cedar": {
"policies": [
"permit(principal, action == Action::\"call_tool\", resource == Tool::\"weather\");",
"permit(principal == Client::\"alice123\", action == Action::\"call_tool\", resource == Tool::\"admin_tool\");",
"permit(principal, action == Action::\"call_tool\", resource) when { principal.claim_roles.contains(\"premium\") };"
],
"entities_json": "[]"
}
}
kubectl apply -f authz-configmap.yaml
Step 3: Update MCPServer to use authorization
Reference the ConfigMap from your MCPServer resource:
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPOIDCConfig
metadata:
name: k8s-sa-authz-oidc
namespace: toolhive-system
spec:
type: kubernetesServiceAccount
kubernetesServiceAccount:
serviceAccount: 'mcp-client'
namespace: 'client-apps'
---
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPServer
metadata:
name: weather-server-with-authz
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: sse
proxyPort: 8080
# Authentication configuration
oidcConfigRef:
name: k8s-sa-authz-oidc
audience: 'toolhive'
# Authorization configuration
authzConfig:
type: configMap
configMap:
name: authz-config
key: authz-config.json
resources:
limits:
cpu: '100m'
memory: '128Mi'
requests:
cpu: '50m'
memory: '64Mi'
kubectl apply -f mcp-server-with-authz.yaml
Step 2: Define policies inline on the MCPServer
Set authzConfig.type to inline and list your Cedar policies directly under
authzConfig.inline.policies. There's no separate ConfigMap to create:
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPOIDCConfig
metadata:
name: k8s-sa-authz-oidc
namespace: toolhive-system
spec:
type: kubernetesServiceAccount
kubernetesServiceAccount:
serviceAccount: 'mcp-client'
namespace: 'client-apps'
---
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPServer
metadata:
name: weather-server-inline-authz
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: sse
proxyPort: 8080
# Authentication configuration
oidcConfigRef:
name: k8s-sa-authz-oidc
audience: 'toolhive'
# Authorization configuration (policies defined inline)
authzConfig:
type: inline
inline:
policies:
# Allow any authenticated client to call the weather tool
- |
permit(
principal,
action == Action::"call_tool",
resource == Tool::"weather"
);
# Allow a specific client to call the admin tool
- |
permit(
principal == Client::"alice123",
action == Action::"call_tool",
resource == Tool::"admin_tool"
);
# Allow clients with the premium role to call any tool
- |
permit(
principal,
action == Action::"call_tool",
resource
)
when {
principal.claim_roles.contains("premium")
};
resources:
limits:
cpu: '100m'
memory: '128Mi'
requests:
cpu: '50m'
memory: '64Mi'
kubectl apply -f mcp-server-inline-authz.yaml
The policies list requires at least one entry. To use transitive policies that
rely on a static entity store (for example, mapping a group claim to a platform
role), add an authzConfig.inline.entitiesJson string alongside policies; it
defaults to "[]". This is the same concept as the entities_json field shown
in the ConfigMap and MCPAuthzConfig examples above, just camelCase to match
the inline field's Go struct on the CRD.
Share policies across resources with MCPAuthzConfig
To reuse the same policy set across multiple MCPServer, MCPRemoteProxy, or
VirtualMCPServer resources, define it once as an
MCPAuthzConfig and reference it by
name. The operator validates the policy bundle once, tracks the references in
MCPAuthzConfig.status.referencingWorkloads, and blocks deletion while any
workload still references it.
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPAuthzConfig
metadata:
name: shared-cedar-policies
namespace: toolhive-system
spec:
type: cedarv1
config:
policies:
- 'permit(principal, action == Action::"call_tool", resource ==
Tool::"weather");'
- 'permit(principal, action == Action::"call_tool", resource) when {
principal.claim_roles.contains("premium") };'
entities_json: '[]'
---
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPServer
metadata:
name: weather-server-shared-authz
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: sse
proxyPort: 8080
oidcConfigRef:
name: k8s-sa-authz-oidc
audience: 'toolhive'
# Reference a shared MCPAuthzConfig instead of inline policies
authzConfigRef:
name: shared-cedar-policies
authzConfig and authzConfigRef are mutually exclusive and the CRD rejects
manifests that set both. The referenced MCPAuthzConfig must exist in the same
namespace as the workload.
VirtualMCPServer only accepts cedarv1 MCPAuthzConfig resources via
spec.incomingAuth.authzConfigRef. Referencing an MCPAuthzConfig with a
different type (for example, httpv1) fails reconciliation with a clear
condition message because the vMCP runtime authorization middleware is
Cedar-only.
Test your setup
- Make requests that should be permitted by your policies
- Make requests that should be denied
- Check the proxy logs to see authorization decisions
Next steps
- Configure rate limiting to set per-user and shared request limits on MCP servers
- Set up audit logging to track authorization decisions and MCP server activity
Related information
- For the Cedar policy language and policy-writing patterns, see Cedar policies
- For the complete dictionary of entity types, actions, and attributes, see Authorization policy reference
- For the Cedar policy language specification, see Cedar documentation
- For a complete end-to-end example with Okta OIDC and role-based access control, see Role-based authorization with Okta
- For connecting a Virtual MCP Server to a corporate IdP with Cedar group-based access control, see Connect ToolHive to an enterprise identity provider
- For the authentication approaches this authorization layer builds on, see Authentication and authorization
Troubleshooting
Clients receive 401 Unauthorized
A 401 means token validation failed before the request reached your policy layer. Work through these checks in order:
- Check the token itself. Decode the JWT (for example, paste it into
jwt.io - never use production tokens). Verify the
issclaim matches your configured issuer URL exactly (including trailing paths like/oauth2/default), theaudclaim matches the configured audience, andexpis in the future. - Check the request. The
Authorizationheader must beAuthorization: Bearer <TOKEN>with no extra whitespace or quoting. - Check provider reachability. ToolHive first fetches the OIDC discovery
document from
${issuerUrl}/.well-known/openid-configuration, then fetches signing keys from thejwks_urilisted in that document (or from the explicitjwksUrlif you've configured one). Both URLs must be reachable. In Kubernetes, that means egress from the proxy pod; with the CLI, it means egress from your machine.
To inspect the proxy logs for the specific validation error:
# Container logs
thv logs <SERVER_NAME>
# Proxy logs (auth runs here)
thv logs <SERVER_NAME> --proxy
# Follow live
thv logs <SERVER_NAME> --proxy --follow
# Operator logs (reconciliation errors)
kubectl logs -n toolhive-system deployment/toolhive-operator
# Proxy logs for a specific MCPServer
kubectl logs -n toolhive-system \
-l app.kubernetes.io/managed-by=toolhive,app.kubernetes.io/name=<SERVER_NAME> \
-c proxy
Authenticated requests are denied (403)
A 403 means authentication succeeded but Cedar policy evaluation rejected the request. ToolHive uses default-deny: anything not explicitly permitted is denied.
- Match principal, action, and resource exactly. Cedar is case-sensitive.
Tool::"GetIssue"is not the same asTool::"get_issue". - Check policy conditions. If your policy has a
when { ... }clause, make sure the JWT actually contains the claim you reference (with theclaim_prefix). For example,principal.claim_groups.contains("writers")requires agroupsclaim on the token. - Use
hasfor tool annotations. Policies that readresource.readOnlyHint,destructiveHint,idempotentHint, oropenWorldHintmust guard the access withresource has <attr>first. Reading a missing attribute throws an evaluation error that ToolHive treats as a deny. - Confirm group claims arrive. If you use
principal in THVGroup::"<name>", decode the token and verify the configured group claim (groups,roles,cognito:groups, or whatever you set ingroup_claim_name) is present and contains the expected values.
For the full attribute and entity reference, see Authorization policy reference.
ConfigMap mounting issues:
- Verify the ConfigMap exists:
kubectl get configmap -n toolhive-system authz-config - Check the ConfigMap content:
kubectl get configmap authz-config -n toolhive-system -o yaml
Authorization ConfigMap not resolved:
When spec.authzConfig.type: configMap, the controller pre-validates the
referenced ConfigMap and surfaces failures on the AuthConfigured condition
with one of two reasons:
AuthzConfigMapNotFound: the ConfigMap does not exist in the namespace. Create it before reconciling, or correct thename/namespacereference.AuthzConfigMapInvalid: the ConfigMap exists but the payload is missing the configuredkey, is empty, has malformed YAML or JSON, or fails Cedar validation. Inspect the payload and the Cedar configuration shape.
kubectl describe mcpserver -n toolhive-system <name>