Authentication configuration
The Registry server provides secure-by-default authentication, defaulting to OAuth mode to protect your registry. You can configure authentication to fit different deployment scenarios, from development environments to production deployments with enterprise identity providers.
Authentication modes
The server supports two authentication modes configured via the required auth
section in your configuration file:
- OAuth (default): Secure authentication using JWT tokens from identity providers
- Anonymous: No authentication (development/testing only)
The server defaults to OAuth mode when no explicit auth configuration is provided. This secure-by-default posture ensures your registry is protected unless you explicitly choose anonymous mode for development scenarios.
OAuth authentication
OAuth mode (the default) validates access tokens from identity providers. The server supports two token formats:
- JWT tokens: Validated locally using the provider's public keys (JWKS endpoint). This is the most common format for modern identity providers.
- Opaque tokens: Validated via token introspection (RFC 7662) by querying the provider's introspection endpoint. Use this when your provider issues non-JWT tokens.
This enables enterprise authentication with providers like Keycloak, Auth0, Okta, Azure AD, Kubernetes service accounts, or any OAuth-compliant service.
Basic OAuth configuration
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: keycloak
issuerUrl: https://keycloak.example.com/realms/mcp
audience: registry-api
OAuth configuration fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
mode | string | Yes | oauth | Authentication mode (oauth or anonymous) |
resourceUrl | string | Yes | - | The URL of the registry resource being protected |
realm | string | No | mcp-registry | OAuth realm identifier |
scopesSupported | []string | No | [mcp-registry:read, mcp-registry:write] | OAuth scopes advertised in the discovery endpoint |
publicPaths | []string | No | [] | Additional paths accessible without authentication |
providers | array | Yes | - | List of OAuth/OIDC identity providers |
Provider configuration fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Provider identifier for logging and monitoring |
issuerUrl | string | Yes | OAuth/OIDC issuer URL (e.g., https://keycloak.example.com/realms/mcp) |
audience | string | Yes | Expected audience claim in the access token |
jwksUrl | string | No | JWKS endpoint URL (skips OIDC discovery if specified) |
introspectionUrl | string | No | Token introspection endpoint URL for opaque token validation |
clientId | string | No | OAuth client ID (for authenticated introspection requests) |
clientSecretFile | string | No | Path to file containing client secret (for authenticated introspection) |
caCertPath | string | No | Path to CA certificate for TLS verification |
authTokenFile | string | No | Path to token file for authenticating JWKS/introspection requests |
allowPrivateIP | bool | No | Allow connections to private IP addresses (for in-cluster use) |
Complete OAuth configuration example
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
realm: mcp-registry
scopesSupported:
- mcp-registry:read
- mcp-registry:write
publicPaths:
- /custom-health
- /metrics
providers:
- name: keycloak-prod
issuerUrl: https://keycloak.example.com/realms/production
audience: registry-api
clientId: registry-client
clientSecretFile: /etc/secrets/keycloak-secret
caCertPath: /etc/ssl/certs/keycloak-ca.crt
- name: keycloak-staging
issuerUrl: https://keycloak.example.com/realms/staging
audience: registry-api-staging
Opaque token configuration
When your identity provider issues opaque (non-JWT) tokens, configure the
introspectionUrl field to enable token introspection:
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: google
issuerUrl: https://accounts.google.com
introspectionUrl: https://oauth2.googleapis.com/tokeninfo
audience: 407408718192.apps.googleusercontent.com
The server automatically detects the token format and uses the appropriate validation method—attempting JWT validation first and falling back to token introspection if needed.
Kubernetes authentication
For Kubernetes deployments, you can configure OAuth to validate service account tokens. This provides automatic, zero-config authentication for workloads running in the cluster.
Kubernetes provider configuration
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: kubernetes
issuerUrl: https://kubernetes.default.svc.cluster.local
jwksUrl: https://kubernetes.default.svc/openid/v1/jwks
audience: registry-server
caCertPath: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
authTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
allowPrivateIP: true
- issuerUrl: Use
https://kubernetes.default.svc.cluster.localto match theissclaim in Kubernetes service account tokens. - jwksUrl: Specify the JWKS endpoint directly to skip OIDC discovery.
- authTokenFile: The server uses this token to authenticate when fetching the JWKS from the Kubernetes API server.
- allowPrivateIP: Required for in-cluster communication with the API server.
How Kubernetes authentication works
- Workloads mount projected service account tokens with a specific audience
- Clients send these tokens in the
Authorization: Bearer <TOKEN>header - The server validates tokens using the Kubernetes API server's public keys
- Only tokens with the correct audience (e.g.,
registry-server) are accepted
Kubernetes deployment example
When deploying in Kubernetes, the service account CA certificate is automatically mounted:
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-api
spec:
replicas: 1
selector:
matchLabels:
app: registry-api
template:
metadata:
labels:
app: registry-api
spec:
serviceAccountName: registry-api
containers:
- name: registry-api
image: ghcr.io/stacklok/thv-registry-api:latest
args:
- serve
- --config=/etc/registry/config.yaml
volumeMounts:
- name: config
mountPath: /etc/registry/config.yaml
subPath: config.yaml
readOnly: true
# Service account token and CA cert are mounted automatically by Kubernetes
volumes:
- name: config
configMap:
name: registry-api-config
The service account token and CA certificate are automatically mounted at:
- Token:
/var/run/secrets/kubernetes.io/serviceaccount/token - CA cert:
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
Client workload example
Clients that need to authenticate with the registry should mount a projected service account token with the correct audience:
apiVersion: v1
kind: Pod
metadata:
name: registry-client
spec:
serviceAccountName: my-client-sa
containers:
- name: client
image: my-client-image
volumeMounts:
- name: registry-token
mountPath: /var/run/secrets/registry
readOnly: true
volumes:
- name: registry-token
projected:
sources:
- serviceAccountToken:
audience: registry-server
expirationSeconds: 3600
path: token
The client reads the token from /var/run/secrets/registry/token and includes
it in the Authorization: Bearer <TOKEN> header when making requests to the
registry.
Provider-specific examples
Keycloak
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: keycloak
issuerUrl: https://keycloak.example.com/realms/YOUR_REALM
audience: registry-api
The issuerUrl should point to your Keycloak realm. The realm name is part of
the URL path.
Auth0
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: auth0
issuerUrl: https://YOUR_DOMAIN.auth0.com/
audience: https://registry.example.com
For Auth0, the issuerUrl is your Auth0 domain. The audience should match the
API identifier configured in your Auth0 dashboard.
Azure AD
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: azure-ad
issuerUrl: https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0
audience: api://YOUR_CLIENT_ID
For Azure AD, the issuerUrl includes your tenant ID, and the audience
typically uses the api:// scheme with your application's client ID.
Okta
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com
providers:
- name: okta
issuerUrl: https://YOUR_DOMAIN.okta.com/oauth2/default
audience: api://default
For Okta, the issuerUrl points to your Okta authorization server. Use
/oauth2/default for the default authorization server or
/oauth2/YOUR_AUTH_SERVER_ID for custom servers.
Anonymous authentication
Anonymous mode disables authentication entirely, allowing unrestricted access to all registry endpoints. This is only suitable for development, testing, or internal deployments where authentication is handled at a different layer (e.g., network policies, VPN, or reverse proxy).
Anonymous configuration
auth:
mode: anonymous
Anonymous mode provides no access control. Only use it in trusted environments or when other security measures are in place. Do not use anonymous mode in production.
Anonymous use cases
- Local development and testing
- Internal deployments behind corporate firewalls
- Read-only public registries
- Environments with external authentication (reverse proxy, API gateway)
Default public paths
The following endpoints are always accessible without authentication, regardless of the auth mode:
/health- Health check endpoint/readiness- Readiness probe endpoint/version- Version information/.well-known/*- OAuth discovery endpoints (RFC 9728)
You can configure additional public paths using the publicPaths field in your
OAuth configuration. See the
Registry API reference for complete endpoint
documentation.
RFC 9728 OAuth discovery
The server implements RFC 9728 for OAuth Protected Resource Metadata, enabling clients to automatically discover authentication requirements.
Discovery endpoint
Clients can discover the server's OAuth configuration at:
GET /.well-known/oauth-protected-resource
Example discovery response
{
"resource": "https://registry.example.com",
"authorization_servers": [
"https://keycloak.example.com/realms/production",
"https://keycloak.example.com/realms/staging"
],
"scopes_supported": ["mcp-registry:read", "mcp-registry:write"],
"bearer_methods_supported": ["header"],
"resource_documentation": "https://docs.example.com/registry"
}
This allows OAuth clients to automatically configure themselves without manual setup, improving interoperability and reducing configuration errors.
When a request fails authentication, the server returns a WWW-Authenticate
header that includes a link to the discovery endpoint, helping clients locate
the authentication requirements.
Testing authentication
Using curl with a bearer token
TOKEN="your-jwt-token-here"
curl -H "Authorization: Bearer $TOKEN" \
https://registry.example.com/default/v0.1/servers
Using kubectl with Kubernetes service accounts
Use kubectl create token to generate a token with the correct audience:
# Create a token with the registry-server audience
TOKEN=$(kubectl create token <service-account-name> \
-n <namespace> \
--audience=registry-server)
# Make authenticated request
curl -H "Authorization: Bearer $TOKEN" \
https://registry.example.com/registry/v0.1/servers
For automated workloads, use projected service account tokens (see
Client workload example). The kubectl create token
command is useful for manual testing and debugging.
Testing token validation
To verify your token is valid:
- Decode the JWT at jwt.io (don't paste production tokens!)
- Check the
iss(issuer) matches your configuredissuerUrl - Check the
aud(audience) matches your configuredaudience - Check the
exp(expiration) is in the future
Choosing an authentication mode
| Mode | Security | Complexity | Best for |
|---|---|---|---|
| OAuth | High | Medium | Production deployments, enterprise environments |
| Anonymous | None | None | Development, testing, internal trusted networks |
Recommendations:
- Production deployments: Always use OAuth mode with your organization's identity provider
- Kubernetes deployments: Use OAuth with Kubernetes provider for automatic authentication
- Development/testing: Use anonymous mode for local development only
- Public read-only registries: Use OAuth mode with rate limiting; avoid anonymous in production
Security considerations
Token validation
All OAuth providers validate:
- Token expiration (
expclaim) - Audience claim (
aud) matches configuration - Issuer (
iss) matches the configured provider
For JWT tokens, signature verification uses the provider's public keys (fetched
from the issuer's JWKS endpoint). For opaque tokens, the server queries the
configured introspectionUrl to validate the token.
HTTPS requirements
Always use HTTPS in production to protect tokens in transit:
auth:
mode: oauth
oauth:
resourceUrl: https://registry.example.com # Use HTTPS
providers:
- issuerUrl: https://keycloak.example.com/realms/mcp # Use HTTPS
Token storage
- Never log or persist JWT tokens in plaintext
- Use short-lived tokens when possible (e.g., 1 hour)
- Implement token refresh flows for long-running clients
- Rotate client secrets regularly if using
clientSecretFile
Custom CA certificates
If your identity provider uses a custom CA certificate, specify the caCertPath
in your provider configuration:
providers:
- name: internal-keycloak
issuerUrl: https://keycloak.internal.example.com/realms/mcp
audience: registry-api
caCertPath: /etc/ssl/certs/internal-ca.crt
Troubleshooting
401 Unauthorized errors
If you receive 401 Unauthorized responses:
- Verify the token is valid: Check expiration, issuer, and audience claims
- Check the Authorization header: Must be
Authorization: Bearer <TOKEN> - Verify issuer URL: Must exactly match the
issclaim in your token - Check audience: The
audclaim must match your configuredaudience - Review server logs: Look for specific validation error messages
Token validation errors
If you see authentication failures in server logs:
- Issuer URL accessibility: Verify the server can reach the issuer's JWKS
endpoint (typically
${issuerUrl}/.well-known/openid-configuration) - Network connectivity: Check DNS resolution and firewall rules
- CA certificates: If using custom CAs, ensure
caCertPathis correct - Token expiration: Ensure your token hasn't expired (check
expclaim)
Provider connectivity issues
If the server cannot connect to the OAuth provider:
- Verify network connectivity to the issuer URL from the server
- Check DNS resolution for the provider's domain
- Ensure firewall rules allow outbound HTTPS (port 443) to the provider
- For Kubernetes providers, verify the API server is reachable at
https://kubernetes.default.svc - Check CA certificate configuration if using self-signed certificates
Multiple providers not working
If tokens from some providers work but others don't:
- Verify each provider's configuration independently using curl
- Check that audience claims differ between providers if needed (or are intentionally the same)
- Ensure issuer URLs are correct and include the full path (including realm for Keycloak)
- Review server logs to identify which specific provider validation is failing
- Test each provider's JWKS endpoint accessibility:
curl ${issuerUrl}/.well-known/openid-configuration
Next steps
- Configure database storage for production deployments
- Deploy the server with authentication enabled
- Configure registry sources for your environment