Authentication and authorization
This guide shows you how to secure your MCP servers in Kubernetes using authentication and authorization with the ToolHive Operator.
Authentication and authorization are emerging capabilities in the MCP ecosystem. The official MCP authorization specification is still evolving, and client support for these features is limited. ToolHive is leading the way in implementing these capabilities, but you may encounter some limitations with certain clients.
Prerequisites
You'll need:
- Kubernetes cluster with RBAC enabled
- ToolHive Operator installed (see Deploy the ToolHive Operator)
kubectlaccess to your cluster
Choose your authentication approach
There are four main ways to authenticate with MCP servers running in Kubernetes:
Approach 1: External identity provider authentication
Use this when you want to authenticate users or external services using providers like Google, GitHub, Microsoft Entra ID, Okta, or Auth0.
Prerequisites for external IdP:
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)
From your identity provider, you'll need:
- Client ID
- Audience value
- Issuer URL
- JWKS URL (for key verification)
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.
Approach 2: Shared OIDC configuration with ConfigMap
Use this when you want to share the same OIDC configuration across multiple MCPServers. This is ideal for managing multiple servers with the same external identity provider.
Prerequisites for shared OIDC:
- External identity provider configured (same as Approach 1)
- Understanding of Kubernetes ConfigMaps
Approach 3: Kubernetes service-to-service authentication
Use this when you have client applications running in the same Kubernetes cluster that need to call MCP servers. This approach uses Kubernetes service account tokens for authentication.
Prerequisites for service-to-service:
- Client applications running in Kubernetes pods
- Understanding of Kubernetes service accounts and RBAC
Approach 4: Embedded authorization server authentication
Use this when your MCP clients support the MCP OAuth specification and you want
ToolHive to handle the full OAuth flow, including redirecting users to an
upstream identity provider for authentication. This approach is ideal when you
need ToolHive to retrieve OAuth tokens from an upstream provider for MCP servers
that accept Authorization: Bearer tokens.
For conceptual background, see Embedded authorization server.
Prerequisites for embedded authorization server:
- An upstream identity provider that supports the OAuth 2.0 authorization code flow (such as Okta, Microsoft Entra ID, Auth0, or any OIDC-compliant provider)
- A registered OAuth application/client with your upstream provider
- Client ID and client secret from your upstream provider
Set up external identity provider authentication
Step 1: Create an MCPServer with external OIDC
Create an MCPServer resource configured to accept tokens from your external
identity provider. The ToolHive proxy will handle authentication before
forwarding requests to the MCP server.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServer
metadata:
name: weather-server-external
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: sse
port: 8080
permissionProfile:
type: builtin
name: network
# Authentication configuration for external IdP
oidcConfig:
type: inline
inline:
issuer: 'https://your-oidc-issuer.com'
audience: 'your-audience'
clientId: 'your-client-id'
jwksUrl: 'https://your-oidc-issuer.com/path/to/jwks'
resources:
limits:
cpu: '100m'
memory: '128Mi'
requests:
cpu: '50m'
memory: '64Mi'
Replace the OIDC placeholders with your actual identity provider configuration.
Step 2: Apply the MCPServer resource
kubectl apply -f mcp-server-external-auth.yaml
Step 3: Test external authentication
Clients connecting to this MCP server must include a valid JWT token from your configured identity provider in their requests. The ToolHive proxy will validate the token before allowing access to the MCP server.
How to obtain JWT tokens varies by identity provider and is outside the scope of this guide. Consult your identity provider's documentation for specific instructions on:
- Interactive user authentication flows (OAuth 2.0 Authorization Code flow)
- Service-to-service authentication (Client Credentials flow)
- API token generation and management
For Kubernetes service accounts, tokens are automatically mounted at
/var/run/secrets/kubernetes.io/serviceaccount/token in pods.
Set up shared OIDC configuration with ConfigMap
Step 1: Create OIDC ConfigMap
Create a ConfigMap containing the OIDC configuration:
apiVersion: v1
kind: ConfigMap
metadata:
name: shared-oidc-config
namespace: toolhive-system
data:
oidc.json: |
{
"issuer": "https://auth.example.com",
"audience": "https://mcp.example.com",
"clientId": "shared-client-id",
"jwksUrl": "https://auth.example.com/.well-known/jwks.json"
}
kubectl apply -f shared-oidc-config.yaml
Step 2: Reference ConfigMap in MCPServer
Create MCPServer resources that reference the shared configuration:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServer
metadata:
name: weather-server-shared-oidc
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: sse
port: 8080
permissionProfile:
type: builtin
name: network
# Reference shared OIDC configuration
oidcConfig:
type: configMap
configMap:
name: shared-oidc-config
key: oidc.json
resources:
limits:
cpu: '100m'
memory: '128Mi'
requests:
cpu: '50m'
memory: '64Mi'
kubectl apply -f mcp-server-with-configmap-oidc.yaml
Benefits of ConfigMap approach
- Centralized management: Update OIDC settings in one place
- Consistency: Ensure all MCPServers use identical authentication config
- GitOps friendly: Manage configuration separately from MCPServer resources
- Multi-server deployments: Deploy multiple servers with same auth easily
Set up Kubernetes service-to-service authentication
This approach is ideal when you have client applications running in the same Kubernetes cluster that need to call MCP servers.
Step 1: Create service account for client application
Create a service account that your client application will use:
apiVersion: v1
kind: ServiceAccount
metadata:
name: mcp-client
namespace: client-apps
kubectl apply -f client-service-account.yaml
Step 2: Create MCPServer for service-to-service auth
Create an MCPServer resource configured to accept Kubernetes service account
tokens:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServer
metadata:
name: weather-server-k8s
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: sse
port: 8080
permissionProfile:
type: builtin
name: network
# Authentication configuration for Kubernetes service accounts
oidcConfig:
type: kubernetes
kubernetes:
serviceAccount: 'mcp-client'
namespace: 'client-apps'
audience: 'toolhive'
issuer: 'https://kubernetes.default.svc'
jwksUrl: 'https://kubernetes.default.svc/openid/v1/jwks'
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 client application with service account
Deploy your client application using 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.
Set up embedded authorization server authentication
The embedded authorization server runs an OAuth authorization server within the
ToolHive proxy. It handles the full OAuth flow by redirecting users to your
upstream identity provider for authentication, then issuing JWTs that the proxy
validates on subsequent requests. This provides MCP servers with
Authorization: Bearer tokens without requiring separate authorization server
infrastructure.
This setup uses the MCPExternalAuthConfig custom resource, following the same
pattern as token exchange configuration.
Step 1: Create a Secret for the upstream provider client credentials
Store the OAuth client secret for your upstream identity provider:
apiVersion: v1
kind: Secret
metadata:
name: upstream-idp-secret
namespace: toolhive-system
type: Opaque
stringData:
client-secret: '<YOUR_UPSTREAM_CLIENT_SECRET>'
kubectl apply -f upstream-idp-secret.yaml
Step 2: Create a Secret for JWT signing keys
The embedded authorization server signs JWTs with a private key you provide. Generate a PEM-encoded private key (RSA or EC), for example:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out signing-key.pem
Then create a Secret containing the key:
apiVersion: v1
kind: Secret
metadata:
name: auth-server-signing-key
namespace: toolhive-system
type: Opaque
stringData:
signing-key: |
-----BEGIN PRIVATE KEY-----
<YOUR_PEM_ENCODED_PRIVATE_KEY>
-----END PRIVATE KEY-----
kubectl apply -f auth-server-signing-key.yaml
For key rotation, you can reference multiple signing key Secrets in the
signingKeySecretRefs list. The first key is used for signing new tokens.
Additional keys are used for verification only, so tokens signed before rotation
remain valid.
Step 3: Create a Secret for HMAC keys
The embedded authorization server uses a symmetric HMAC key to sign authorization codes and refresh tokens. The key must be at least 32 bytes and cryptographically random, for example:
openssl rand -base64 32
apiVersion: v1
kind: Secret
metadata:
name: auth-server-hmac-secret
namespace: toolhive-system
type: Opaque
stringData:
hmac-key: '<YOUR_CRYPTOGRAPHICALLY_RANDOM_KEY>'
kubectl apply -f auth-server-hmac-secret.yaml
If you omit the signingKeySecretRefs and hmacSecretRefs fields, ToolHive
generates ephemeral keys that are lost on pod restart. All previously issued
tokens become invalid after a restart. Only omit these Secrets for development
and testing.
Step 4: Create the MCPExternalAuthConfig resource
Create an MCPExternalAuthConfig resource with the embeddedAuthServer type.
This example configures an OIDC upstream provider (the most common case):
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPExternalAuthConfig
metadata:
name: embedded-auth-server
namespace: toolhive-system
spec:
type: embeddedAuthServer
embeddedAuthServer:
issuer: 'https://mcp.example.com'
signingKeySecretRefs:
- name: auth-server-signing-key
key: signing-key
hmacSecretRefs:
- name: auth-server-hmac-secret
key: hmac-key
tokenLifespans:
accessTokenLifespan: '1h'
refreshTokenLifespan: '168h'
authCodeLifespan: '10m'
upstreamProviders:
- name: okta
type: oidc
oidcConfig:
issuerUrl: 'https://dev-123456.okta.com/oauth2/default'
clientId: '<YOUR_OKTA_CLIENT_ID>'
clientSecretRef:
name: upstream-idp-secret
key: client-secret
scopes:
- openid
- offline_access
- profile
- email
kubectl apply -f embedded-auth-config.yaml
Configuration reference:
| Field | Description |
|---|---|
issuer | HTTPS URL identifying this authorization server. Appears in the iss claim of issued JWTs. |
signingKeySecretRefs | References to Secrets containing JWT signing keys. First key is active; additional keys support rotation. |
hmacSecretRefs | References to Secrets with symmetric keys for signing authorization codes and refresh tokens. |
tokenLifespans | Configurable durations for access tokens (default: 1h), refresh tokens (default: 168h), and auth codes (default: 10m). |
upstreamProviders | Configuration for the upstream identity provider. Currently supports one provider. |
Step 5: Create the MCPServer resource
Create an MCPServer resource that references the embedded authorization server
configuration. The MCPServer uses two fields together:
externalAuthConfigRef: references the embedded authorization server configuration you created in step 4oidcConfig: validates JWTs issued by the embedded authorization server, with the issuer pointed at the embedded authorization server itself
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServer
metadata:
name: weather-server-embedded
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: streamable-http
port: 8080
permissionProfile:
type: builtin
name: network
# Reference the embedded authorization server configuration
externalAuthConfigRef:
name: embedded-auth-server
# Validate JWTs issued by the embedded authorization server
oidcConfig:
type: inline
resourceUrl: 'https://mcp.example.com'
inline:
issuer: 'https://mcp.example.com'
resources:
limits:
cpu: '100m'
memory: '128Mi'
requests:
cpu: '50m'
memory: '64Mi'
kubectl apply -f mcp-server-embedded-auth.yaml
The oidcConfig issuer must match the issuer in your MCPExternalAuthConfig.
The embedded authorization server exposes a JWKS endpoint that the proxy uses to
validate the JWTs it issues. The proxy also exposes OAuth discovery endpoints
(/.well-known/oauth-authorization-server) so MCP clients can discover the
authorization endpoints automatically.
Using an OAuth 2.0 upstream provider
If your upstream identity provider does not support OIDC discovery, you can configure it as an OAuth 2.0 provider with explicit endpoints. This is useful for providers like GitHub that use OAuth 2.0 but don't implement the full OIDC specification.
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPExternalAuthConfig
metadata:
name: embedded-auth-oauth2
namespace: toolhive-system
spec:
type: embeddedAuthServer
embeddedAuthServer:
issuer: 'https://mcp.example.com'
signingKeySecretRefs:
- name: auth-server-signing-key
key: signing-key
hmacSecretRefs:
- name: auth-server-hmac-secret
key: hmac-key
upstreamProviders:
- name: github
type: oauth2
oauth2Config:
authorizationEndpoint: 'https://github.com/login/oauth/authorize'
tokenEndpoint: 'https://github.com/login/oauth/access_token'
userInfo:
endpointUrl: 'https://api.github.com/user'
httpMethod: GET
additionalHeaders:
Accept: 'application/vnd.github+json'
fieldMapping:
subjectFields:
- id
- login
nameFields:
- name
- login
emailFields:
- email
clientId: '<YOUR_GITHUB_CLIENT_ID>'
clientSecretRef:
name: upstream-idp-secret
key: client-secret
scopes:
- user:email
- read:user
OAuth 2.0 providers require explicit endpoint configuration and a userInfo
section, unlike OIDC providers which auto-discover these from the issuer URL.
The fieldMapping section maps provider-specific response fields to standard
user identity fields. For example, GitHub returns login instead of the
standard name field.
Set up authorization
All authentication approaches can use the same authorization configuration using Cedar policies.
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": "[]"
}
}
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.
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
Add the authorization configuration to your MCPServer resources:
apiVersion: toolhive.stacklok.dev/v1alpha1
kind: MCPServer
metadata:
name: weather-server-with-authz
namespace: toolhive-system
spec:
image: ghcr.io/stackloklabs/weather-mcp/server
transport: sse
port: 8080
permissionProfile:
type: builtin
name: network
# Authentication configuration
oidcConfig:
type: kubernetes
kubernetes:
serviceAccount: 'mcp-client'
namespace: 'client-apps'
audience: 'toolhive'
issuer: 'https://kubernetes.default.svc'
jwksUrl: 'https://kubernetes.default.svc/openid/v1/jwks'
# 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
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
Test embedded authorization server authentication
-
Deploy the
MCPExternalAuthConfigandMCPServerresources -
Check that the MCPServer is running:
kubectl get mcpserver -n toolhive-system weather-server-embedded -
If the server is exposed outside the cluster, verify the OAuth discovery endpoint is available:
curl https://<YOUR_SERVER_URL>/.well-known/oauth-authorization-server -
Connect with an MCP client that supports the MCP OAuth specification. The client should be redirected to your upstream identity provider for authentication.
-
Check the proxy logs for successful authentication:
kubectl logs -n toolhive-system \
-l app.kubernetes.io/name=weather-server-embedded
Test authorization
- Make requests that should be permitted by your policies
- Make requests that should be denied
- Check the proxy logs to see authorization decisions
Related information
- For conceptual understanding, see Authentication and authorization framework
- For conceptual background on the embedded authorization server, see Embedded authorization server
- For a similar configuration pattern using token exchange, see Configure token exchange
- For detailed Cedar policy syntax, see Cedar policies and the Cedar documentation
- For running MCP servers without authentication, see Run MCP servers in Kubernetes
- For ToolHive Operator installation, see Deploy the ToolHive Operator
Troubleshooting
Authentication issues
If clients can't authenticate:
-
Check that the JWT token is valid and not expired
-
Verify that the audience and issuer match your configuration
-
Ensure the JWKS URL is accessible
-
Check the server logs for specific authentication errors:
# View logs
thv logs <server-name>
# Follow logs in real-time (like tail -f)
thv logs <server-name> --follow
# View proxy logs instead of container logs
thv logs <server-name> --proxy
Authorization issues
If authenticated clients are denied access:
- Make sure your Cedar policies explicitly permit the specific action (remember, default deny)
- Check that the principal, action, and resource match what's in your policies (including case and formatting)
- Examine any conditions in your policies to ensure they're satisfied (for example, required JWT claims or tool arguments)
- Remember that Cedar uses a default deny policy—if no policy explicitly permits an action, it will be denied
Troubleshooting tip: If access is denied, check that your policies explicitly permit the action. Cedar uses a default deny model—if no policy matches, the request is denied.
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
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
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
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
Embedded authorization server issues
OAuth flow not initiating:
- Verify the
MCPExternalAuthConfigresource exists in the same namespace:kubectl get mcpexternalauthconfig -n toolhive-system - Check that the
externalAuthConfigRef.namein yourMCPServermatches theMCPExternalAuthConfigresource name - Verify the upstream provider's client ID and redirect URI are correctly
configured in the
MCPExternalAuthConfig
Token validation failures after restart:
- Ensure you have configured
signingKeySecretRefsandhmacSecretRefswith persistent keys - Without these, ephemeral keys are generated on startup, invalidating all previously issued tokens
Upstream IdP redirect errors:
- Verify the redirect URI configured in your upstream provider matches the
ToolHive proxy's callback URL (typically
https://<YOUR_SERVER_URL>/oauth/callback) - Check that the upstream provider's issuer URL is accessible from within the cluster
- For OIDC providers, ensure the
/.well-known/openid-configurationendpoint is reachable from the proxy pod
JWT signing key issues:
- Verify signing key Secrets exist:
kubectl get secret -n toolhive-system auth-server-signing-key - Ensure the key format is correct (PEM-encoded RSA or EC private key)
- Check proxy logs for key loading errors:
kubectl logs -n toolhive-system -l app.kubernetes.io/name=weather-server-embedded
OIDC configuration mismatch:
- Ensure the
oidcConfig.inline.issueron yourMCPServermatches theissuerin yourMCPExternalAuthConfig - Verify the
resourceUrlinoidcConfigmatches the external URL of the MCP server