Skip to main content

Authentication and authorization

This guide shows you how to secure your MCP servers in Kubernetes using authentication and authorization with the ToolHive Operator.

Ways to authenticate MCP servers in Kubernetes

There are three ways to authenticate MCP servers in Kubernetes, and you can layer Cedar authorization on top of any of them:

Every approach attaches to your MCPServer (or MCPRemoteProxy) through one or more shared configuration CRDs. oidcConfigRef is required to enable token validation; authServerRef and authzConfig/authzConfigRef are optional add-ons for the embedded OAuth server and Cedar authorization, respectively:

Choose a backend authentication pattern

Everything above covers how clients authenticate to ToolHive. Once a request is authenticated, your MCP server or MCPRemoteProxy may separately need its own way to authenticate to the backend API it calls. Which pattern fits depends on that backend's relationship to your identity provider:

ScenarioPatternK8s guide
Backend only accepts API keys or static credentialsStatic credentialsRun a server with secrets (MCPServer), HashiCorp Vault integration, or inject custom headers (MCPRemoteProxy)
Backend trusts the same IdP as your clientsToken exchange (RFC 8693)Configure token exchange
Backend trusts a federated IdP (for example, AWS)Federated token exchangeAWS STS integration
Backend is an external API with no federation (for example, GitHub)Embedded authorization serverRun an embedded OAuth server

For the full comparison and why each pattern fits its scenario, see Choosing the right backend authentication pattern.

Authenticate with OIDC

Both external IdP and Kubernetes service-to-service authentication use the same MCPOIDCConfig CRD, which lets you define OIDC provider settings once and reference them from multiple MCPServer or VirtualMCPServer resources. Each server specifies its own audience (and optionally scopes) to maintain token isolation.

Before you begin, make sure you have:

  • ToolHive installed and working
  • Basic familiarity with OAuth, OIDC, and JWT concepts
  • An identity provider that supports OpenID Connect (OIDC), such as Google, GitHub, Microsoft Entra ID (Azure AD), Okta, Auth0, or Kubernetes (for service accounts)

If you're connecting to an external identity provider, the only value you need from it is the issuer URL. ToolHive uses it to fetch the JWKS automatically via OIDC discovery (${issuerUrl}/.well-known/openid-configuration), so you don't need to look up the JWKS URL yourself unless your provider doesn't support discovery.

Kubernetes service accounts don't need any of this. ToolHive validates those tokens against the cluster's own OIDC issuer and keys automatically.

Two more values matter, but neither is always required:

  • Audience: a value you choose to identify this MCP server, not something the provider generates. ToolHive checks that the token's aud claim matches it; some providers let you register the value so they include it in issued tokens, but ToolHive doesn't require that.
  • Client ID and secret: only needed if your provider validates tokens by introspection rather than issuing JWTs (for example, GitHub). When you do need them, they're the credentials the provider issues when you register an OAuth application with them. Plain JWT validation never uses them.

ToolHive uses OIDC to connect to your existing identity provider, so you can authenticate with your own credentials (for example, Google login) or with service account tokens (for example, in Kubernetes). ToolHive never sees your password, only signed tokens from your identity provider.

For background on authentication, authorization, and Cedar policy examples, see Authentication and authorization framework.

Connect to an external identity provider

Use this when you want to authenticate users or external services using an external identity provider.

external-oidc-config.yaml
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPOIDCConfig
metadata:
name: production-oidc
namespace: toolhive-system
spec:
type: inline
inline:
issuer: 'https://auth.example.com'
clientId: 'your-client-id'
clientSecretRef:
name: oidc-secret
key: client-secret
jwksUrl: 'https://auth.example.com/.well-known/jwks.json'
kubectl apply -f external-oidc-config.yaml

Reference it from an MCPServer using oidcConfigRef instead of inline oidcConfig. Each server must set a unique audience to prevent token replay across servers:

mcp-server-external-auth.yaml
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPServer
metadata:
name: weather-server
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: streamable-http
proxyPort: 8080
oidcConfigRef:
name: production-oidc
audience: weather-server
scopes:
- openid
kubectl apply -f mcp-server-external-auth.yaml

Clients connecting to this MCP server must include a valid JWT token from your configured identity provider in their requests. The ToolHive proxy validates the token before allowing access to the MCP server. How you obtain that token varies by identity provider - consult your provider's documentation for interactive user login (OAuth 2.0 Authorization Code flow), service-to-service login (Client Credentials flow), or API token generation.

Check the MCPOIDCConfig status to confirm it's valid and see which workloads reference it:

kubectl get mcpoidc -n toolhive-system

The REFERENCES column shows which workloads use this config. The VALID column confirms validation passed.

Upgrading from before v0.21.0?

Older releases supported an inline spec.oidcConfig field directly on MCPServer. That field was removed in v0.21.0 in favor of oidcConfigRef with a shared MCPOIDCConfig resource. See Inline oidcConfig removed for the migration steps.

Use a custom CA certificate for the OIDC issuer

If your OIDC provider serves its endpoints with a certificate signed by a non-public CA, such as a corporate Keycloak instance with an internal CA, ToolHive can't reach the issuer's discovery and JWKS endpoints over TLS until it trusts that CA. Use caBundleRef to point the MCPOIDCConfig at a ConfigMap containing the CA bundle. ToolHive auto-mounts the ConfigMap and uses it when connecting to the issuer.

First, create a ConfigMap with the CA certificate. The ConfigMap must live in the same namespace as the MCPServer that references the config:

