From 977e35d986560642c447717d2106aeafe74287c7 Mon Sep 17 00:00:00 2001 From: Chuck McAndrew <6248903+dcmcand@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:57:41 +0200 Subject: [PATCH] docs: document OIDC secret structure, app-native OAuth, and wrap-chart gotchas - Document full OIDC secret structure including issuer-url key, client ID naming convention, and auto-created RBAC resources - Add comprehensive app-native OAuth section with dual-layer auth pattern, credential wiring examples, and Superset reference - Cross-reference auth-flow.md from CRD reference for provisionClient and enforceAtGateway fields - Add service name discovery guidance for wrap-existing-chart pattern - Add Bitnami paywall warning and alternatives (CloudNativePG, Valkey) - Document that Helm sub-chart values cannot use template expressions Closes #4, closes #5, closes #6, closes #7, closes #9 --- docs/auth-flow.md | 115 +++++++++++++++++++++---- docs/nebariapp-crd-reference.md | 6 +- examples/wrap-existing-chart/README.md | 53 +++++++++++- 3 files changed, 151 insertions(+), 23 deletions(-) diff --git a/docs/auth-flow.md b/docs/auth-flow.md index 728ebbe..4148dfb 100644 --- a/docs/auth-flow.md +++ b/docs/auth-flow.md @@ -172,26 +172,45 @@ When `auth.enabled: true`, the nebari-operator creates these resources: The operator calls the Keycloak Admin API to create an OIDC client: -- **Client ID:** `` +- **Client ID:** `-` (namespace-scoped to prevent collisions) - **Client protocol:** `openid-connect` -- **Access type:** `confidential` -- **Redirect URIs:** `https://` +- **Access type:** `confidential` (not public) +- **Standard Flow:** enabled (OAuth2 Authorization Code flow) +- **Redirect URIs:** Both HTTP and HTTPS variants of the hostname +- **Web Origins:** `*` (allows CORS) - **Scopes:** As configured in `spec.auth.scopes` ### 2. Kubernetes Secret -Client credentials are stored in a Secret: +Client credentials are stored in a Secret named `-oidc-client`: ```yaml apiVersion: v1 kind: Secret metadata: name: -oidc-client + labels: + app.kubernetes.io/name: nebariapp + app.kubernetes.io/instance: + app.kubernetes.io/managed-by: nebari-operator data: - client-id: - client-secret: + client-id: # Always present. Value: - + client-secret: # Always present. Cryptographically generated. + issuer-url: # Present when external consumers are configured. + # Value: Keycloak issuer URL (e.g., https://keycloak.example.com/realms/nebari) + spa-client-id: # Present when spaClient is enabled. + device-client-id: # Present when deviceFlowClient is enabled. ``` +The operator also creates RBAC resources granting your app's ServiceAccount read +access to the secret: + +- **Role:** `-oidc-secret-reader` +- **RoleBinding:** `-oidc-secret-reader` + +This means your app's pods can reference the secret in `env.valueFrom.secretKeyRef` +without additional RBAC configuration. + ### 3. Envoy Gateway SecurityPolicy (when `enforceAtGateway: true`) ```yaml @@ -249,10 +268,24 @@ spec: kind: ClusterIssuer ``` -## App-Native OAuth (enforceAtGateway: false) +## App-Native OAuth + +Some applications handle OAuth natively (e.g., Grafana, Superset, Gitea). For these +apps, the operator provisions the Keycloak client and stores credentials, but the app +handles the OAuth flow itself. This is useful when the app needs deeper integration +with the OAuth flow, such as mapping Keycloak groups/roles to app-internal roles. -Some applications handle OAuth natively (e.g., Grafana, GitLab). For these apps, -set `enforceAtGateway: false`: +### Gateway-only auth (app reads JWT from cookies) + +If your app just needs user identity (not role mapping), use `enforceAtGateway: true` +(the default) and read the IdToken cookie as described above. + +### App-native auth only (no gateway enforcement) + +Set `enforceAtGateway: false` to skip gateway auth. The operator will: +- Provision a Keycloak client +- Store credentials in a Secret +- **NOT** create a SecurityPolicy ```yaml auth: @@ -262,14 +295,64 @@ auth: enforceAtGateway: false ``` -The operator will: -- Provision a Keycloak client -- Store credentials in a Secret -- **NOT** create a SecurityPolicy +### Dual-layer auth (recommended for RBAC apps) + +Use both gateway enforcement AND app-native OAuth. The gateway ensures users are +authenticated, while the app reads roles/groups for authorization: + +```yaml +auth: + enabled: true + provider: keycloak + provisionClient: true + # enforceAtGateway defaults to true +``` + +The app also authenticates against the same Keycloak client to get roles/groups. + +### Wiring credentials to your app + +Reference the operator-created OIDC secret to inject credentials as environment +variables. Use `valueFrom.secretKeyRef` to map specific keys: + +```yaml +# In your Helm values (e.g., for an upstream chart's extraEnv/extraEnvRaw) +extraEnvRaw: + - name: OAUTH_CLIENT_ID + valueFrom: + secretKeyRef: + name: -oidc-client + key: client-id + - name: OAUTH_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: -oidc-client + key: client-secret + - name: OAUTH_ISSUER_URL + valueFrom: + secretKeyRef: + name: -oidc-client + key: issuer-url + optional: true # May not be present in all configurations +``` + +The OIDC discovery URL can be constructed as: +`/.well-known/openid-configuration` + +Your app then configures its OAuth provider using these environment variables. + +> **Note on `extraEnv` vs `extraEnvRaw`:** Many upstream Helm charts support +> multiple env var formats. Use `extraEnvRaw` (or the equivalent) when you need +> `valueFrom.secretKeyRef` syntax. Check your upstream chart's documentation for +> the correct field name. + +### Example: Superset with Keycloak RBAC -Your app reads the credentials from the Secret and handles the OAuth flow itself. -This is useful when the app needs deeper integration with the OAuth flow (like -Grafana mapping groups to roles). +The [nebari-superset-pack](https://github.com/nebari-dev/nebari-superset-pack) uses +dual-layer auth with a custom SecurityManager that reads Keycloak roles from the +userinfo endpoint and maps them to Superset roles (Admin, Gamma, Public). See its +[nebari-values.yaml](https://github.com/nebari-dev/nebari-superset-pack/blob/main/examples/nebari-values.yaml) +for the complete configuration. ## Limitations diff --git a/docs/nebariapp-crd-reference.md b/docs/nebariapp-crd-reference.md index 693a13f..6781e8b 100644 --- a/docs/nebariapp-crd-reference.md +++ b/docs/nebariapp-crd-reference.md @@ -83,10 +83,10 @@ spec: |-------|------|----------|---------|-------------| | `enabled` | bool | No | `false` | Whether to enforce OIDC authentication. | | `provider` | string | No | `"keycloak"` | OIDC provider. Values: `keycloak`, `generic-oidc`. | -| `provisionClient` | *bool | No | `true` | Auto-provision an OIDC client in the provider. Only supported for `keycloak`. The operator creates the client and stores credentials in a Secret. | -| `enforceAtGateway` | *bool | No | `true` | Create an Envoy Gateway SecurityPolicy for gateway-level auth. When `false`, the operator provisions the client and Secret but does NOT create a SecurityPolicy - the app handles OAuth natively (e.g., Grafana's built-in OAuth). | +| `provisionClient` | *bool | No | `true` | Auto-provision an OIDC client in the provider. Only supported for `keycloak`. The operator creates the client and stores credentials in a Secret named `-oidc-client`. The client ID follows the convention `-`. See [auth-flow.md](auth-flow.md#2-kubernetes-secret) for the full secret structure. | +| `enforceAtGateway` | *bool | No | `true` | Create an Envoy Gateway SecurityPolicy for gateway-level auth. When `false`, the operator provisions the client and Secret but does NOT create a SecurityPolicy - the app handles OAuth natively. See [auth-flow.md](auth-flow.md#app-native-oauth) for wiring guidance. | | `redirectURI` | string | No | `"/oauth2/callback"` | OAuth2 callback path. The full URL is `https://`. | -| `clientSecretRef` | *string | No | - | Reference to a Secret containing `client-id` and `client-secret`. If omitted and `provisionClient` is true, the operator creates `-oidc-client`. | +| `clientSecretRef` | *string | No | - | Reference to a Secret containing `client-id` and `client-secret`. If omitted and `provisionClient` is true, the operator creates `-oidc-client` with keys: `client-id`, `client-secret`, and optionally `issuer-url`. | | `scopes` | []string | No | `["openid", "profile", "email"]` | OIDC scopes to request during authentication. | | `groups` | []string | No | - | Groups that have access. When specified, only users in these groups are authorized. Case-sensitive. | | `issuerURL` | string | No | - | OIDC issuer URL. Required when `provider=generic-oidc`, ignored for `keycloak`. Example: `https://accounts.google.com`. | diff --git a/examples/wrap-existing-chart/README.md b/examples/wrap-existing-chart/README.md index fe281cb..0b84049 100644 --- a/examples/wrap-existing-chart/README.md +++ b/examples/wrap-existing-chart/README.md @@ -122,9 +122,54 @@ To wrap a different upstream chart: 1. Replace the `dependencies` entry in `Chart.yaml` with your chart 2. Update `nebariapp.service.port` to match the upstream service port -3. Update the `my-pack.podinfo-service-name` helper in `_helpers.tpl` to match - the upstream chart's service naming convention -4. Add any value overrides under the dependency's key in `values.yaml` -5. Run `helm dependency update ./chart/` +3. **Discover the upstream service name** (see below) +4. Update the service name helper in `_helpers.tpl` to match +5. Add any value overrides under the dependency's key in `values.yaml` +6. Run `helm dependency update ./chart/` + +### Discovering the upstream service name + +The NebariApp must reference the upstream chart's Service by its exact name. +Different charts use different naming conventions. **Always verify:** + +```bash +helm template myrelease ./chart/ | grep "kind: Service" -A2 +``` + +Common patterns: +- `-` (e.g., `myrelease-podinfo`) - most charts +- `` only (e.g., `myrelease`) - when the release name contains the + chart name, the fullname template collapses (common with charts like Superset) + +Don't assume - always check with `helm template`. + +### Wrapping charts with Bitnami sub-dependencies + +Many popular Helm charts (Superset, Airflow, etc.) use Bitnami PostgreSQL and +Redis as sub-chart dependencies. Since August 2025, Bitnami images are behind a +paywall. The `bitnamilegacy/*` Docker Hub mirrors exist but are frozen and receive +no security updates. + +If your upstream chart depends on Bitnami sub-charts: +- Override image repositories (e.g., `postgresql.image.repository: bitnamilegacy/postgresql`) +- Consider alternatives: [CloudNativePG](https://cloudnative-pg.io/) for PostgreSQL, + [Valkey](https://valkey.io/) for Redis +- See [nebari-dev/nebari-operator#96](https://github.com/nebari-dev/nebari-operator/issues/96) + for the longer-term platform solution + +### Sub-chart values are static + +Helm sub-chart values do not support template expressions (`{{ }}`). You cannot +dynamically generate configuration for the upstream app from your wrapper chart's +values.yaml. If you need templated config (e.g., generating Python OAuth config +from structured values), put it in example values files that deployers customize, +or create a ConfigMap template in your wrapper chart and mount it via the upstream +chart's volume support. + +### App-native OAuth for RBAC + +If your wrapped application needs role-based access control (not just +"authenticated or not"), see the [App-Native OAuth](../../docs/auth-flow.md#app-native-oauth) +section for how to wire operator-provisioned Keycloak credentials into your app. See the [main README](../../README.md) for the full customization guide.