This guide explains how to deploy Frontegg's MCP Gateway Helm chart in your Kubernetes cluster. The chart packages two services that expose an MCP-compliant authorization and tool routing layer in front of your MCP servers.
| Component | Role |
|---|---|
mcp-auth | Serves OAuth, dynamic client registration, authorization, and callback endpoints for MCP clients. |
mcp-gw | Receives MCP requests and routes or authorizes them against Frontegg. |
Both components share a Redis backend for token and session caching and are configured against a Frontegg vendor using region, client credentials, and applicationId.
For each release, the chart renders:
Deployment/<release>-mcp-gateway-auth, which runs themcp-authimage.Deployment/<release>-mcp-gateway-gw, which runs themcp-gwimage.Service/<release>-mcp-gateway-auth, a ClusterIP service onmcpAuth.port.Service/<release>-mcp-gateway-gw, a ClusterIP service onmcpGw.port.- A shared
ServiceAccount, which is optional and enabled by default. - HorizontalPodAutoscaler resources per component, which are optional and disabled by default.
The chart does not ship an Ingress, API gateway, or other layer-7 router. You must front the two services with the path-matching ingress or API gateway you already use, such as NGINX Ingress, Traefik, Istio, Envoy, AWS ALB, GCP HTTPS LB, or Kong. See Routing for the path map you need to configure.
helm repo add frontegg https://frontegg.github.io/helm-charts/
helm repo update
helm upgrade --install mcp-gateway frontegg/mcp-gateway -f my-values.yamlEvery value under env must be filled in for a working deployment. Names are camelCase in values.yaml and are translated to UPPER_SNAKE_CASE environment variables on both containers. For example, vendorClientId becomes VENDOR_CLIENT_ID.
env:
# Redis shared cache for sessions and tokens.
redisHost: my-redis.example.com
redisPort: "6379"
redisPassword: "<redis-password>"
redisDb: "0"
redisTlsEnabled: "true"
cacheTtl: "300"
# Frontegg vendor.
fronteggRegion: "eu" # eu | us | ca | au | stg
vendorClientId: "<vendor-client-id>"
vendorClientSecret: "<vendor-client-secret>"
applicationId: "<application-id>"
# The hostname this gateway will be reached at, without scheme.
fronteggMcpGwHost: "tenant.mcp-gw.frontegg.com"
# The same host with scheme, used as the OAuth issuer and authorization URL.
externalAuthorizationUrl: "https://tenant.mcp-gw.frontegg.com"
# Your hybrid auth backend, with scheme.
hybridAuthHost: "https://hybrid-auth.customer.example.com"Optional environment values:
| Key | Purpose |
|---|---|
secretEncryptionKey | 32-character key used to decrypt integration secrets (OAuth client secrets and API keys) that Frontegg delivers encrypted. See Integration secret encryption. |
approvalFlowWebhookEndpoint | Webhook that receives tool-call approval requests. |
eventStdoutEnabled | Set to "true" to write events to the pod's stdout instead of a webhook. See Event delivery. |
eventWebhookProvider | Where to forward audit events: datadog, splunk, coralogix, or webhook. |
eventWebhookUrl | Destination URL for the event webhook. |
eventWebhookSecret | Shared secret for signing event webhook deliveries. |
These are commented out in the default values.yaml. Uncomment and set them if needed.
By default the values under env — including sensitive ones such as redisPassword, vendorClientSecret, and secretEncryptionKey — are rendered inline on the deployments and live in your Helm values. To keep secret material out of Helm entirely, provide it through a Kubernetes Secret you manage yourself (for example with the External Secrets Operator, sealed-secrets, or your secret manager) and reference it with existingSecret.
The Secret's keys must be the final UPPER_SNAKE_CASE environment variable names — the chart passes them through as-is, without the camelCase translation applied to env:
kubectl create secret generic my-mcp-secrets \
--from-literal=REDIS_PASSWORD='<redis-password>' \
--from-literal=VENDOR_CLIENT_SECRET='<vendor-client-secret>' \
--from-literal=SECRET_ENCRYPTION_KEY='<32-character-key>'existingSecret: "my-mcp-secrets"
# Omit the corresponding keys from env so they are sourced from the Secret instead.The chart loads the Secret into both deployments with envFrom:
envFrom:
- secretRef:
name: my-mcp-secretsNotes:
- The Secret must exist in the release namespace before the pods start, or they fail with
CreateContainerConfigError. The chart does not create or manage it. - Only non-empty
envvalues are rendered inline. An empty or unsetenvkey (such as the defaultredisPassword: "") is omitted, so the matching key from the Secret takes effect. If a key is set to a non-empty value both inline underenvand in the Secret, the inlineenvvalue wins (a containerenv.valueoverridesenvFrom) — leave theenvkey empty or unset to source it from the Secret. - Updating the Secret does not restart the pods automatically. Roll them after a change:
kubectl rollout restart deploy/<release>-mcp-gateway-auth deploy/<release>-mcp-gateway-gw(or use a controller such as Stakater Reloader).
The two services must sit behind a path-matching HTTP router, such as an Ingress controller, service mesh gateway, or cloud API gateway. Configure it so that the following auth-related paths land on mcp-auth and everything else lands on mcp-gw:
| Path | Target service |
|---|---|
/.well-known/* | <release>-mcp-gateway-auth |
/authorize | <release>-mcp-gateway-auth |
/dcr/register | <release>-mcp-gateway-auth |
/token | <release>-mcp-gateway-auth |
/integration-callback | <release>-mcp-gateway-auth |
/security-stepup-verify | <release>-mcp-gateway-auth |
/external-mcp/authorize | <release>-mcp-gateway-auth |
/external-mcp/callback | <release>-mcp-gateway-auth |
Everything else, / | <release>-mcp-gateway-gw |
Your router must satisfy these requirements:
- Order and specificity: The auth paths above must win over the catch-all
/route. Most ingress controllers do this automatically based on prefix length. On routers that match in declared order, declare the auth rules first. - Host header preservation: The
Hostheader of the incoming request must matchenv.fronteggMcpGwHost. Both services use it to validate issuer URLs. If your gateway rewrites the upstream host, configure it to pass the original host. - Single external hostname: Both services are reached on the same external host. Only the path differs.
Image tags are pinned in the chart and are bumped by Frontegg as part of releasing a new chart version. Do not override mcpAuth.tag or mcpGw.tag. To pick up a new image, upgrade to a newer chart version:
helm repo update
helm upgrade mcp-gateway frontegg/mcp-gateway -f my-values.yamlEach component has independent resources blocks under mcpAuth.resources and mcpGw.resources. Defaults are conservative, with 200m CPU and 256Mi memory requests and a 512Mi memory limit, and are suitable for low-traffic tenants.
Autoscaling is disabled by default. Enable it per component:
autoscaling:
mcpGw:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
mcpAuth:
enabled: true
minReplicas: 2
maxReplicas: 5
targetCPUUtilizationPercentage: 70When autoscaling.<component>.enabled is true, the Deployment's replicas field is omitted so the HPA can manage it.
All three probes, liveness, readiness, and startup, hit GET /health on port http. Override per component if your environment needs different timings:
mcpGw:
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 10
periodSeconds: 10nodeSelector, tolerations, affinity, podAnnotations, podLabels, podSecurityContext, and securityContext are top-level values and apply to both deployments. There is no per-component override today. To schedule the two components differently, install the chart twice with a nameOverride.
A single ServiceAccount is shared by both deployments. To use an existing one:
serviceAccount:
create: false
name: my-existing-saThe gateway emits audit events to a single sink. Choose one:
- Webhook: set
eventWebhookProvidertogether witheventWebhookUrl(andeventWebhookSecretif the provider requires it). Events are POSTed to your Datadog, Splunk, Coralogix, or generic webhook endpoint. - stdout: set
eventStdoutEnabled: "true". Each event is written to the pod's stdout as a single JSON line, tagged with a stablestreamfield so a log collector (such as the OpenTelemetry Collector, Fluent Bit, or Vector) can pick events out of the container log stream. This needs no external webhook infrastructure.
{"stream":"mcp-gw-events","event":{"eventDomain":"...","origin":"mcp-gw","context":{},"subject":{},"payload":{}}}For example, to keep only gateway events in an OpenTelemetry Collector pipeline:
processors:
filter/events:
logs:
log_record:
- 'body["stream"] != "mcp-gw-events"'Events are written directly to stdout, not through the application logger, so they are not affected by log-level settings.
Set secretEncryptionKey to enable end-to-end encryption of integration secrets (OAuth client secrets and API keys). When configured, Frontegg delivers these secrets to the gateway encrypted and they are never written to your Redis cache in cleartext — the gateway decrypts them at runtime, in memory, only when an integration is used.
- Algorithm: AES-256-GCM, encoded as
v2:<iv_hex>:<authTag_hex>:<ciphertext_hex>. secretEncryptionKeymust be a 32-character string, and identical to the key Frontegg encrypts with. Configure the same key in the Frontegg portal and here.- Values that are not in the
v2:format are passed through unchanged, so the feature can be enabled gradually.
The key is used as-is: its raw characters are the AES-256 key, so the string must be exactly 32 characters (a 32-byte UTF-8 key). Do not generate a 32-byte binary key and then Base64- or hex-encode it for the environment variable — that produces a 44- or 64-character string, which is no longer 32 bytes and fails with an
Invalid key lengtherror. Use a plain 32-character string, and use only ASCII characters (a multi-byte UTF-8 character would push the byte length past 32).
To keep the key out of Helm values, provide it as SECRET_ENCRYPTION_KEY in an existing Secret rather than setting secretEncryptionKey inline.
Generate a valid 32-character key:
node -e "console.log(require('node:crypto').randomBytes(16).toString('hex'))"randomBytes(16) encoded as hex is 32 characters (each byte becomes two hex characters), and those 32 ASCII characters are exactly 32 bytes when used as the key. Use the printed string verbatim as secretEncryptionKey — do not encode it again.
To encrypt a secret with that key in the format the gateway expects:
const { createCipheriv, randomBytes } = require('node:crypto');
function encryptSecret(plaintext, key /* 32-character string */) {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return `v2:${iv.toString('hex')}:${authTag.toString('hex')}:${ciphertext.toString('hex')}`;
}
console.log(encryptSecret(process.argv[2], process.env.SECRET_ENCRYPTION_KEY));SECRET_ENCRYPTION_KEY="<your-32-character-key>" node encrypt-secret.js "my-client-secret"| Key | Default | Description |
|---|---|---|
mcpAuth.repository | 527305576865.dkr.ecr.us-east-1.amazonaws.com/docker-hub/frontegg/hybrid-agen-co-auth | mcp-auth image repository. Override only for private registry mirrors. |
mcpAuth.tag | Pinned by chart | Managed by Frontegg and bumped by chart release. Do not override. |
mcpAuth.pullPolicy | IfNotPresent | Image pull policy. |
mcpAuth.replicaCount | 1 | Used when autoscaling.mcpAuth.enabled is false. |
mcpAuth.port | 8080 | Service port. The container always listens on 8080. |
mcpAuth.resources | requests: {cpu: 200m, memory: 256Mi}, limits: {memory: 512Mi} | Default resource requests and limits. |
mcpAuth.{liveness,readiness,startup}Probe | GET /health on port http | Default health probes. |
mcpGw.* | Mirrors mcpAuth.* defaults | mcp-gw deployment configuration. |
imagePullSecrets | [] | List of name entries for private registry credentials. |
nameOverride and fullnameOverride | "" | Name override values. |
serviceAccount.create | true | Whether to create a ServiceAccount. |
serviceAccount.automount | true | Whether to automount the ServiceAccount token. |
serviceAccount.annotations | {} | Useful for IRSA or Workload Identity. |
serviceAccount.name | "" | If empty, defaults to the chart fullname. |
service.type | ClusterIP | Applied to both services. |
podAnnotations and podLabels | {} | Applied to both deployments' pod templates. |
podSecurityContext | {} | Pod-level security context. |
securityContext | capabilities.drop: [NET_RAW] | Container-level security context. |
autoscaling.mcpAuth.enabled | false | Whether autoscaling is enabled for mcp-auth. |
autoscaling.mcpAuth.minReplicas and maxReplicas | 1 and 100 | Replica bounds for mcp-auth. |
autoscaling.mcpAuth.targetCPUUtilizationPercentage | 80 | CPU utilization target. |
autoscaling.mcpAuth.targetMemoryUtilizationPercentage | Unset | Set to enable memory-based scaling. |
autoscaling.mcpGw.* | Mirrors autoscaling.mcpAuth.* | Autoscaling configuration for mcp-gw. |
volumes and volumeMounts | [] | Applied to both deployments. |
nodeSelector, tolerations, and affinity | {}, [], and {} | Applied to both deployments. |
env.redisHost | "" | Hostname of the shared Redis instance. |
env.redisPort | "6379" | Redis port. |
env.redisPassword | "" | Redis password. |
env.redisDb | "0" | Redis database. |
env.redisTlsEnabled | "true" | Set to "false" for plain Redis. |
env.cacheTtl | "300" | Token and session cache TTL in seconds. |
env.fronteggRegion | "eu" | Supported values are eu, us, ca, au, and stg. |
env.vendorClientId | "" | Frontegg vendor client ID. |
env.vendorClientSecret | "" | Frontegg vendor client secret. |
env.applicationId | "" | Frontegg application ID. |
env.fronteggMcpGwHost | "" | External hostname of the gateway, without scheme. |
env.externalAuthorizationUrl | "" | Same hostname with scheme, published as the OAuth issuer. |
env.hybridAuthHost | "" | URL of your hybrid auth service, with scheme. |
env.secretEncryptionKey | Commented | 32-character key to decrypt integration secrets Frontegg delivers encrypted (AES-256-GCM). |
env.approvalFlowWebhookEndpoint | Commented | Webhook for approval-flow callbacks. |
env.eventStdoutEnabled | Commented | Set to "true" to emit events to stdout instead of a webhook. |
env.eventWebhookProvider | Commented | Example: datadog. |
env.eventWebhookUrl | Commented | Destination URL for event webhooks. |
env.eventWebhookSecret | Commented | Shared secret for signing event webhooks. |
existingSecret | "" | Name of a pre-existing Kubernetes Secret to load into both deployments via envFrom. Its keys must be UPPER_SNAKE_CASE env var names. See Referencing an existing Secret. |
Every key under .Values.env is converted from camelCase to UPPER_SNAKE_CASE and emitted as a container environment variable on both containers. To add a new environment variable, add a key under env.
# Watch the rollout.
kubectl rollout status deploy/<release>-mcp-gateway-auth
kubectl rollout status deploy/<release>-mcp-gateway-gw
# Health checks. Both should return 200.
kubectl port-forward svc/<release>-mcp-gateway-auth 8080:8080 &
curl -fsS http://localhost:8080/health
kubectl port-forward svc/<release>-mcp-gateway-gw 8081:8080 &
curl -fsS http://localhost:8081/healthThere are no CRDs and no persistent state owned by the chart. Redis lives outside it.
Image versions are tied to chart versions. Each chart release pins the matching mcp-auth and mcp-gw images. To roll out a new image, upgrade the chart:
helm repo update
helm upgrade mcp-gateway frontegg/mcp-gateway --version <new-chart-version> -f my-values.yamlDo not override mcpAuth.tag or mcpGw.tag in your values file. Those are managed by Frontegg as part of the chart release.