kubectl create configmap corporate-ca-bundle \
--from-file=ca.crt=/path/to/corporate-ca.crt \
-n toolhive-system

Then reference it from the MCPOIDCConfig with caBundleRef:

corporate-oidc-config.yaml
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPOIDCConfig
metadata:
name: corporate-oidc
namespace: toolhive-system
spec:
type: inline
inline:
issuer: 'https://keycloak.corp.example.com/realms/myrealm'
clientId: 'your-client-id'
clientSecretRef:
name: oidc-secret
key: client-secret
caBundleRef:
configMapRef:
name: corporate-ca-bundle
key: ca.crt

Set key to the ConfigMap key that holds the bundle, commonly ca.crt. After you apply the config and point an MCPServer's oidcConfigRef at corporate-oidc, check the CABundleRefValidated condition on that MCPServer to confirm the CA bundle resolved:

kubectl get mcpserver <YOUR_SERVER_NAME> -n toolhive-system \
-o jsonpath='{.status.conditions[?(@.type=="CABundleRefValidated")]}'

If the ConfigMap is missing or the key isn't found, the condition reports a False status with a reason of CABundleRefNotFound or CABundleRefInvalid.

Authenticate Kubernetes service-to-service traffic

Use this when you have client applications running in the same Kubernetes cluster that need to call MCP servers. This uses the same MCPOIDCConfig CRD as above, with type: kubernetesServiceAccount instead of inline.

Step 1: Create a service account for the client application

client-service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: mcp-client
namespace: client-apps
kubectl apply -f client-service-account.yaml

Step 2: Create MCPOIDCConfig and MCPServer for service-to-service auth

mcp-server-k8s-auth.yaml
apiVersion: toolhive.stacklok.dev/v1beta1
kind: MCPOIDCConfig
metadata:
name: k8s-sa-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-k8s
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: sse
proxyPort: 8080
# Authentication configuration for Kubernetes service accounts
oidcConfigRef:
name: k8s-sa-oidc
audience: 'toolhive'
resources:
limits:
cpu: '100m'
memory: '128Mi'
requests:
cpu: '50m'
memory: '64Mi'

This configuration only allows requests from pods using the mcp-client service account in the client-apps namespace.

kubectl apply -f mcp-server-k8s-auth.yaml

Step 3: Deploy the client application with the service account

client-app.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-client-app
namespace: client-apps
spec:
replicas: 1
selector:
matchLabels:
app: mcp-client-app
template:
metadata:
labels:
app: mcp-client-app
spec:
serviceAccountName: mcp-client
containers:
- name: client
image: your-client-app:latest
env:
- name: MCP_SERVER_URL
value: 'http://weather-server-k8s.toolhive-system.svc.cluster.local:8080'
kubectl apply -f client-app.yaml

Your client application can now authenticate to the MCP server using its Kubernetes service account token, which is automatically mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

Run an embedded OAuth server

Use this when you want ToolHive to handle the full OAuth flow, including redirecting users to an upstream identity provider for authentication. This approach is ideal for MCP servers that accept Authorization: Bearer tokens but have no federation relationship with your identity provider, such as GitHub, Google Workspace, or Atlassian.

Setup requires an upstream identity provider that supports the OAuth 2.0 authorization code flow, a registered OAuth application with that provider, and Secrets for the client credentials and JWT signing keys. For conceptual background, see Embedded authorization server. For the full walkthrough, see Set up the embedded authorization server in Kubernetes.

Add Cedar authorization

All three authentication approaches above can layer the same Cedar-based authorization on top. You can provide policies inline on the MCPServer, in a ConfigMap, or in a shared MCPAuthzConfig resource referenced from multiple workloads. For the full walkthrough, see Set up Cedar authorization in Kubernetes. For the Cedar policy language and writing patterns, see Cedar policies.

Test your setup

Test external IdP authentication

  1. Deploy the external IdP configuration
  2. Obtain a valid JWT token from your identity provider
  3. Make a request to the MCP server including the token

Test service-to-service authentication

  1. Deploy both the MCP server and client application
  2. Check that the client can successfully call the MCP server
  3. Verify authentication in the ToolHive proxy logs:
kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-k8s

Next steps

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

Kubernetes-specific issues

MCPServer resource issues:

  • Check the MCPServer status: kubectl get mcpserver -n toolhive-system
  • Describe the resource for details: kubectl describe mcpserver weather-server-k8s -n toolhive-system

Service account issues:

  • Verify the service account exists: kubectl get sa -n client-apps mcp-client
  • Check RBAC permissions if needed

OIDC configuration issues:

  • For external IdP: Ensure the issuer URL is accessible from within the cluster
  • For Kubernetes auth: Ensure the Kubernetes API server has OIDC enabled
  • Check that the JWKS URL returns valid keys
  • Verify the MCPOIDCConfig resource is valid: kubectl get mcpoidc -n toolhive-system

Network connectivity:

  • Verify pods can reach the Kubernetes API server
  • Check cluster DNS resolution
  • Test service-to-service connectivity: kubectl exec -n client-apps deployment/mcp-client-app -- curl http://weather-server-k8s.toolhive-system.svc.cluster.local:8080

ToolHive Operator issues:

  • Check operator logs: kubectl logs -n toolhive-system -l app.kubernetes.io/name=toolhive-operator
  • Verify the operator is running: kubectl get pods -n toolhive-system

For embedded-authorization-server-specific troubleshooting, see Set up the embedded authorization server: Troubleshooting. For Cedar authorization troubleshooting, see Set up Cedar authorization: Troubleshooting.