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.

info

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:

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.

mcp-server-external-auth.yaml
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.

Obtaining JWT tokens

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:

shared-oidc-config.yaml
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:

mcp-server-with-configmap-oidc.yaml
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:

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 MCPServer for service-to-service auth

Create an MCPServer resource configured to accept Kubernetes service account tokens:

mcp-server-k8s-auth.yaml
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:

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.

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:

upstream-idp-secret.yaml
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:

auth-server-signing-key.yaml
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
Key rotation

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
auth-server-hmac-secret.yaml
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
Ephemeral keys for development only

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

embedded-auth-config.yaml
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:

FieldDescription
issuerHTTPS URL identifying this authorization server. Appears in the iss claim of issued JWTs.
signingKeySecretRefsReferences to Secrets containing JWT signing keys. First key is active; additional keys support rotation.
hmacSecretRefsReferences to Secrets with symmetric keys for signing authorization codes and refresh tokens.
tokenLifespansConfigurable durations for access tokens (default: 1h), refresh tokens (default: 168h), and auth codes (default: 10m).
upstreamProvidersConfiguration 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 4
  • oidcConfig: validates JWTs issued by the embedded authorization server, with the issuer pointed at the embedded authorization server itself
mcp-server-embedded-auth.yaml
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
note

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.

embedded-auth-oauth2-config.yaml
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
note

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.

tip

For more policy examples and advanced usage, see Cedar policies.

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

Add the authorization configuration to your MCPServer resources:

mcp-server-with-authz.yaml
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

  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

Test embedded authorization server authentication

  1. Deploy the MCPExternalAuthConfig and MCPServer resources

  2. Check that the MCPServer is running:

    kubectl get mcpserver -n toolhive-system weather-server-embedded
  3. 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
  4. Connect with an MCP client that supports the MCP OAuth specification. The client should be redirected to your upstream identity provider for authentication.

  5. Check the proxy logs for successful authentication:

    kubectl logs -n toolhive-system \
    -l app.kubernetes.io/name=weather-server-embedded

Test authorization

  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

Troubleshooting

Authentication issues

If clients can't authenticate:

  1. Check that the JWT token is valid and not expired

  2. Verify that the audience and issuer match your configuration

  3. Ensure the JWKS URL is accessible

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

  1. Make sure your Cedar policies explicitly permit the specific action (remember, default deny)
  2. Check that the principal, action, and resource match what's in your policies (including case and formatting)
  3. Examine any conditions in your policies to ensure they're satisfied (for example, required JWT claims or tool arguments)
  4. 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 MCPExternalAuthConfig resource exists in the same namespace: kubectl get mcpexternalauthconfig -n toolhive-system
  • Check that the externalAuthConfigRef.name in your MCPServer matches the MCPExternalAuthConfig resource 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 signingKeySecretRefs and hmacSecretRefs with 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-configuration endpoint 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.issuer on your MCPServer matches the issuer in your MCPExternalAuthConfig
  • Verify the resourceUrl in oidcConfig matches the external URL of the MCP server