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:
- Connect to an external identity provider
- authenticate users or external services with providers like Google, GitHub, Microsoft Entra ID, Okta, or Auth0.
- Authenticate Kubernetes service-to-service traffic
- authenticate client applications running in the same cluster using their Kubernetes service account tokens.
- Run an embedded OAuth server
- let ToolHive handle the full OAuth flow, including redirecting users to an upstream identity provider, for MCP servers or remote endpoints with no federation relationship to your IdP.
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:
| Scenario | Pattern | K8s guide |
|---|---|---|
| Backend only accepts API keys or static credentials | Static credentials | Run a server with secrets (MCPServer), HashiCorp Vault integration, or inject custom headers (MCPRemoteProxy) |
| Backend trusts the same IdP as your clients | Token exchange (RFC 8693) | Configure token exchange |
| Backend trusts a federated IdP (for example, AWS) | Federated token exchange | AWS STS integration |
| Backend is an external API with no federation (for example, GitHub) | Embedded authorization server | Run 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
audclaim 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.
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:
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.
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:
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
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
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
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
- Deploy the external IdP configuration
- Obtain a valid JWT token from your identity provider
- Make a request to the MCP server including the token
Test service-to-service authentication
- Deploy both the MCP server and client application
- Check that the client can successfully call the MCP server
- Verify authentication in the ToolHive proxy logs:
kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-k8s
Next steps
- Set up the embedded authorization server for OAuth-based access to MCP servers with no IdP federation
- Set up Cedar authorization to control what authenticated clients can do
- Configure token exchange to let MCP servers authenticate to backend services, or set up AWS STS specifically for AWS-hosted backends
- Set up audit logging to track authentication decisions and MCP server activity
Related information
- For conceptual understanding, see Authentication and authorization framework
- 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 (Entra ID or Okta) with Cedar group-based access control, see Connect ToolHive to an enterprise identity provider
- For running MCP servers without authentication, see Run MCP servers in Kubernetes
- For ToolHive Operator installation, see Deploy the ToolHive Operator
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.
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.