Skip to main content

Configure the Connector Gateway

The Connector Gateway provides an identity-aware MCP endpoint and brokers upstream OAuth for connectors. Enable it through the Stacklok Enterprise platform chart. Enabling it takes more than a single flag, because the gateway signs users in and stores their connector credentials itself.

This page uses the stacklok-enterprise release name and the stacklok-system namespace from Deploy the platform. Several values below embed those names, so adjust them if yours differ.

Prerequisites​

  • A corporate identity provider at global.stacklok.primaryIdp. The gateway resolves every caller to a directory user before it decides what they can see. See Configure identity.
  • The Enterprise Manager, with enterprise-manager.directory.enabled: true. The gateway gets each caller's identity, connector grants, and connector configuration from it. It needs its PostgreSQL database, set up as described in the Enterprise Manager database. To administer connectors across users in the console, also create the BYPASSRLS admin database role described there.
  • The Enterprise Manager's key-encryption key, gRPC TLS, and NetworkPolicy, configured as described in Encrypting stored credentials. The Enterprise Manager encrypts connector credentials and the identity provider's client secret with this key, and returns them to the gateway only over TLS. That section's NetworkPolicy example already admits pods labeled app.kubernetes.io/name: connector-gateway.
  • Redis or Valkey, reachable from the cluster. The embedded authorization server stores its sessions only in Redis, at every replica count, and the chart doesn't deploy an instance. Configure it once under global.redis, as described in Global Redis/Valkey defaults, and the gateway inherits the host, port, username, password Secret, and TLS setting. To connect as a least-privilege ACL user, set global.redis.username and limit that user to keys matching thv:auth:* (authorization server sessions) and connector-gateway:* (tool call recording and key checks).

Prepare the gateway's secrets​

The gateway needs three Secrets in the release namespace. Create them before you install or upgrade.

Key-encryption key (KEK). The gateway seals the upstream OAuth tokens and client registrations it stores with this key. It is separate from the Enterprise Manager's KEK. The Secret holds 32 random bytes under the key kek:

openssl rand 32 > kek
kubectl create secret generic connector-gateway-kek \
--namespace stacklok-system \
--from-file=kek=./kek

Keep a backup of the key. Losing or replacing it makes every credential the gateway has stored unreadable. To change the key later, add a new version rather than replacing it, as described in Rotate the Connector Gateway encryption key.

Authorization server keys. The signing key keeps issued tokens valid across restarts and replicas, and the HMAC secret keeps authorization codes and refresh tokens valid. The signing key must be a PEM-encoded ECDSA private key:

openssl ecparam -name prime256v1 -genkey -noout -out signing-key.pem
openssl rand 32 > hmac-secret
kubectl create secret generic connector-gateway-auth-server-keys \
--namespace stacklok-system \
--from-file=signing-key.pem=./signing-key.pem \
--from-file=hmac-secret=./hmac-secret
Use existing Secrets with GitOps

kek.generate: true and authServerKeys.generate: true have the chart create and preserve the key material itself, but only on a live helm install or helm upgrade. helm template, Argo CD, and other render-and-apply workflows can't look up the existing Secret, so they would mint new keys on every sync and orphan everything sealed or signed with the old ones. Use existingSecret for those workflows.

Enterprise Manager CA bundle. The gateway verifies the Enterprise Manager's gRPC serving certificate against a CA bundle. Put only the CA certificate in this Secret. If cert-manager issues the Enterprise Manager's certificate, copy ca.crt out of its Secret rather than referencing that Secret, which also holds the private key:

kubectl create secret generic enterprise-manager-grpc-ca \
--namespace stacklok-system \
--from-file=ca.crt=./ca.crt

A copied Secret goes stale when cert-manager rotates the CA. For GitOps, keep the copy in sync automatically instead, for example with a trust-manager Bundle that targets a Secret, or issue the Enterprise Manager's certificate from a cert-manager CA Issuer whose CA you manage and distribute yourself.

Enable the Connector Gateway​

Add the following to the values file you use for the platform. Merge the global keys into your existing global block rather than adding a second one.

values.yaml
global:
stacklok:
# primaryIdp from "Configure identity" stays here unchanged.
connectorGateway:
enabled: true
connectorGatewayId: '<GATEWAY_ID>'
authServerIssuer: 'https://<GATEWAY_HOST>'
# Redis or Valkey from "Global Redis/Valkey defaults"
redis:
host: 'redis.example.com'
port: 6379
existingSecret: 'redis-auth'

