Skip to main content

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.

Looking for authorization?

This page covers authentication (verifying caller identity). For authorization (controlling what callers can do), including role-based access control and claims-based scoping, see Authorization.

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)
Secure by default

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

config-oauth.yaml
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

FieldTypeRequiredDefaultDescription
modestringYesoauthAuthentication mode (oauth or anonymous)
resourceUrlstringYes-The URL of the registry resource being protected
realmstringNomcp-registryOAuth realm identifier
scopesSupported[]stringNo[mcp-registry:read, mcp-registry:write]OAuth scopes advertised in the discovery endpoint
publicPaths[]stringNo[]Additional paths accessible without authentication
providersarrayYes-List of OAuth/OIDC identity providers

Provider configuration fields

FieldTypeRequiredDescription
namestringYesProvider identifier for logging and monitoring
issuerUrlstringYesOAuth/OIDC issuer URL (e.g., https://keycloak.example.com/realms/mcp)
audiencestringYesExpected audience claim in the access token
jwksUrlstringNoJWKS endpoint URL (skips OIDC discovery if specified)
introspectionUrlstringNoToken introspection endpoint URL for opaque token validation
clientIdstringNoOAuth client ID (for authenticated introspection requests)
clientSecretFilestringNoPath to file containing client secret (for authenticated introspection)
caCertPathstringNoPath to CA certificate for TLS verification
authTokenFilestringNoPath to token file for authenticating JWKS/introspection requests
allowPrivateIPboolNoAllow connections to private IP addresses (for in-cluster use)

Complete OAuth configuration example

config-oauth-complete.yaml
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:

config-opaque-tokens.yaml
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

config-k8s-auth.yaml
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
Kubernetes-specific configuration
  • issuerUrl: Use https://kubernetes.default.svc.cluster.local to match the iss claim 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

  1. Workloads mount projected service account tokens with a specific audience
  2. Clients send these tokens in the Authorization: Bearer <TOKEN> header
  3. The server validates tokens using the Kubernetes API server's public keys
  4. 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:

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

client-workload.yaml
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

Pick the provider you use and adapt the example. Each tab includes the configuration and any notes specific to that provider.

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.

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

config-anonymous.yaml
auth:
mode: anonymous
No access control

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:

  • /openapi.json - OpenAPI specification
  • /.well-known/* - OAuth discovery endpoints (RFC 9728)

The /health, /readiness, and /version endpoints are served on a separate internal server (default port 8081) and are not exposed on the main API port. See the command-line flags for the --internal-address option.

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/registry/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/default/v0.1/servers
Projected tokens vs kubectl create token

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:

  1. Decode the JWT at jwt.io (don't paste production tokens!)
  2. Check the iss (issuer) matches your configured issuerUrl
  3. Check the aud (audience) matches your configured audience
  4. Check the exp (expiration) is in the future

Choosing an authentication mode

ModeSecurityComplexityBest for
OAuthHighMediumProduction deployments, enterprise environments
AnonymousNoneNoneDevelopment, 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 (exp claim)
  • 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

Next steps

Troubleshooting

401 Unauthorized responses

A 401 means the server rejected the bearer token. Decode the token (for example, paste it into jwt.io, and never use a production token), then verify:

  • Header format. The Authorization header must be Authorization: Bearer <TOKEN> with no extra whitespace or quoting.
  • Issuer (iss) matches. The iss claim must exactly match the configured issuerUrl for one of your providers, including any path components (for example, /realms/production for Keycloak or /oauth2/default for Okta).
  • Audience (aud) matches. The aud claim must match the configured audience for that provider.
  • Token hasn't expired. Check the exp claim is in the future.

If those all check out, look at the Registry Server logs for the specific validation error, which usually pinpoints the failing claim or signature problem.

Server can't reach the identity provider's JWKS endpoint

The Registry Server first fetches the OIDC discovery document from ${issuerUrl}/.well-known/openid-configuration, then fetches signing keys from the jwks_uri listed in that document. Either fetch can fail. If logs show failed to fetch JWKS, connection refused, or x509: certificate signed by unknown authority, check the URL in the error to identify which fetch failed:

  1. DNS and network egress. Confirm the Registry Server pod or host can resolve and reach both the issuer hostname and the JWKS hostname (often the same, but providers like Auth0 host JWKS on a separate domain). Test with curl against the URL in the error message.
  2. Firewall rules. Ensure outbound HTTPS (port 443) to the provider is permitted.
  3. Custom CA. If your provider uses an internal CA, set caCertPath on the provider config to a file containing the trust chain. Mount that file into the pod.
  4. Kubernetes API server provider. For the Kubernetes provider, set allowPrivateIP: true, point caCertPath at /var/run/secrets/kubernetes.io/serviceaccount/ca.crt, and confirm the Service Account has the system:auth-delegator role.
Tokens from one provider work but tokens from another don't

When you configure multiple providers, the server tries each in order and accepts the first match. If only one provider works:

  1. Confirm iss matches exactly. A trailing slash mismatch is a common culprit; the iss claim and the configured issuerUrl must be byte-for- byte identical.
  2. Confirm aud is correct per provider. Each provider can have a different audience; verify the failing provider's tokens have the audience you configured for it.
  3. Verify each issuer's discovery endpoint. Run curl ${issuerUrl}/.well-known/openid-configuration for each provider to confirm it's reachable from the server and returns valid JSON.
  4. Check the logs. The server logs which provider is being tried and why validation failed. That tells you whether the issue is reachability, signature validation, or claim mismatch.