Connector Gateway Helm values
Configure the Connector Gateway chart with these Helm values.
This reference lists values declared in the chart's default values.yaml.
Templates can also accept optional settings and inherited global values.
Chart: connector-gateway. Chart version: 0.22.0. Generated from the platform
source at e71926418e98c7598815d0abf07039d3c1646f21.
Values
replicaCount
Type: int
Default: 1
Number of replicas for the deployment
image.repository
Type: string
Default value
image-proxy.stacklok.com/proxy/stacklok-enterprise/ghcr.io/stacklok/stacklok-enterprise/connector-gateway
Container image repository. Enterprise build produced by _release-image.yml
(ko, -tags=enterprise), then promoted to the platform tag by release.yml.
Served to customers via Replicated's image proxy. Customers running their own
mirror override this in their values file. task version-bump rewrites the
trailing :<tag> via a regex anchored on
ghcr.io/stacklok/stacklok-enterprise/<name>: — that anchor still matches
inside this longer proxy URL.
image.tag
Type: string
Default: ""
Image tag (defaults to appVersion if empty)
image.pullPolicy
Type: string
Default: "IfNotPresent"
Image pull policy
imagePullSecrets
Type: list
Default: []
Extra image pull Secrets to apply to the connector-gateway pod. On Replicated
installs, the SDK injects global.replicated.dockerconfigjson and creates an
enterprise-pull-secret Secret in the release namespace; the
connector-gateway.imagePullSecrets helper auto-detects that injection and
merges the SDK secret in front of anything listed here, so customers do NOT need
to add enterprise-pull-secret themselves. Use this list only for additional,
customer-supplied registries.
nameOverride
Type: string
Default: ""
Override the name of the chart
fullnameOverride
Type: string
Default: ""
Override the full name of the resources
serviceAccount.create
Type: bool
Default: true
Specifies whether a service account should be created. The gateway's in-cluster BackendWatcher authenticates to the API server with this ServiceAccount's token, so it is created (and its token mounted) by default.
serviceAccount.automount
Type: bool
Default: true
Automatically mount a ServiceAccount's API credentials. Required: the BackendWatcher reads the MCP backend CRs via the mounted token.
serviceAccount.annotations
Type: object
Default: {}
Annotations to add to the service account
serviceAccount.name
Type: string
Default: ""
The name of the service account to use (if not set and create is true, a name is generated using the fullname template)
rbac.create
Type: bool
Default: true
Specifies whether RBAC resources should be created: the namespaced Role granting
the BackendWatcher get/list/watch on the MCP backend CRs and ConfigMaps, plus
the RoleBinding to the chart's ServiceAccount. The BackendReconciler is
single-namespace by design, so this is a Role / RoleBinding in
.Release.Namespace — never a ClusterRole. Set to false if you manage RBAC
externally or bind a pre-existing ServiceAccount.
podAnnotations
Type: object
Default: {}
Annotations to add to the pod
podLabels
Type: object
Default: {}
Labels to add to the pod
podSecurityContext
Type: object
Default value
fsGroup: 65532
runAsGroup: 65532
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
Security context for the pod (ko/Chainguard nonroot UID = 65532)
securityContext
Type: object
Default value
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65532
Security context for the container
service.type
Type: string
Default: "ClusterIP"
Kubernetes service type
service.port
Type: int
Default: 8443
Service port
service.targetPort
Type: int
Default: 8443
Container target port — the data-plane port the gateway serves /gw/mcp (and the built-in /health, /readyz routes) on.
controlPlane
Type: object
Default: {"enabled":true,"port":8090,"type":"ClusterIP"}
Control plane (per-user connector-configuration API). The gateway serves this on a separate port from the data plane. When enabled, the chart binds VMCP_CONTROL_ADDR=0.0.0.0:<port>, exposes the container port, and creates a dedicated ClusterIP Service (<fullname>-control-plane) so in-cluster callers (e.g. cloud-ui) can reach it without going through the public data-plane route.
Enabled by default: the control plane is core to connector-gateway's per-user model, and enabling it is safe regardless of the rest of the config. The binary only actually serves the control plane when enterpriseConfig.directory is set (otherwise VMCP_CONTROL_ADDR is never bound), and when served without controlPlane auth it is fail-closed (401). So the worst case for a config that doesn't use it is a Service with no backing listener.
Disable only for a deliberately data-plane-only deployment (no directory, e.g. a static/auth-less MCP aggregator) to avoid that dangling Service.
controlPlane.enabled
Type: bool
Default: true
Enable the control-plane port + Service
controlPlane.type
Type: string
Default: "ClusterIP"
Control-plane Service type
controlPlane.port
Type: int
Default: 8090
Control-plane port. The gateway binds 0.0.0.0:<port> and the Service exposes it on the same port.
internalApi
Type: object
Default value
allowedSubjects: []
audience: connector-gateway-internal
enabled: false
issuer: ''
port: 8091
type: ClusterIP
Internal service-to-service API: a listener of its own, called by other platform components (today, the enterprise-manager's directory service, to refresh a Connector's stored tool list) with a projected ServiceAccount token. It is separate from controlPlane because the control plane's auth chain wraps every route with end-user identity, which a service caller does not have. Off by default.
A caller must present a token bound to audience AND have its subject listed in
allowedSubjects. The audience alone is not authorization: the kubelet mints a
token for any audience a pod's spec asks for, so without the subject list every
pod in the cluster could call.
Verifying a token needs the cluster's ServiceAccount signing keys, which the gateway fetches from the kube-apiserver using its own ServiceAccount token (so serviceAccount.automount must stay true). Under a restrictive networkPolicy.allowExternalEgress: false, allow that apiserver endpoint in networkPolicy.egress, or every call is rejected as unauthenticated.
internalApi.enabled
Type: bool
Default: false
Enable the internal listener, its Service, and the app-config section.
internalApi.port
Type: int
Default: 8091
Internal listener port. The gateway binds 0.0.0.0:<port> and the Service exposes it on the same port.
internalApi.type
Type: string
Default: "ClusterIP"
Internal Service type. Keep ClusterIP: this API is for in-cluster callers only.
internalApi.audience
Type: string
Default: "connector-gateway-internal"
The aud claim a caller's token must carry. The enterprise-manager chart's directory.connectorGateway.tokenAudience must be set to the same value.
internalApi.allowedSubjects
Type: list
Default: []
ServiceAccount subjects permitted to call, as system:serviceaccount:<namespace>:<name>. REQUIRED when enabled: the chart refuses to render an enabled internal API with none. Include the enterprise-manager's ServiceAccount.
internalApi.issuer
Type: string
Default: ""
Expected iss claim. Empty skips the issuer check (the signature, audience and subject already bind the token).
telemetry
Type: object
Default value
metrics:
port: 9464
otlp:
enabled: false
endpoint: ''
insecure: false
loggingEnabled: false
tracingEnabled: false
prometheus:
enabled: false
Metrics export (opt-in; internal/telemetry wires both onto ONE MeterProvider via toolhive-core's composite provider). Both exporters default off — no OTLP push, no Prometheus pull, no /metrics route anywhere.
telemetry.otlp.enabled
Type: bool
Default: false
Push metrics to an OTLP collector.
telemetry.otlp.endpoint
Type: string
Default: ""
Collector endpoint (e.g. "otel-collector.observability:4318"). REQUIRED whenever otlp.enabled, tracingEnabled, or loggingEnabled is true; the chart render fails when it is missing (see connector-gateway.autoAppConfig), and internal/infra/vmcp.validateTelemetry refuses it again at the binary's own config-load time.
telemetry.otlp.insecure
Type: bool
Default: false
Disable TLS on the OTLP connection. Only for a collector reachable over a private, trusted network (e.g. cluster-local) — never true against a collector reachable from outside the cluster.
telemetry.otlp.tracingEnabled
Type: bool
Default: false
Push traces to the OTLP collector above, and wrap the data-plane and control-plane HTTP handlers with span instrumentation. Requires endpoint to be set.
telemetry.otlp.loggingEnabled
Type: bool
Default: false
Push slog records to the OTLP collector above, in addition to the existing stderr output. Requires endpoint to be set.
telemetry.prometheus.enabled
Type: bool
Default: false
Expose a pull-scrapeable /metrics route.
/metrics always serves on its own dedicated listener (metrics.port below), never on the control-plane port: that port carries the customer-authenticated per-user connector-config API, and its NetworkPolicy ingress rule has no from: selector, so an unauthenticated route there would be reachable by anything that can reach the port. This holds regardless of controlPlane.enabled.
telemetry.metrics
Type: object
Default: {"port":9464}
Dedicated /metrics listener, bound whenever prometheus.enabled is true. 9464 is OTel's conventional Prometheus-exporter port.
resources
Type: object
Default:
{"limits":{"cpu":"500m","memory":"512Mi"},"requests":{"cpu":"100m","memory":"256Mi"}}
Resource limits and requests for the container. The memory limit leaves headroom for the spikes fan-out adds on top of steady-state usage.
startupProbe
Type: object
Default value
failureThreshold: 60
httpGet:
path: /health
port: http
initialDelaySeconds: 0
periodSeconds: 2
successThreshold: 1
timeoutSeconds: 5
Startup probe configuration. Hits the same /health route as the liveness probe, but its whole job is to HOLD THE LIVENESS PROBE OFF until the process has finished its startup directory work.
WHY IT EXISTS. Nothing serves — not even /health — until Run() reaches its listener bind, and the whole startup directory path runs before that. On a Directory that accepts TCP but does not answer (mid-migration, or a NetworkPolicy that DROPs rather than REJECTs) /health stays unanswerable for that whole path. Against the liveness probe alone (initialDelaySeconds 0, periodSeconds 10, failureThreshold 3) the third failure lands inside that window and the kubelet kills the Pod before it can finish booting: a CrashLoopBackOff caused by the probe budget rather than by anything wrong.
The budget is sized against the CUMULATIVE pre-listener path, since /health binds only after all of it:
- the startup EnsureConnectorGateway registration, which retries a bounded number of times on a transient status before it aborts (registerGateway / registrationRetryPolicy): worst case 3 × 10s call budget + 2 × 2s backoff = 34s;
- then, on success, the backend substrate's FIRST, SYNCHRONOUS catalog refresh: one 10s directory-call read, the activation phase (activationCycleBudget 30s) and the credential phase (credentialCycleBudget, 2 × 10s = 20s), so up to ~60s. The two run in series, so a slow-recovering transient followed by a slow first refresh reaches ~94s before /health binds. 60 × 2s = 120s of grace covers that with margin. Sizing it to registration alone would let the kubelet reap a Pod that had already recovered the transient and was mid-refresh. Any change to the registration attempt budget, or to the refresh cycle budgets, must move with this value; the refresh budgets do not scale with catalog size, tracked at directory_refresh.go's activationCycleBudget note (#3733).
A registration that genuinely cannot succeed still aborts Run() on its own at 34s — the intended self-healing path — before this probe would reap it, so this probe does not mask that abort. After startup the liveness probe takes over with its own (deliberately tight) budget.
livenessProbe
Type: object
Default value
failureThreshold: 3
httpGet:
path: /health
port: http
initialDelaySeconds: 0
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
Liveness probe configuration. Hits the built-in /health route on the data-plane port. Startup is covered by startupProbe above, not by this probe's initialDelaySeconds.
readinessProbe
Type: object
Default value
failureThreshold: 3
httpGet:
path: /readyz
port: http
initialDelaySeconds: 0
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
Readiness probe configuration. Hits the built-in /readyz route, which returns 503 until the gateway's backend substrate is ready and 200 afterwards: the FIRST refresh of the Connector catalog from Directory has succeeded. An unregistered gateway id, or an unreachable Directory, therefore holds this probe at 503 rather than serving an empty catalog as though the install had no Connectors.
Note that these pods also back the <fullname>-control-plane Service, so an unready pod removes the per-user connector-config API from its Endpoints too.
vmcpConfig
Type: object
Default value
aggregation:
conflictResolution: prefix
conflictResolutionConfig:
prefixFormat: '{workload}_'
groupRef: ''
incomingAuth:
type: anonymous
name: connector-gateway
outgoingAuth:
source: discovered
The vMCP configuration the gateway loads at startup. Rendered into the ConfigMap
mounted at {{ .Values.config.mountPath }}/{{
.Values.config.fileName }} and read by the binary via VMCP_CONFIG_PATH
(config.NewYAMLLoader, #1772). The loader is STRICT — unknown fields are
rejected — so keep this to the vMCP config schema. groupRef is status/logging
metadata only: backends are always Directory-sourced now (#3902 — auth-less mode
and the static/K8s-CR backend substrates are retired).
Every field renders verbatim EXCEPT audit: leaving it unset here falls back to
the umbrella's global.stacklok.audit (#4409) — see
connector-gateway.effectiveVmcpConfig. Setting audit here at all, in any
shape, opts out of that fallback and always wins in full.
vmcpConfig.groupRef
Type: string
Default: ""
MCPGroup name backing this gateway, used for status/logging visibility only. Backends are always read from the directory (enterpriseConfig.directory).
enterpriseConfig
Type: string
Default: nil
Enterprise-only configuration (authServer/gateway/directory/controlPlane),
combined with a computed featureFlags section into the gateway's AppConfig
(internal/infra/vmcp/app_config.go). Rendered to a SEPARATE ConfigMap file
({{ .Values.config.enterpriseFileName }}) — ALWAYS rendered
(not gated on this value being non-empty, per the runtime feature-flag mirror:
featureFlags must reliably reflect global.stacklok.* even for the default
auth-less install) — and strict-decoded by the gateway (toolhive-core/config,
KnownFields(true)), so the OSS vmcpConfig above stays pure and the strict OSS
loader handles it untouched (no hand-maintained re-implementation of the OSS
post-processing). OMITTED by default → auth-less gateway. The next enterprise
feature adds a section here, not a new file. NOT a raw passthrough. Two fields
under it are CHART-OWNED and resolved from their own source of truth:
gateway.id from global.stacklok.connectorGatewayId (stripped and
re-projected by connector-gateway.enterpriseConfigProjected), and
authServer.issuer from global.stacklok.authServerIssuer (projected by
connector-gateway.effectiveAppConfig). Writing either here is REFUSED, not
silently ignored — see templates/_helpers.tpl.
appConfig
Type: object
Default: {}
Inline AppConfig override, deep-merged over the document connector-gateway.autoAppConfig computes from enterpriseConfig above plus featureFlags — keys set here always win on conflict (mergeOverwrite). No secret-shaped field currently exists on AppConfig, so there is nothing this key could leak (connector-gateway.rejectSecretOverrides is a no-op today, reserved for the first section that adds one). Independent of enterpriseConfig, which is not customer-facing policy here (that concept is enterprise-manager's own enterpriseConfig) but the existing granular values autoAppConfig reads from.
The primary IdP is normally set at the umbrella via global.stacklok.primaryIdp; appConfig.primaryIdp is the per-install OVERRIDE door (it wins over the projected section via mergeOverwrite). clientClaim there names the corp-IdP access-token claim the tools-route client check reads (the client-claim rule): empty reads the standard cid/client_id pair, and a set value is constrained to cid/client_id/azp/appid, refused at render and at config load unless clientClaimAllowUnsafe (a real boolean, never --set-string) admits it. enterpriseConfig.primaryIdp is NOT a door — it is chart-owned and refused at render, the same as enterpriseConfig.gateway.
appConfig:
featureFlags:
aiGateway: true
# primaryIdp:
# clientClaim: azp
redis
Type: object
Default: {"host":""}
Per-subchart Redis/Valkey TOPOLOGY override. This block overrides ONLY the
topology fields — host, port, clusterMode — of the umbrella's global.redis for
this gateway. The AUTH MODE and the ACL username always resolve from
global.redis and are NOT overridable here: a subchart never diverges the auth
mode from the deployer's single global.redis choice (mirrors how the
ai-gateway operator subchart resolves topology locally but inherits auth from
global). Leave every field empty (the default) to inherit the umbrella's
topology too.
When a host resolves (this block's host, else global.redis.host), the chart
projects a Redis connection into BOTH of connector-gateway's Redis sites — the
shared enterpriseConfig.redis connection AND
enterpriseConfig.authServer.storage.redis_config — so the deployer's single
global.redis choice reaches both. Auth is presence-inferred from
global.redis: an existingSecret renders an ACL password reference
(password_env_var: REDIS_PASSWORD), and a global.redis.username adds the ACL
username (username_env_var: REDIS_USERNAME); with neither, no credential is
rendered at either site. The password itself never enters the config — it is
delivered to the pod as an env var from the referenced Secret (see the
deployment template), never inlined. An operator-set value at either site still
wins over the projection.
redis.host
Type: string
Default: ""
Hostname (or IP/CNAME) of the external Redis/Valkey instance. Empty inherits
global.redis.host.
config.mountPath
Type: string
Default: "/etc/connector-gateway/config"
Directory the vMCP config ConfigMap is mounted into.
config.fileName
Type: string
Default: "vmcp.yaml"
File name of the rendered OSS vMCP config within mountPath. The full path (mountPath/fileName) is passed to the binary via VMCP_CONFIG_PATH.
config.enterpriseFileName
Type: string
Default: "enterprise.yaml"
File name of the rendered app-config document within mountPath. ALWAYS rendered
(not gated on enterpriseConfig being non-empty — see the comment above
enterpriseConfig); its full path is passed to the binary via
VMCP_ENTERPRISE_CONFIG_PATH (a sibling of fileName in the same ConfigMap).
nodeSelector
Type: object
Default: {}
Node labels for pod assignment
tolerations
Type: list
Default: []
Tolerations for pod assignment
affinity
Type: object
Default: {}
Affinity rules for pod assignment
directoryTLS
Type: object
Default value
audience: directory-service
caBundle:
key: ca.crt
secretName: ''
enabled: false
expirationSeconds: 3600
mountPath: /etc/directory-tls
tokenMountPath: /var/run/secrets/directory
Transport to the directory service's gRPC API. When enabled, the chart mounts
the CA bundle, projects an audience-bound ServiceAccount token, and writes the
matching enterpriseConfig.directory.tls paths into the rendered app-config —
so the material the chart provisions is, by construction, the material the
binary reads. Splitting the two (mount here, path there) is how a chart ends up
with a certificate mounted, a port open, and nothing reading it.
The two halves are ONE decision because they are independent gates that are each useless alone (the credential-RPC transport gate): TLS authenticates the directory to the gateway, the projected token authenticates the gateway to the directory's caller gate. A gateway with only one of them provably cannot complete a call.
enterpriseConfig.directory.addr REQUIRES a transport arm — either this block
or the dev-only enterpriseConfig.directory.insecure: true — or the gateway
refuses to start (validateDirectoryTransport,
internal/infra/vmcp/app_config.go). An absent key is not an unknown key, so
strict decoding cannot catch an omitted transport, and a config that never
states one would dial cleartext with nothing identifying the caller.
Leaving this disabled does NOT strip a hand-written
enterpriseConfig.directory.tls block: an operator who mounts their own bundle
through extraVolumes/extraVolumeMounts keeps that escape hatch. The projection
only overrides what the chart itself mounts.
ENABLING it, though, makes this block the ONLY place the tls arm is written, and
that is enforced rather than assumed: a hand-written directory.tls alongside
the toggle is refused at render time through both doors. The two doors fail in
opposite directions, which is why neither can be left to convention —
appConfig.directory.tls.caBundlePath WINS over the chart's mount
(effectiveAppConfig mergeOverwrites the projection), pointing the gateway at a
file nothing mounted; enterpriseConfig.directory.tls LOSES, because the
projection replaces the whole tls map, dropping the operator's value with no
message. Enabling the toggle also requires a directory.addr, or the chart
would mount a bundle and mint a token that nothing reads.
The two halves rotate differently, which matters when you rotate the gRPC CA. The TOKEN is re-read from disk on every RPC, so kubelet's in-place refresh needs no reconnect and no restart. The CA BUNDLE is not: the client reads it once and builds its trust pool at startup, so a rotated bundle lands in the container and is ignored until the Pod is rolled. Since the directory itself picks up a new keypair without a restart, rolling the gateway is the step that is easy to forget — until then every handshake fails.
directoryTLS.enabled
Type: bool
Default: false
Enable the chart-managed directory transport: the CA bundle mount, the projected ServiceAccount token, and the app-config paths that name them.
directoryTLS.caBundle.secretName
Type: string
Default: ""
Secret holding the PEM bundle that verifies the directory's serving certificate. REQUIRED when enabled (render fails without it).
⚠️ Point this at a CA-ONLY Secret. The tempting target is enterprise-manager's
cert-manager-issued <fullname>-grpc-tls, because that is where
cert-manager writes ca.crt — but it also holds tls.key, the directory's serving
private key. Copy ca.crt into its own Secret, or publish the issuer's CA with a
trust-manager Bundle. The chart mounts only the key below, so naming that
Secret would not expose tls.key today; the separation is what keeps that true,
and key: tls.key is refused at render time so the warning above is enforced
rather than merely written down.
directoryTLS.caBundle.key
Type: string
Default: "ca.crt"
Key within that Secret holding the PEM bundle. This is the only key mounted into the container, and it is also the filename half of the app-config's directory.tls.caBundlePath — both are resolved from this one value, so the mount and the path cannot disagree. An empty value is refused (remove the key to use the default).
directoryTLS.mountPath
Type: string
Default: "/etc/directory-tls"
Directory the CA bundle Secret is mounted into. The rendered directory.tls.caBundlePath is <mountPath>/<caBundle.key>.
directoryTLS.tokenMountPath
Type: string
Default: "/var/run/secrets/directory"
Directory the projected ServiceAccount token is mounted into. The token lands at <tokenMountPath>/token, which is the gateway's DefaultDirectoryTokenPath and the rendered directory.tls.tokenPath.
directoryTLS.audience
Type: string
Default: "directory-service"
Audience the directory's caller-auth gate expects. Must equal the directory's
own callerAudience constant, which lives in another Go module behind another
chart — nothing in the platform compiles or renders the two against each other,
so a mismatch surfaces only at runtime, as Unauthenticated on every directory
RPC. Pinning the two is #3330.
In practice there is exactly one working value: the directory's side is a Go constant documented as fixed and not operator-configurable, so changing this can only break an install. It stays a key for now because #3330 may remove it outright; do not treat it as a tuning knob.
An EMPTY value is refused at render time, because it is not a no-op: a projected serviceAccountToken with no audience is minted for the API SERVER's own audience, so the gateway would present the directory a token scoped to a different recipient. Writing the key as null in an overlay restores this default rather than emptying it.
directoryTLS.expirationSeconds
Type: int
Default: 3600
Requested lifetime of the projected TOKEN. kubelet refreshes it in place at ~80% of this, and the gateway re-reads the file per call, so token rotation needs no reconnect and no restart.
Minimum 600. Below that the API server REJECTS the Pod spec outright ("may not specify a duration less than 10 minutes") — it does not clamp, so a smaller value renders cleanly and then fails at apply or sync time. The clamp exists only at the upper bound, against the API server's --service-account-max-token-expiration.
primaryIdpCA
Type: object
Default: {"mountPath":"/etc/primary-idp-ca"}
Where the primary IdP's CA bundle is mounted. CHART-OWNED, and the only knob this feature has here: the SOURCE of the bundle is a platform-wide value, not a per-chart one.
The corporate IdP's network and trust settings ride the primaryIdp mirror
(global.stacklok.primaryIdp), per the primaryIdp trust-settings rule:
global:
stacklok:
primaryIdp:
issuer: https://corp-idp.internal
clientId: connector-gateway
# -- The PEM CA bundle that verifies the IdP's serving certificate.
# Exactly ONE of secretRef or configMapRef, plus an optional key
# (default ca.crt). A CA bundle is public data, so a ConfigMap is a
# first-class source. Read by connector-gateway and cloud-ui.
caBundleRef:
configMapRef:
name: corp-idp-ca
# secretRef:
# name: corp-idp-ca
key: ca.crt
# -- Let OIDC discovery and the token exchange reach an issuer on a
# private address (RFC-1918, link-local). Defaults to false; HTTPS is
# still required for non-localhost hosts. Read by connector-gateway (its
# config upstream) and by enterprise-manager's Directory primary-IdP
# synthesis.
allowPrivateIps: false
What the chart does with them: the bundle is mounted (items-scoped, 0444, no
subPath) at mountPath below, and <mountPath>/<caBundleRef.key>
is written as the primary-idp auth-server upstream's ca_file_path AND as the
primaryIdp.caFilePath the gateway's own code reads — one resolution, so the
mounted file and every configured path cannot drift. Trust is ADDITIVE to the
system roots and scoped to this issuer (the OSS client builds it with
WithSystemRootsPlusCABundle), never a process-wide SSL_CERT_FILE replacement.
⚠️ Prefer a CA-ONLY source, the same warning directoryTLS.caBundle carries: a
cert-manager Secret holds ca.crt next to tls.key, the IdP's serving private key.
Only key is ever mounted, and key: tls.key is refused at render time on the
secretRef arm, so the warning is enforced rather than merely written down.
ROTATION: changing the ref's name, its key, or allowPrivateIps rolls the Pod (the volume source and the hashed ConfigMap are part of the pod template). Rotating the CONTENTS of the same Secret/ConfigMap in place does NOT — the bundle is read once at startup, exactly like directoryTLS's — so that case needs a manual roll.
primaryIdpCA.mountPath
Type: string
Default: "/etc/primary-idp-ca"
Directory the primary IdP CA bundle is mounted into. The rendered ca_file_path
/ primaryIdp.caFilePath is <mountPath>/<caBundleRef.key>.
Chart-owned so those two resolve from one place; writing it as null in an
overlay restores this default rather than emptying it.
meteringTokenProjection
Type: object
Default value
audience: metering-ingest
enabled: false
expirationSeconds: 3600
mountPath: /var/run/secrets/metering
Chart-managed ServiceAccount token projection for the metering ingest's
"metering-ingest" audience (durable-outbox review finding B4/comment #890).
Enable this OR set enterpriseConfig.metering.insecure: true OR supply your own
extraVolumes/extraVolumeMounts — one of the three is REQUIRED whenever
enterpriseConfig.outbox.enabled: true, or the render fails
(connector-gateway.validateConfig's outbox/metering guard). Unlike directoryTLS,
there is no CA bundle here: the sender's dial reuses directory.tls's CA whenever
metering.addr is unset (the common case — the metering ingest rides the same
shared platform gRPC listener the directory client already dials), so this block
projects only the token.
meteringTokenProjection.enabled
Type: bool
Default: false
Project the token and mount it at <mountPath>/token (internal/infra/vmcp.DefaultMeteringTokenPath, or enterpriseConfig.metering.tokenPath if you've overridden that instead of relying on the default path this chart mounts to).
meteringTokenProjection.mountPath
Type: string
Default: "/var/run/secrets/metering"
Directory the projected ServiceAccount token is mounted into. The token lands at <mountPath>/token.
meteringTokenProjection.audience
Type: string
Default: "metering-ingest"
Audience the metering ingest's caller-auth gate expects. Unlike directoryTLS.audience, this is NOT operator-configurable at the ingest side today — "metering-ingest" is a fixed string the Go code on both ends hardcodes — so this key exists for the same reason directoryTLS.audience does (a future pin, #3330-style) rather than as a real tuning knob.
meteringTokenProjection.expirationSeconds
Type: int
Default: 3600
Requested lifetime of the projected TOKEN, same rotation and floor semantics as directoryTLS.expirationSeconds above (kubelet refreshes at ~80%, minimum 600, API server rejects below that outright).
extraEnv
Type: list
Default: []
Additional environment variables for the connector-gateway container.
SECURITY CAVEAT — HTTP_PROXY / HTTPS_PROXY RELOCATE THE SSRF GUARD. The
Gateway refuses to dial a Connector endpoint that resolves into a loopback,
private, link-local (cloud instance metadata), or multicast range unless that
Connector opted in with allow_private_ips. That check runs in a dialer Control
hook, on the peer the kernel is about to connect to. Setting a proxy variable
here makes the PROXY that peer for every backend request, so a Connector whose
endpoint resolves to a private address becomes reachable as far as this gateway
is concerned. The check is relocated, not removed, and the proxy must enforce
the equivalent policy instead. If you proxy backend egress, either exclude
Connector endpoints via NO_PROXY or enforce the deny-list at the proxy.
The gateway emits a startup WARN naming the variable when one is set, since it cannot tell a deliberately-enforcing egress proxy from an open one — it can only tell you the guard moved. There is deliberately no startup REFUSAL, for the same reason. No other variable turns the deny-list off; see TestBackendDialer_NoDenyListDisablingVariable.
extraVolumes
Type: list
Default: []
Additional volumes to attach to the pod. Rendered verbatim under .spec.template.spec.volumes alongside the chart-managed config volume.
extraVolumeMounts
Type: list
Default: []
Additional volume mounts to attach to the connector-gateway container. Rendered
verbatim under the container's .volumeMounts alongside the chart-managed config
mount. Names must match an entry in extraVolumes.
kek
Type: object
Default value
activeVersion: ''
allowGenerateOnUpgrade: false
existingSecret: ''
generate: false
keyringGeneration: ''
value: ''
KEK for embedded auth-server envelope encryption. Required when the effective
AppConfig enables authServer. Choose exactly one source below; losing or
replacing the key makes existing credentials unreadable. Operator procedures
(key formats, delivery, rotation/rollback/recovery/retirement) are in
Rotate the encryption key.
kek.existingSecret
Type: string
Default: ""
Existing Secret in the release namespace with key material under kek. The
chart does not manage it. Required for Argo CD and render-and-apply workflows.
Restart pods after changing its content; retain old versions during rotation.
kek.value
Type: string
Default: ""
Inline, base64-encoded key material. Accepted decoded forms:
single key base64 of 32 random bytes openssl rand -base64 32 version map
base64 of {"1":"<base64 32B>","2":"<base64 32B>"}
Rotation requires the version map: add a higher version and retain older entries
until their rows expire. Prefer existingSecret; Helm release data and
helm get values expose inline values.
kek.generate
Type: bool
Default: false
Generate and preserve a version-1 key on a live Helm install. Do not use with
helm template, Argo CD, or other render-and-apply workflows because they
cannot preserve a key through lookup.
kek.allowGenerateOnUpgrade
Type: bool
Default: false
Allow generation during an upgrade only for a confirmed first enable. A lookup miss can also mean missing Secret RBAC or a client-only renderer.
kek.activeVersion
Type: string
Default: ""
The keyring version new writes seal under during a two-phase rotation. Unset
(empty / 0, the default) means new writes seal under the highest keyring version
present, matching Current()=highest. Set it to a version present in the keyring
to PIN the write-under version for the duration of a rolling rotation: phase one
deploys the new key at a higher version with activeVersion still pinned to the
old one (so no replica writes under a key a peer cannot yet read), phase two
moves activeVersion to the new one once every replica carries it. Changing this
value alone re-hashes the pod checksum, so a phase-two helm upgrade triggers a
rolling restart on its own. When kek.value is an inline version map, this is
validated at render to name a version actually present in that map. See
Rotate the encryption key.
kek.keyringGeneration
Type: string
Default: ""
A free-form token that forces a rolling restart when it changes, so phase one of a rotation reliably rolls out the new keyring even when activeVersion does not change. It is hashed into the pod checksum alongside kek.value / kek.existingSecret / kek.activeVersion. This is REQUIRED in existingSecret mode, where the Secret's CONTENT is not part of the checksum: adding a version to the external Secret changes nothing the chart can see, and on a second or later rotation activeVersion is already pinned to the current version, so phase one would otherwise perform no rollout and phase two's single restart would strand rows. Bump this (e.g. an incrementing integer, or the delivered Secret's resourceVersion) on every phase one. Harmless in value/generate mode, where a keyring change already re-hashes the checksum. See Rotate the encryption key.
authServerKeys
Type: object
Default value
allowGenerateOnUpgrade: false
existingSecret: ''
generate: false
rotatedHmacSecretKeys: []
rotatedSigningKeys: []
Durable key material for the embedded auth server. Required when the effective
AppConfig enables authServer. The signing key keeps JWTs valid across pod
restarts and replicas; the distinct HMAC secret keeps authorization codes and
refresh tokens valid. Choose exactly one source below.
authServerKeys.existingSecret
Type: string
Default: ""
Existing Secret in the release namespace containing signing-key.pem and
hmac-secret. The signing key must be a PEM-encoded ECDSA private key. The HMAC
file must contain at least 32 bytes; the command openssl rand 32 > hmac-secret
produces a suitable raw file. The chart does not manage this Secret. Required
for Argo CD and render-and-apply workflows. Restart pods after changing its
content.
authServerKeys.generate
Type: bool
Default: false
Generate and preserve an ECDSA P-256 signing key and a separate 32-byte HMAC
secret on a live Helm install. Do not use with helm template, Argo CD, or
other render-and-apply workflows because they cannot preserve generated material
through lookup.
authServerKeys.allowGenerateOnUpgrade
Type: bool
Default: false
Allow generation during an upgrade only for a confirmed first enable. A lookup miss can also mean missing Secret RBAC or a client-only renderer.
authServerKeys.rotatedHmacSecretKeys
Type: list
Default: []
Keys, in the authServerKeys.existingSecret Secret, holding HMAC secrets retired
from hmac-secret by a rotation. Requires existingSecret: generate only ever
writes signing-key.pem and hmac-secret and has no mechanism to preserve a
rotated key, so the chart refuses this combination rather than render a Pod that
wedges in ContainerCreating. The gateway still accepts authorization codes and
refresh tokens signed with any listed key, so listing an old key here before
removing it from the Secret is what makes rotation possible without invalidating
everything outstanding. Order matters only in that the chart always mounts and
wires hmac-secret first; add the operator's newest retired key last so
removing the oldest is a one-line diff.
authServerKeys.rotatedSigningKeys
Type: list
Default: []
Keys, in the authServerKeys.existingSecret Secret, holding signing keys retired
from signing-key.pem by a rotation. Requires existingSecret: generate only
ever writes signing-key.pem and hmac-secret and has no mechanism to preserve
a rotated key, so the chart refuses this combination rather than render a Pod
that wedges in ContainerCreating. Each listed key is published in the auth
server's JWKS alongside the current signing key, so tokens it already signed
keep validating for every resource server while the primary key rotates. Order
matters only in that the chart always mounts and wires signing-key.pem first;
add the operator's newest retired key last so removing the oldest is a one-line
diff. Remove a retired key from this list (and from the Secret) once every token
it signed has expired — leaving it listed indefinitely, or listing a key known
to be compromised, keeps forged tokens verifying fleet-wide for as long as it
stays here.
networkPolicy.enabled
Type: bool
Default: false
Enable a NetworkPolicy for the connector-gateway pod. Default off: regulated
customers running a default-deny CNI policy engine (Kyverno
add-network-policy, OPA Gatekeeper) opt in explicitly.
networkPolicy.allowExternalEgress
Type: bool
Default: true
Allow ALL egress traffic. NOTE: NetworkPolicies are additive, so this does not
merely "not restrict" egress — it GRANTS allow-all egress to this pod,
OVERRIDING any cluster-wide default-deny egress policy (Kyverno
add-network-policy, Gatekeeper) that would otherwise apply.
networkPolicy.enabled: true with this left true is NOT a locked-down workload
— it only adds an ingress restriction.
Recommended default nonetheless: every outbound destination this gateway dials
(global.stacklok.primaryIdp.issuer, enterprise-manager's directory gRPC, and
every MCP backend the BackendWatcher discovers via in-cluster CRs) is
deployment-specific — this chart has no way to know your actual backends at
template time, so a fixed allow-list here would be a guess. Set to false and
populate egress below to actually lock this pod down (this gateway holds the
KEK and the primary-IdP client secret, so this matters more than usual).
networkPolicy.egress
Type: list
Default: []
Additional egress rules (Kubernetes NetworkPolicyEgressRule shape), applied when
allowExternalEgress is false. Must cover DNS (UDP/TCP 53) plus every
backend/IdP/directory destination this install actually dials — omitting one
doesn't fail loudly, the gateway just can't reach it. If
enterpriseConfig.outbox.enabled is true (issue #2349), this MUST also include
redis.addr and whichever of metering.addr/directory.addr the background
sender resolves to — see the outbox/metering/redis comments above. If
internalApi.enabled is true, this MUST also allow the kube-apiserver endpoint
(typically TCP 443 or 6443 to the control-plane address of the
kubernetes.default Service, which is cluster-specific and is not a pod, so a
podSelector rule cannot match it): the internal API fetches the cluster's
ServiceAccount signing keys from it. Without that rule every call to the
internal API is rejected as unauthenticated, with no render-time signal.