connector-gateway:
vmcpConfig:
groupRef: 'connector-gateway'
incomingAuth:
type: oidc
oidc:
issuer: 'https://<GATEWAY_HOST>'
audience: 'https://<GATEWAY_HOST>/gw/mcp'
enterpriseConfig:
authServer:
schema_version: 'v0.1.0'
allowed_audiences:
- 'https://<GATEWAY_HOST>/gw/mcp'
baseline_client_scopes:
- openid
- offline_access
storage:
type: redis
controlPlane:
incomingAuth:
oidc:
issuer: 'https://idp.example.com'
audience: '<CONSOLE_TOKEN_AUDIENCE>'
directory:
addr: 'stacklok-enterprise-enterprise-manager.stacklok-system.svc.cluster.local:9443'
directoryTLS:
enabled: true
caBundle:
secretName: 'enterprise-manager-grpc-ca'
kek:
existingSecret: 'connector-gateway-kek'
authServerKeys:
existingSecret: 'connector-gateway-auth-server-keys'
ValuePurpose
global.stacklok.connectorGateway.enabledDeploy the Connector Gateway
global.stacklok.connectorGatewayIdIdentify this gateway to the Enterprise Manager and console
global.stacklok.authServerIssuerSet the gateway's public authorization server URL
vmcpConfig.groupRefName the gateway in its status and logs. Required; any non-empty value works
vmcpConfig.incomingAuthRequire tokens from the gateway's own authorization server on the MCP endpoint
enterpriseConfig.authServerConfigure the embedded authorization server and its Redis storage
enterpriseConfig.controlPlaneAccept the corporate tokens the console sends to the gateway's REST API
enterpriseConfig.directory.addrPoint the gateway at the Enterprise Manager's TLS gRPC port
directoryTLSMount the Enterprise Manager CA bundle and the gateway's ServiceAccount token
kek, authServerKeysReference the Secrets from Prepare the gateway's secrets

Choose the gateway ID and issuer​

Choose a stable connectorGatewayId such as prod-eu or platform-staging, and give every install its own value. The gateway registers this ID with the Enterprise Manager, so two installs sharing one ID act as a single gateway. The value cannot be empty or contain a colon. Changing it creates a new gateway identity and changes the prefix used for stored per-user tokens.

Set authServerIssuer to the HTTPS URL that clients use to reach this deployment. The gateway derives connector OAuth callbacks as {issuer}/oauth/callback. Configure the value under global.stacklok; the chart rejects equivalent settings under the component's internal configuration.

The MCP endpoint is {issuer}/gw/mcp, and that URL is the token audience. Set vmcpConfig.incomingAuth.oidc.issuer to the same value as authServerIssuer, and list the audience in allowed_audiences exactly as it appears in incomingAuth.oidc.audience. The gateway compares these strings exactly and refuses to start when they disagree.

vmcpConfig.groupRef labels the gateway in its status and logs. The gateway refuses to start while it's empty. It reads its backends from the Enterprise Manager, so no MCPGroup by that name needs to exist.

baseline_client_scopes adds openid and offline_access to every client that registers through dynamic client registration. MCP clients such as Claude Code, Cursor, and VS Code register with openid and then request offline_access when they sign in. Without the baseline, the authorization server rejects that request with invalid_scope and the client stays at "needs authentication." Each baseline scope must also be in the authorization server's supported scopes (openid, profile, email, and offline_access by default), or the gateway refuses to start.

Two different issuers

global.stacklok.primaryIdp.issuer identifies the corporate identity provider. authServerIssuer identifies the Connector Gateway's authorization server. Configure both values. See Configure platform identity.

Omit upstreams from enterpriseConfig.authServer. The authorization server loads its identity providers from the Enterprise Manager: your corporate identity provider from global.stacklok.primaryIdp, plus the connector identity providers that admins register in the Enterprise Manager.

Accept console tokens on the REST API​

The console calls the gateway's REST API with the user's corporate identity provider access token. enterpriseConfig.controlPlane.incomingAuth.oidc configures the validator that accepts those tokens. Without it, the REST API accepts only tokens from the gateway's own authorization server, and the console's Connectors pages fail with 401.

Set issuer to global.stacklok.primaryIdp.issuer; the gateway refuses to start if it doesn't match an identity provider it signs users in with. Set audience to the exact aud value of the access tokens the console sends. To find it, decode a console token as shown in Configure platform identity. Prefer an audience that only Stacklok components accept over a shared default such as Okta's api://default, which would let tokens minted for unrelated applications reach the gateway. The token must be a JWT: opaque access tokens, such as those from Okta's org authorization server, are rejected with 401. To fetch signing keys from a URL other than the issuer's discovery document, also set jwksUrl.

