Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 99 additions & 16 deletions docs/auth-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:** `<nebariapp-name>`
- **Client ID:** `<namespace>-<nebariapp-name>` (namespace-scoped to prevent collisions)
- **Client protocol:** `openid-connect`
- **Access type:** `confidential`
- **Redirect URIs:** `https://<hostname><redirectURI>`
- **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 `<nebariapp-name>-oidc-client`:

```yaml
apiVersion: v1
kind: Secret
metadata:
name: <nebariapp-name>-oidc-client
labels:
app.kubernetes.io/name: nebariapp
app.kubernetes.io/instance: <nebariapp-name>
app.kubernetes.io/managed-by: nebari-operator
data:
client-id: <base64-encoded>
client-secret: <base64-encoded>
client-id: <base64-encoded> # Always present. Value: <namespace>-<nebariapp-name>
client-secret: <base64-encoded> # Always present. Cryptographically generated.
issuer-url: <base64-encoded> # Present when external consumers are configured.
# Value: Keycloak issuer URL (e.g., https://keycloak.example.com/realms/nebari)
spa-client-id: <base64-encoded> # Present when spaClient is enabled.
device-client-id: <base64-encoded> # Present when deviceFlowClient is enabled.
```

The operator also creates RBAC resources granting your app's ServiceAccount read
access to the secret:

- **Role:** `<nebariapp-name>-oidc-secret-reader`
- **RoleBinding:** `<nebariapp-name>-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
Expand Down Expand Up @@ -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:
Expand All @@ -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: <nebariapp-name>-oidc-client
key: client-id
- name: OAUTH_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: <nebariapp-name>-oidc-client
key: client-secret
- name: OAUTH_ISSUER_URL
valueFrom:
secretKeyRef:
name: <nebariapp-name>-oidc-client
key: issuer-url
optional: true # May not be present in all configurations
```

The OIDC discovery URL can be constructed as:
`<issuer-url>/.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

Expand Down
6 changes: 3 additions & 3 deletions docs/nebariapp-crd-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<name>-oidc-client`. The client ID follows the convention `<namespace>-<nebariapp-name>`. 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://<hostname><redirectURI>`. |
| `clientSecretRef` | *string | No | - | Reference to a Secret containing `client-id` and `client-secret`. If omitted and `provisionClient` is true, the operator creates `<name>-oidc-client`. |
| `clientSecretRef` | *string | No | - | Reference to a Secret containing `client-id` and `client-secret`. If omitted and `provisionClient` is true, the operator creates `<name>-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`. |
Expand Down
53 changes: 49 additions & 4 deletions examples/wrap-existing-chart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- `<release>-<chartname>` (e.g., `myrelease-podinfo`) - most charts
- `<release>` 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.
Loading