Skip to main content

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.