Identify users by a claim other than sub​

By default, the gateway and the Enterprise Manager identify users by the sub claim in your corporate identity provider's tokens. If your provider identifies users with a different claim, such as uid in Okta, configure it on both sides. Set global.stacklok.primaryIdp.subjectClaim, which the gateway uses at sign-in, and add an enterprise-manager.directory.issuers entry whose issuer_url matches global.stacklok.primaryIdp.issuer exactly, with the same claim in binding_claim:

values.yaml
global:
stacklok:
primaryIdp:
# issuer and client settings from "Configure identity" stay here.
subjectClaim: 'uid'

enterprise-manager:
directory:
issuers:
- id: 'corporate-idp'
issuer_url: 'https://idp.example.com'
audience: 'enterprise-manager'
binding_claim: 'uid'

The two values must match. When they differ, identity resolution fails and every user is rejected with 401.

The gateway reads the claim from the ID token, and it must be a top-level string claim that also appears on refreshed ID tokens. Okta adds uid to access tokens from a custom authorization server, but not to ID tokens. In Okta, add a uid claim with the value user.id and the token type ID Token. Choose the claim before users sign in, because it becomes each user's stable identity in the directory.

The same issuers entry configures SCIM provisioning for that provider. See SCIM identity providers.

Connect the gateway to the Enterprise Manager​

enterpriseConfig.directory.addr requires exactly one transport. With directoryTLS.enabled: true, the gateway dials the Enterprise Manager's TLS gRPC port (9443) and authenticates with a projected ServiceAccount token. The Enterprise Manager side of that connection needs a TLS listener, caller authentication, and the gateway's ServiceAccount on three allowlists:

values.yaml
enterprise-manager:
grpc:
tls:
enabled: true
existingSecret: 'enterprise-manager-grpc-tls'
callerAuth:
clusterOidcIssuer: '<CLUSTER_OIDC_ISSUER>'
saSubjectAllowlist:
- 'system:serviceaccount:stacklok-system:stacklok-enterprise-connector-gateway'
gatewaySaSubjectAllowlist:
- 'system:serviceaccount:stacklok-system:stacklok-enterprise-connector-gateway'
credentialSaSubjectAllowlist:
- 'system:serviceaccount:stacklok-system:stacklok-enterprise-connector-gateway'

grpc.tls is the same setting you configured in Encrypting stored credentials. The chart refuses to render a TLS gateway transport while enterprise-manager.grpc.tls.enabled is false.

Set clusterOidcIssuer to your cluster's ServiceAccount token issuer, which this command prints:

kubectl get --raw /.well-known/openid-configuration | jq -r .issuer

The Enterprise Manager fetches the issuer's discovery document and signing keys directly from that URL, not through the Kubernetes API server. On managed clusters such as Amazon EKS, the issuer is a public URL, so allow egress to it from the Enterprise Manager pods. If the fetch fails, the Enterprise Manager rejects every gRPC caller.

Each allowlist covers different calls from the gateway. saSubjectAllowlist covers identity resolution and per-user connector configuration. gatewaySaSubjectAllowlist covers the gateway's startup registration and its catalog reads. credentialSaSubjectAllowlist covers credential resolution, which the gateway needs for the identity provider's client secret. The subject is system:serviceaccount:<NAMESPACE>:<RELEASE_NAME>-connector-gateway. The chart fills in a fourth list, meteringSaSubjectAllowlist, from the same subject for tool call recording.

Keep the AI Gateway's access​

Caller authentication covers both the TLS and cleartext gRPC ports, so turning it on also gates the AI Gateway. Its API key service checks every virtual API key against the Enterprise Manager. If your platform also runs the AI Gateway with virtual API keys:

  • Add the API key service's subject to saSubjectAllowlist: system:serviceaccount:<AIGATEWAY_NAMESPACE>:<AIGATEWAY_NAME>-api-key-service, where <AIGATEWAY_NAME> is the name of your AIGateway resource.
  • Keep spec.auth.virtualAPIKeys.directory.insecure on the AIGateway at its default, false, so the API key service presents its ServiceAccount token.

Otherwise every virtual-key request fails with 503 and key store temporarily unavailable. The chart refuses to render while the AI Gateway is enabled and no subject ending in -api-key-service is on the list.

