# 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](/toolhive/guides-k8s/auth-k8s.md): once a client is authenticated, Cedar policies decide what it's allowed to do. When using the [embedded authorization server](/toolhive/guides-k8s/embedded-auth-server-k8s.md), Cedar policies can also evaluate upstream identity provider claims such as group membership. See [Upstream identity provider claims](/toolhive/concepts/cedar-policies.md#upstream-identity-provider-claims) for details.

For the Cedar policy language itself, including syntax, entity types, and policy-writing patterns, see [Cedar policies](/toolhive/concepts/cedar-policies.md). This guide covers the Kubernetes-specific mechanics of attaching those policies to a workload.

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

You'll need an MCPServer, MCPRemoteProxy, or VirtualMCPServer with authentication already configured (see [Authentication and authorization](/toolhive/guides-k8s/auth-k8s.md)).

## Ways to provide Cedar policies[​](#ways-to-provide-cedar-policies "Direct link to 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 separate [`MCPAuthzConfig`](/toolhive/reference/crds/mcpauthzconfig.md) resource that holds the policies. The same `MCPAuthzConfig` can 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[​](#step-1-create-authorization-configuration "Direct link to 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](/toolhive/reference/authz-policy-reference.md#how-groups-are-resolved).

You can also define custom resource attributes in `entities_json` for per-tool ownership or sensitivity labels.

tip

For more policy examples and advanced usage, see [Cedar policies](/toolhive/concepts/cedar-policies.md).

* ConfigMap
* Inline

**Step 2: Create a ConfigMap with policies**

Store your authorization configuration in a ConfigMap:

authz-configmap.yaml

```
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:

mcp-server-with-authz.yaml

```
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:

mcp-server-inline-authz.yaml

```
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[​](#share-policies-across-resources-with-mcpauthzconfig "Direct link to 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`](/toolhive/reference/crds/mcpauthzconfig.md) and reference it by name. The operator validates the policy bundle once and blocks deletion while any workload still references it.

mcp-authz-shared.yaml

```
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 support

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[​](#test-your-setup "Direct link to Test your setup")

1. Make requests that should be permitted by your policies
2. Make requests that should be denied
3. Check the proxy logs to see authorization decisions

## Next steps[​](#next-steps "Direct link to Next steps")

* [Configure rate limiting](/toolhive/guides-k8s/rate-limiting.md) to set per-user and shared request limits on MCP servers
* [Set up audit logging](/toolhive/guides-k8s/logging.md) to track authorization decisions and MCP server activity

## Related information[​](#related-information "Direct link to Related information")

* For the Cedar policy language and policy-writing patterns, see [Cedar policies](/toolhive/concepts/cedar-policies.md)
* For the complete dictionary of entity types, actions, and attributes, see [Authorization policy reference](/toolhive/reference/authz-policy-reference.md)
* For the Cedar policy language specification, see [Cedar documentation](https://docs.cedarpolicy.com/)
* For a complete end-to-end example with Okta OIDC and role-based access control, see [Role-based authorization with Okta](/toolhive/integrations/okta.md)
* For connecting a Virtual MCP Server to a corporate IdP with Cedar group-based access control, see [Connect ToolHive to an enterprise identity provider](/toolhive/integrations/vmcp-idp-overview.md)
* For the authentication approaches this authorization layer builds on, see [Authentication and authorization](/toolhive/guides-k8s/auth-k8s.md)

## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting")

Clients receive 401 Unauthorized

A 401 means token validation failed before the request reached your policy layer. Work through these checks in order:

1. **Check the token itself.** Decode the JWT (for example, paste it into [jwt.io](https://jwt.io/) - never use production tokens). Verify the `iss` claim matches your configured issuer URL exactly (including trailing paths like `/oauth2/default`), the `aud` claim matches the configured audience, and `exp` is in the future.
2. **Check the request.** The `Authorization` header must be `Authorization: Bearer <TOKEN>` with no extra whitespace or quoting.
3. **Check provider reachability.** ToolHive first fetches the OIDC discovery document from `${issuerUrl}/.well-known/openid-configuration`, then fetches signing keys from the `jwks_uri` listed in that document (or from the explicit `jwksUrl` if 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:

ToolHive CLI

```
# Container logs

thv logs <SERVER_NAME>



# Proxy logs (auth runs here)

thv logs <SERVER_NAME> --proxy



# Follow live

thv logs <SERVER_NAME> --proxy --follow
```

Kubernetes

```
# 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.

1. **Match principal, action, and resource exactly.** Cedar is case-sensitive. `Tool::"GetIssue"` is not the same as `Tool::"get_issue"`.
2. **Check policy conditions.** If your policy has a `when { ... }` clause, make sure the JWT actually contains the claim you reference (with the `claim_` prefix). For example, `principal.claim_groups.contains("writers")` requires a `groups` claim on the token.
3. **Use `has` for tool annotations.** Policies that read `resource.readOnlyHint`, `destructiveHint`, `idempotentHint`, or `openWorldHint` must guard the access with `resource has <attr>` first. Reading a missing attribute throws an evaluation error that ToolHive treats as a deny.
4. **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 in `group_claim_name`) is present and contains the expected values.

For the full attribute and entity reference, see [Authorization policy reference](/toolhive/reference/authz-policy-reference.md).

**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 the `name`/`namespace` reference.
* `AuthzConfigMapInvalid`: the ConfigMap exists but the payload is missing the configured `key`, 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>
```
