For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.
Set up authentik
Deploy and configure authentik as an OAuth identity provider for MCP authentication with agentgateway.
Secure your Model Context Protocol (MCP) servers with OAuth 2.0 authentication by using agentgateway and authentik as the identity provider.
About this guide
In this guide, you configure the agentgateway proxy to protect a static MCP server with MCP auth by using authentik as the authorization server. Because authentik does not fully implement the OAuth behaviors that the MCP authorization specification assumes, agentgateway includes a native Authentik provider that bridges the gaps. When you set provider: Authentik, agentgateway serves authorization server metadata from authentik’s OpenID Connect discovery document. Agentgateway also injects a Dynamic Client Registration (DCR) endpoint that authentik does not provide, and answers registration requests with the client that you pre-register.
Important
Setting clientId is required for open source authentik. Those builds do not implement Dynamic Client Registration (RFC 7591), so the pre-registered client in clientId is the only way for MCP clients to complete registration. If you omit it, registration requests fail. authentik 2026.8.0 adds a registration endpoint (authentik#8751), but only as an enterprise feature.
For more information about MCP auth, see the About MCP auth page.
Before you begin
- Set up an agentgateway proxy.
- Follow the steps to set up an MCP server with a fetch tool.
- Install the experimental channel Gateway API.
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.0/experimental-install.yaml
Install authentik
Install authentik in your cluster to act as the authorization server.
Add the authentik Helm repository.
helm repo add authentik https://charts.goauthentik.io helm repo update authentikSet the credentials that bootstrap the authentik admin account and API token. In a production environment, generate strong values and store them in a secret manager.
export AUTHENTIK_SECRET_KEY=$(openssl rand -base64 36 | tr -d '\n') export AUTHENTIK_BOOTSTRAP_PASSWORD='Admin123!docs' export AUTHENTIK_BOOTSTRAP_TOKEN='docs-bootstrap-token-0123456789'Install authentik with the bundled PostgreSQL and Redis dependencies. The
authentik.postgresql.passwordvalue must matchpostgresql.auth.passwordso that the authentik server can connect to its database.helm upgrade --install authentik authentik/authentik \ --namespace authentik --create-namespace \ --version 2026.5.6 \ --timeout 15m \ --set authentik.secret_key="${AUTHENTIK_SECRET_KEY}" \ --set authentik.bootstrap_password="${AUTHENTIK_BOOTSTRAP_PASSWORD}" \ --set authentik.bootstrap_token="${AUTHENTIK_BOOTSTRAP_TOKEN}" \ --set authentik.bootstrap_email='admin@example.com' \ --set authentik.error_reporting.enabled=false \ --set authentik.postgresql.password='authentik-docs-pg' \ --set postgresql.enabled=true \ --set postgresql.auth.password='authentik-docs-pg' \ --set postgresql.auth.postgresPassword='authentik-docs-pg' \ --set redis.enabled=true \ --set redis.auth.enabled=falseWait for the authentik server to become available. The server does not accept API requests until it finishes migrating its database, which can take several minutes on a first install.
kubectl wait --for=condition=Available deployment/authentik-server \ -n authentik --timeout=15mVerify that the authentik pods are running.
kubectl get pods -n authentikExample output:
NAME READY STATUS RESTARTS AGE authentik-postgresql-0 1/1 Running 0 2m authentik-server-564544fd8d-4lzw8 1/1 Running 0 1m authentik-worker-7ccdd7cb6f-bf2qp 1/1 Running 0 1m
Create an OAuth provider and application in authentik
Create an OAuth2 provider and an application in authentik, and capture the client ID that agentgateway uses.
Expose the authentik API so that you can administer it from outside the cluster. The Helm chart gives the authentik server a ClusterIP Service, which agentgateway uses from inside the cluster. This extra Service is only for the administrative API calls in the following steps.
kubectl apply -f - <<EOF apiVersion: v1 kind: Service metadata: name: authentik-admin namespace: authentik spec: type: LoadBalancer selector: app.kubernetes.io/name: authentik app.kubernetes.io/instance: authentik app.kubernetes.io/component: server ports: - name: http port: 9000 targetPort: 9000 EOFNote
The Service listens on port 9000 rather than 80 on purpose. A local
kindcluster publishes each LoadBalancer Service on the matching port of your workstation, so a second Service on port 80 collides with the gateway’s own LoadBalancer and never receives an address.export AUTHENTIK_ADDRESS=$(kubectl get svc -n authentik authentik-admin \ -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}"):9000 echo "authentik address: $AUTHENTIK_ADDRESS"
Look up the flow, signing key, and scope IDs that the provider requires.
export AUTHENTIK_API=http://${AUTHENTIK_ADDRESS}/api/v3 export AK_AUTH_HEADER="Authorization: Bearer ${AUTHENTIK_BOOTSTRAP_TOKEN}" export AK_FLOW=$(curl -s -H "${AK_AUTH_HEADER}" \ "${AUTHENTIK_API}/flows/instances/?slug=default-provider-authorization-implicit-consent" | jq -r '.results[0].pk') export AK_INVALIDATION_FLOW=$(curl -s -H "${AK_AUTH_HEADER}" \ "${AUTHENTIK_API}/flows/instances/?slug=default-invalidation-flow" | jq -r '.results[0].pk') export AK_SIGNING_KEY=$(curl -s -H "${AK_AUTH_HEADER}" \ "${AUTHENTIK_API}/crypto/certificatekeypairs/?has_key=true" | jq -r '.results[0].pk') export AK_SCOPES=$(curl -s -H "${AK_AUTH_HEADER}" \ "${AUTHENTIK_API}/propertymappings/provider/scope/" \ | jq -c '[.results[] | select(.scope_name=="openid" or .scope_name=="profile" or .scope_name=="email") | .pk]')Create a public OAuth2 provider. MCP clients are public clients that use PKCE, because they cannot keep a client secret.
export AUTHENTIK_CLIENT_ID=$(curl -s -X POST -H "${AK_AUTH_HEADER}" -H "Content-Type: application/json" \ "${AUTHENTIK_API}/providers/oauth2/" -d "{ \"name\": \"agentgateway-mcp\", \"authorization_flow\": \"${AK_FLOW}\", \"invalidation_flow\": \"${AK_INVALIDATION_FLOW}\", \"client_type\": \"public\", \"signing_key\": \"${AK_SIGNING_KEY}\", \"property_mappings\": ${AK_SCOPES}, \"grant_types\": [\"authorization_code\", \"refresh_token\", \"client_credentials\"], \"redirect_uris\": [{\"matching_mode\": \"regex\", \"url\": \".*\"}], \"sub_mode\": \"user_username\", \"include_claims_in_id_token\": true }" | jq -r '.client_id') echo "Client ID: ${AUTHENTIK_CLIENT_ID}"If the client ID is empty, the provider was not created. Check that
AUTHENTIK_ADDRESSstill resolves and thatAUTHENTIK_BOOTSTRAP_TOKENmatches the value you installed authentik with.The following table describes the settings that matter for MCP.
Setting Description client_typeMust be public. MCP clients cannot keep a client secret, so they authenticate with PKCE instead.property_mappingsThe scopes that the provider can issue. The profilescope is what puts thegroupsclaim in the token, which the authorization rule reads.grant_typesauthentik rejects any grant that is not listed here with invalid_grant. MCP clients useauthorization_code.sub_modeSets the subclaim to the username, which makes tokens easier to read while you test.Warning
The
.*redirect URI matcher accepts any callback URL, so that you can connect different MCP clients while you test. Do not use it outside a test cluster. An authorization server that accepts any redirect URI lets an attacker intercept authorization codes by sending a victim through a crafted callback. In production, list only the callback URLs of the MCP clients that you allow.Create an application that uses the provider. The application slug appears in the issuer URL.
export AK_PROVIDER_PK=$(curl -s -H "${AK_AUTH_HEADER}" \ "${AUTHENTIK_API}/providers/oauth2/?name=agentgateway-mcp" | jq -r '.results[0].pk') curl -s -X POST -H "${AK_AUTH_HEADER}" -H "Content-Type: application/json" \ "${AUTHENTIK_API}/core/applications/" -d "{ \"name\": \"agentgateway MCP\", \"slug\": \"agentgateway-mcp\", \"provider\": ${AK_PROVIDER_PK} }" | jq -r '.slug'
Create a group for the users that can access the MCP server, and add your user to it. When a client requests the
profilescope, authentik puts the names of the groups that the user belongs to in thegroupsclaim of the token. The authorization rule that you configure later reads that claim.export AK_GROUP_PK=$(curl -s -X POST -H "${AK_AUTH_HEADER}" -H "Content-Type: application/json" \ "${AUTHENTIK_API}/core/groups/" -d '{"name": "mcp-agent"}' | jq -r '.pk') export AK_USER_PK=$(curl -s -H "${AK_AUTH_HEADER}" \ "${AUTHENTIK_API}/core/users/?username=akadmin" | jq -r '.results[0].pk') curl -s -X POST -H "${AK_AUTH_HEADER}" -H "Content-Type: application/json" \ "${AUTHENTIK_API}/core/groups/${AK_GROUP_PK}/add_user/" -d "{\"pk\": ${AK_USER_PK}}" echo "Group: ${AK_GROUP_PK}"Repeat the
add_userrequest for each user that you want to give access to the MCP server.
- Save the issuer URL. authentik issuers take the form
https://<authentik-host>/application/o/<app-slug>/, including the trailing slash. Because the agentgateway control plane fetches the JWKS from inside the cluster, use the in-cluster address of the authentik Service.export AUTHENTIK_ISSUER="http://authentik-server.authentik.svc.cluster.local/application/o/agentgateway-mcp/" export AUTHENTIK_JWKS_PATH="/application/o/agentgateway-mcp/jwks/"
Configure MCP auth
With your MCP backend configured, create an AgentgatewayPolicy that enforces authentik authentication and authorization for the MCP backend.
Create an AgentgatewayPolicy with the
Authentikprovider. The policy validates tokens that authentik issues and uses a Common Expression Language (CEL) rule to require themcp-agentgroup.kubectl apply -f- <<EOF apiVersion: agentgateway.dev/v1alpha1 kind: AgentgatewayPolicy metadata: name: mcp-authentik-authn spec: # Target the HTTPRoute to apply authentication at the route level targetRefs: - group: gateway.networking.k8s.io kind: HTTPRoute name: mcp traffic: jwtAuthentication: mode: Strict providers: - # The authentik issuer URL, including the trailing slash issuer: "${AUTHENTIK_ISSUER}" # authentik sets the 'aud' claim to the OAuth client ID audiences: - "${AUTHENTIK_CLIENT_ID}" jwks: remote: # Reference the in-cluster authentik Service to fetch public keys backendRef: name: authentik-server kind: Service namespace: authentik port: 80 # authentik serves JWKS at {issuer}/jwks/, not /.well-known/jwks.json jwksPath: "${AUTHENTIK_JWKS_PATH}" mcp: # Use the native authentik provider provider: Authentik # Required: authentik does not support Dynamic Client Registration clientId: "${AUTHENTIK_CLIENT_ID}" resourceMetadata: resource: http://localhost:8080/mcp scopesSupported: - openid - profile bearerMethodsSupported: - header # Allow only tokens from members of the mcp-agent group authorization: action: Allow policy: matchExpressions: - '"mcp-agent" in jwt.groups' EOFReview the following table to understand this configuration. For more information, see the JwtAuthentication API docs.
Setting Description providers[].issuerThe authentik issuer URL, including the trailing slash. This must exactly match the issclaim in tokens that authentik issues.providers[].audiencesThe OAuth client ID. authentik sets the audclaim of its tokens to the client ID rather than to a separate API identifier, so this value must matchclientId.providers[].jwks.remote.backendRefThe in-cluster authentik Service that the control plane fetches public keys from. providers[].jwks.remote.jwksPathThe path to authentik’s JWKS endpoint. authentik serves keys at {issuer}/jwks/.mcp.providerThe identity provider to adapt agentgateway’s OAuth behavior to. In this example, Authentikis used.mcp.clientIdThe pre-registered public client that agentgateway returns to MCP clients that attempt Dynamic Client Registration. Required for authentik. mcp.resourceMetadataMCP OAuth resource metadata for discovery. Includes the resource identifier, supported scopes, and bearer token methods. authorization.policy.matchExpressionsCEL rules that authorize the claims in the verified JWT. This example requires membership in the mcp-agentgroup that you created. Requests that present a valid token without that group are denied with a 403 HTTP response code.Note
When the policy is first applied, the control plane might briefly log
jwks keyset ... isn't availableuntil it completes the first JWKS fetch. This condition resolves on its own.Verify that the policy was accepted.
kubectl get AgentgatewayPolicy mcp-authentik-authn -o yamlIn the
statussection, confirm that theAcceptedandAttachedconditions areTrue.Update the HTTPRoute that routes incoming traffic to the MCP server to include the OAuth discovery paths. This way, the agentgateway proxy can serve the resource and authorization server metadata during the MCP auth flow.
kubectl apply -f - <<EOF apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: name: mcp spec: parentRefs: - group: gateway.networking.k8s.io kind: Gateway name: agentgateway-proxy namespace: agentgateway-system rules: - filters: # Enable CORS for browser-based MCP clients - type: CORS cors: allowCredentials: true allowHeaders: - Origin - Authorization - Content-Type allowMethods: - "*" allowOrigins: - "*" exposeHeaders: - Origin - Mcp-Session-Id maxAge: 86400 backendRefs: - group: agentgateway.dev kind: AgentgatewayBackend name: mcp-backend matches: # Main MCP endpoint to connect to the MCP server - path: type: PathPrefix value: /mcp # Path to access resource server metadata - path: type: PathPrefix value: /.well-known/oauth-protected-resource/mcp # Path to access authorization server metadata, including the # gateway-served client registration endpoint - path: type: PathPrefix value: /.well-known/oauth-authorization-server/mcp EOF
Verify MCP auth
Get the address of the agentgateway proxy.
export INGRESS_GW_ADDRESS=$(kubectl get svc -n agentgateway-system agentgateway-proxy \ -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}") echo "Gateway address: $INGRESS_GW_ADDRESS"Send an unauthenticated request to the MCP endpoint. Verify that the request is rejected with a 401 HTTP response code and a
WWW-Authenticateheader that points MCP clients to the protected resource metadata.curl -i http://$INGRESS_GW_ADDRESS:80/mcp -X POST \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{}},"id":1}'Example output:
HTTP/1.1 401 Unauthorized www-authenticate: Bearer resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource/mcp"Verify that the gateway serves the protected resource metadata.
curl -s http://$INGRESS_GW_ADDRESS:80/.well-known/oauth-protected-resource/mcp | jqExample output:
{ "resource": "http://localhost:8080/mcp", "authorization_servers": ["http://localhost:8080/mcp"], "mcp_protocol_version": "2025-06-18", "resource_type": "mcp-server", "bearer_methods_supported": ["header"], "scopes_supported": ["openid", "profile"] }Verify that the gateway serves authorization server metadata from authentik’s discovery document, and that it injected a
registration_endpointthat points back at the gateway. authentik’s own discovery document does not include this field.curl -s http://$INGRESS_GW_ADDRESS:80/.well-known/oauth-authorization-server/mcp \ | jq '{issuer, jwks_uri, authorization_endpoint, registration_endpoint}'Example output:
{ "issuer": "http://authentik-server.authentik.svc.cluster.local/application/o/agentgateway-mcp/", "jwks_uri": "http://authentik-server.authentik.svc.cluster.local/application/o/agentgateway-mcp/jwks/", "authorization_endpoint": "http://authentik-server.authentik.svc.cluster.local/application/o/authorize/", "registration_endpoint": "http://localhost:8080/.well-known/oauth-authorization-server/mcp/client-registration" }Verify that the gateway answers Dynamic Client Registration with your pre-registered client, instead of proxying the request to authentik.
curl -s -X POST http://$INGRESS_GW_ADDRESS:80/.well-known/oauth-authorization-server/mcp/client-registration \ -H "Content-Type: application/json" \ -d '{"client_name":"test-mcp-client","redirect_uris":["http://localhost:9999/callback"]}' \ | jq -r '.client_id'The returned client ID matches the
AUTHENTIK_CLIENT_IDvalue that you configured in the policy.
Connect an MCP client
Point your MCP client at the gateway’s MCP endpoint, such as http://localhost:8080/mcp. The client discovers the authorization server through the gateway, registers against the pre-registered client, and redirects the user to authentik to log in and consent.
Important
The authorization and token endpoints that the gateway advertises come from authentik. In this guide, those endpoints use the in-cluster Service address, which a browser outside the cluster cannot reach. To complete an interactive sign-in, expose authentik at an address that both your MCP client and the gateway can resolve, and set AUTHENTIK_ISSUER to that address.
Group-based authorization
The policy that you created gates the MCP endpoint on the mcp-agent group, which authentik puts in the groups claim of the token when the client requests the profile scope. Authentication alone is not enough: any caller that authentik issues a token to for this client passes JWT validation, including service accounts that authorize themselves rather than a user. The authorization rule denies those tokens with a 403 HTTP response code.
Because MCP authentication runs at the route level, every claim in the verified token is also available to other route-level policies, such as rate limiting and transformations. For more information about the rules that you can write, see Authorization.
To authorize individual tools instead of the whole MCP endpoint, use an MCP authorization policy. For more information, see Tool access.
Clean up
You can remove the resources that you created in this guide.kubectl delete AgentgatewayPolicy mcp-authentik-authn
helm uninstall authentik -n authentik
kubectl delete namespace authentik