diff --git a/charts/nebari-apps/README.md b/charts/nebari-apps/README.md index 5a86ec7..45719b7 100644 --- a/charts/nebari-apps/README.md +++ b/charts/nebari-apps/README.md @@ -62,6 +62,8 @@ wrong type fails the install. | mcp.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy. | | mcp.image.repository | string | `"ghcr.io/nebari-dev/apps-pack/apps-mcp"` | apps-mcp image repository. | | mcp.image.tag | string | `""` | Image tag. Empty defaults to the chart's appVersion. | +| mcp.orgCABundle.configMapName | string | `""` | ConfigMap in the release namespace holding the bundle. Empty disables it and the image's default roots apply. | +| mcp.orgCABundle.key | string | `"ca-bundle.crt"` | Key in the ConfigMap that holds the bundle. | | mcp.replicaCount | int | `1` | apps-mcp replicas. | | mcp.resources | object | `{"limits":{"cpu":"250m","memory":"256Mi"},"requests":{"cpu":"250m","memory":"256Mi"}}` | Container resources. Requests equal limits, so the pod is Guaranteed. | | nameOverride | string | `""` | Override the chart name used in resource names. | diff --git a/charts/nebari-apps/templates/mcp.yaml b/charts/nebari-apps/templates/mcp.yaml index 3e212c3..0b9f4b7 100644 --- a/charts/nebari-apps/templates/mcp.yaml +++ b/charts/nebari-apps/templates/mcp.yaml @@ -31,6 +31,11 @@ spec: value: http://{{ include "nebari-apps.fullname" . }}-api.{{ .Release.Namespace }}.svc.cluster.local:8080 # The SPA id: bearer passthrough may carry a UI token. {{- include "nebari-apps.oidcEnv" (dict "root" . "spaVar" "OIDC_SPA_CLIENT_ID") | nindent 12 }} + {{- if .Values.mcp.orgCABundle.configMapName }} + # httpx (the device flow's Keycloak calls) reads SSL_CERT_FILE. + - name: SSL_CERT_FILE + value: /etc/ssl/nebari/ca-bundle.crt + {{- end }} {{- /* Last, so an extraEnv entry overrides a chart variable of the same name. */}} {{- with .Values.mcp.extraEnv }} {{- toYaml . | nindent 12 }} @@ -62,10 +67,25 @@ spec: # The only writable path: Python's tempfile and any library cache. - name: tmp mountPath: /tmp + {{- if .Values.mcp.orgCABundle.configMapName }} + - name: org-ca + mountPath: /etc/ssl/nebari + readOnly: true + {{- end }} volumes: - name: tmp emptyDir: sizeLimit: 64Mi + {{- with .Values.mcp.orgCABundle }} + {{- if .configMapName }} + - name: org-ca + configMap: + name: {{ .configMapName }} + items: + - key: {{ .key }} + path: ca-bundle.crt + {{- end }} + {{- end }} --- apiVersion: v1 kind: Service diff --git a/charts/nebari-apps/values.schema.json b/charts/nebari-apps/values.schema.json index 4b5bc57..ab2c5de 100644 --- a/charts/nebari-apps/values.schema.json +++ b/charts/nebari-apps/values.schema.json @@ -957,6 +957,22 @@ "additionalProperties": false }, "description": "Extra container env vars (Kubernetes EnvVar)." + }, + "orgCABundle": { + "type": "object", + "properties": { + "configMapName": { + "type": "string", + "description": "ConfigMap in the release namespace holding a PEM CA bundle; empty disables it." + }, + "key": { + "type": "string", + "minLength": 1, + "description": "Key in the ConfigMap that holds the bundle." + } + }, + "additionalProperties": false, + "description": "CA bundle apps-mcp trusts for outbound HTTPS (SSL_CERT_FILE)." } }, "additionalProperties": false diff --git a/charts/nebari-apps/values.yaml b/charts/nebari-apps/values.yaml index 48ac329..73bba13 100644 --- a/charts/nebari-apps/values.yaml +++ b/charts/nebari-apps/values.yaml @@ -295,6 +295,16 @@ mcp: # -- Extra environment variables for apps-mcp, after the chart's own (an # entry with the same name wins). extraEnv: [] + # A PEM CA bundle apps-mcp trusts for outbound HTTPS, for clusters whose + # Keycloak certificate (`keycloak.url`, which the login device flow calls) + # is signed by a private CA. The bundle replaces the image's trust store, + # so include the public roots too if anything else needs them. + orgCABundle: + # -- ConfigMap in the release namespace holding the bundle. Empty + # disables it and the image's default roots apply. + configMapName: "" + # -- Key in the ConfigMap that holds the bundle. + key: ca-bundle.crt # ============================================================================= # Keycloak (used when api.auth.enabled) diff --git a/docs/src/content/docs/configuration.md b/docs/src/content/docs/configuration.md index b9595a0..17ab1c9 100644 --- a/docs/src/content/docs/configuration.md +++ b/docs/src/content/docs/configuration.md @@ -155,11 +155,12 @@ in). | `mcp.image.repository` / `tag` / `pullPolicy` | `ghcr.io/nebari-dev/apps-pack/apps-mcp` / `""` / `IfNotPresent` | MCP image. | | `mcp.replicaCount` | `1` | **Keep at 1**: device-flow sessions are held in process memory. | | `mcp.resources` | `250m/256Mi` (requests = limits) | Pod resources. | +| `mcp.orgCABundle.configMapName` / `key` | `""` / `ca-bundle.crt` | ConfigMap (release namespace) with a PEM CA bundle the MCP server trusts for outbound HTTPS. Set it when `keycloak.url` has a certificate from a private CA, or `authenticate` fails with `CERTIFICATE_VERIFY_FAILED`. The bundle replaces the image's trust store, so include the public roots too. | Environment: `API_URL`, `AUTH_ENABLED`, `OIDC_ISSUER`, `OIDC_ISSUER_INTERNAL`, `OIDC_DEVICE_CLIENT_ID` (from the UI's OIDC Secret); optional `OIDC_JWKS_URL`, `OIDC_AUDIENCE`, `HOST`, `PORT`, `LOG_LEVEL`, `MCP_SESSION_TTL_SECONDS` (default 28800: -how long an idle device-flow login is kept). +how long an idle device-flow login is kept), `SSL_CERT_FILE` (set by `mcp.orgCABundle`). ## Logs and probes diff --git a/docs/src/content/docs/mcp.md b/docs/src/content/docs/mcp.md index a214d20..ed32d5d 100644 --- a/docs/src/content/docs/mcp.md +++ b/docs/src/content/docs/mcp.md @@ -58,6 +58,11 @@ Enforcement is layered: On clusters with auth disabled, `authenticate` reports `not_required` and every tool works anonymously. +The device flow calls Keycloak at its browser-facing URL (`keycloak.url`) from inside the +cluster. If that certificate comes from a private CA, `authenticate` fails with +`CERTIFICATE_VERIFY_FAILED` until the CA is trusted through +[`mcp.orgCABundle`](/configuration/#apps-mcp). + ## Tools | Tool | Purpose |