Name overrides​

The umbrella chart computes two values from the default gateway name, <RELEASE_NAME>-connector-gateway: meteringSaSubjectAllowlist and toolhive-cloud-ui.connectorGatewayUrl, the in-cluster REST API URL that enables the console's Connectors pages. If you set connector-gateway.fullnameOverride or nameOverride, or change the control-plane Service port, the render fails until you set both by hand. The error message prints the value to use. Also update the subjects in your three allowlists to match the new ServiceAccount name.

For a development cluster without TLS, set connector-gateway.enterpriseConfig.directory.insecure: true in place of directoryTLS, point addr at the cleartext port (9091), and set enterprise-manager.grpc.insecure: true. The chart requires both settings together. This transport carries credentials in cleartext with no caller authentication, so reserve it for local testing.

Corporate identity provider settings​

The gateway reads these optional keys from global.stacklok.primaryIdp, in addition to the issuer and client settings in Configure platform identity:

KeyPurpose
clientClaimAccess-token claim that names the OAuth client. Empty reads cid or client_id. Allowed values are cid, client_id, azp, and appid; set azp for Dex, or azp or appid for Microsoft Entra ID
subjectClaimID-token claim that identifies each user. Defaults to sub. Must match the Enterprise Manager's binding_claim; see Identify users by a claim other than sub
clientClaimAllowUnsafeBoolean. Accepts a clientClaim outside the allowed values. A weaker claim can match tokens minted for a different client, and the gateway logs a warning at startup
allowPrivateIpsBoolean. Lets discovery and token exchange reach an identity provider on a private or link-local address. HTTPS is still required for non-localhost hosts
caBundleRefConfigMap or Secret holding the PEM CA bundle for an identity provider served by a private CA. Set exactly one of configMapRef or secretRef, each with a name, plus an optional key (default ca.crt)

Set the boolean keys as unquoted true or false. The chart refuses a quoted string, and --set-string produces one.

Include the full chain in caBundleRef

For sign-in, the gateway trusts caBundleRef in addition to the system roots. Its control-plane token validator, which checks the corporate tokens the console sends, trusts only this bundle. Include every intermediate and root certificate in the chain that serves your identity provider's JWKS endpoint. A bundle that omits a public intermediate or root makes every control-plane request fail with 401, even though sign-in works.

The gateway reads the bundle once at startup. After you replace the contents of the same ConfigMap or Secret, restart the gateway with kubectl rollout restart deployment/stacklok-enterprise-connector-gateway -n stacklok-system.

Publish and verify the endpoints​

Publish the gateway at the HTTPS origin you set in authServerIssuer. The chart creates two HTTP Services in the release namespace:

TrafficDefault ServicePort
MCP calls and authorization server<RELEASE_NAME>-connector-gateway8443
Per-user REST API<RELEASE_NAME>-connector-gateway-control-plane8090

Confirm the installed names and ports, especially if you use Helm name or port overrides:

kubectl get svc -n stacklok-system
kubectl get svc stacklok-enterprise-connector-gateway -n stacklok-system -o yaml

Route the public hostname​

The following Gateway API example requires an existing Gateway with an HTTPS listener named <HTTPS_LISTENER>. Its certificate must cover <GATEWAY_HOST>, and its allowedRoutes must admit routes from stacklok-system. Use the Gateway and namespace managed by your ingress team. The controller must be able to reach the backend Service over HTTP, including through any NetworkPolicy.

connector-gateway-route.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: connector-gateway
namespace: stacklok-system
spec:
parentRefs:
- name: <EDGE_GATEWAY_NAME>
namespace: <EDGE_GATEWAY_NAMESPACE>
sectionName: <HTTPS_LISTENER>
hostnames:
- '<GATEWAY_HOST>'
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: stacklok-enterprise-connector-gateway
port: 8443

Replace the placeholders and adjust the backend name and port to match your Service. Apply the route, then inspect its parent conditions:

kubectl apply -f connector-gateway-route.yaml
kubectl get httproute connector-gateway -n stacklok-system \
-o jsonpath='{.status.parents}' | jq .
kubectl get gateway <EDGE_GATEWAY_NAME> -n <EDGE_GATEWAY_NAMESPACE> -o yaml

Wait for Accepted and ResolvedRefs to be True on the route and for the Gateway to be Programmed. Point <GATEWAY_HOST> in DNS at your edge Gateway's address. Terminate TLS there and preserve request paths and the Authorization header. Routing / preserves /gw/mcp, /.well-known/*, and /oauth/*, including the callback at /oauth/callback. Register that callback URL on the corporate identity provider's OAuth client.

Configure your controller to pass streamed responses as they arrive and keep long-lived MCP connections open. Set its request and idle timeouts to accommodate your tool calls, and disable response buffering if your controller buffers by default.

Keep the REST API reachable by the console​

The console calls the control-plane Service from inside the cluster, so that Service can remain cluster-internal. For external API clients, create a separate HTTPS route to stacklok-enterprise-connector-gateway-control-plane on port 8090. You can adapt the HTTPRoute above with a separate hostname and backend; preserve the /v1 paths. See the Connector Gateway API reference for authentication and a port-forward alternative.

Verify each part of the connection​

  1. Confirm the gateway is ready and registered. Check its pod and find your connectorGatewayId in the console's administration experience under Connectors:

    kubectl rollout status deployment/stacklok-enterprise-connector-gateway \
    -n stacklok-system
  2. Check DNS, TLS, and authorization-server routing from a client machine:

    curl --fail --silent --show-error \
    https://<GATEWAY_HOST>/.well-known/openid-configuration | jq .

    The response's issuer must match authServerIssuer, and its authorization and token endpoints must use the published gateway origin. For a private CA, use --cacert <CA_FILE> and install that CA in your clients' trust stores.

  3. Check the REST API independently. Use the API reference's port-forward and a corporate identity provider access token accepted by the gateway's control-plane validator:

    curl --fail --silent --show-error \
    http://localhost:8080/v1/users/me/gateway/connectors/status \
    -H 'Authorization: Bearer <CORPORATE_ACCESS_TOKEN>' | jq .

    A successful JSON response confirms control-plane authentication and directory access. It can be empty before you grant connectors. This API uses corporate tokens; the MCP endpoint uses tokens from the gateway's own authorization server.

  4. Complete the Connector Gateway quickstart with a test account to verify browser sign-in and an authenticated MCP tool call through https://<GATEWAY_HOST>/gw/mcp.

For discovered Kubernetes backends, also complete Connect to in-cluster endpoints before testing their tools. Once these checks pass, follow Roll out gateway clients.

How connector access works​

The gateway asks the Enterprise Manager to resolve each caller and return their connector grants. Each connector's connector policy determines which callers the gateway exposes it to.

Turn on tool call recording and observability​

Tool call recording, which populates the console's Tool Usage screen, is off by default. Turn it on with connector-gateway.enterpriseConfig.outbox.enabled and connector-gateway.meteringTokenProjection.enabled, as described in Record tool calls.

Audit events, OpenTelemetry export, and Prometheus metrics are also off by default. See Forward Connector Gateway audit logs and Collect Connector Gateway telemetry.

After enabling​

  1. Confirm the gateway pod is ready. It registers with the Enterprise Manager at startup and becomes ready once its first connector catalog read succeeds. The admin Connectors view in the console then appears for the ID you set.
  2. Add connectors and grant them to groups, either in the console under Connectors or through the Enterprise Manager API, which exposes the same operations under /v1/gateways/{gateway_id}/connectors.
  3. Direct users to the deployment-specific instructions in the console. See Roll out gateway clients.

Next steps​

Troubleshooting​

The gateway pod stays unready or restarts

The readiness probe fails until the gateway's first connector catalog read from the Enterprise Manager succeeds, so an unready pod means the gateway can't complete its calls to the Enterprise Manager. Check the pod's logs:

kubectl logs deployment/stacklok-enterprise-connector-gateway -n stacklok-system
  • PermissionDenied during registration, followed by a restart. The gateway's ServiceAccount subject is missing from enterprise-manager.grpc.callerAuth.gatewaySaSubjectAllowlist. Add it as shown in Connect the gateway to the Enterprise Manager.
  • Unauthenticated on every call to the Enterprise Manager. The Enterprise Manager rejected the gateway's ServiceAccount token. Check that clusterOidcIssuer matches the issuer your cluster reports.
  • TLS handshake errors. Check that enterprise-manager-grpc-ca holds the CA that signed the Enterprise Manager's gRPC certificate, and that addr uses port 9443 and a hostname on that certificate. After you rotate the CA, restart the gateway; it reads the bundle only at startup.
  • Connection timeouts. Check that the Enterprise Manager's gRPC NetworkPolicy admits the gateway pods, and that the gateway can reach the Redis or Valkey host.