You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The chart's KeyCloakOAuthenticator (config/jupyterhub/00-gateway-auth.py) derives all four OIDC endpoints from a single issuer via KeyCloakConfig.build():
On private-VPC deployments where DNS for the external Keycloak hostname is only served outside the cluster (i.e. browsers can resolve it via a private DNS zone / external resolver, but in-cluster pods cannot), the hub pod cannot reach the external URL for the OAuth backchannel — curl from the hub returns exit code 6 (host not resolved). Meanwhile the iss claim in tokens has to remain the external URL (that's what token validators and browsers see), so we can't just point everything at the in-cluster URL.
Result: deployers hit this every time on private-VPC clusters and have to override the entire authenticator via hub.extraConfig — a ~40-line Python block that instantiates a plain GenericOAuthenticator and hardcodes split URLs (authorize_url external, token_url + userdata_url in-cluster). That's a lot of load-bearing code that only exists because the chart can't express two-URL configuration.
Proposal
Accept an optional backchannel URL that, when set, overrides token_url and userdata_url while authorize_url and end_session_url keep using the primary (external) issuer.
Values shape:
keycloak:
hostname: keycloak.example.com # external — used for authorize + iss claim# Optional: in-cluster base URL for backchannel calls (token + userinfo).# Empty = same as hostname (backward compatible).backchannelURL: http://keycloak-keycloakx-http.keycloak.svc.cluster.local:8080
Then in KeyCloakConfig.build():
@classmethoddefbuild(
cls,
*,
issuer: str,
backchannel_issuer: str="",
post_logout_redirect_uri: str="",
) ->"KeyCloakConfig":
"""Derive KC endpoint URLs from the realm issuer. When `backchannel_issuer` is set, `token_url` and `userdata_url` use it instead of `issuer`. Enables split-horizon OIDC on private-VPC clusters where hub pods can't reach the external Keycloak hostname. """base=f"{issuer}/protocol/openid-connect"bc_base=f"{(backchannel_issuerorissuer)}/protocol/openid-connect"returncls(
issuer=issuer,
authorize_url=f"{base}/auth", # browsertoken_url=f"{bc_base}/token", # hub → in-cluster if setuserdata_url=f"{bc_base}/userinfo", # hub → in-cluster if setend_session_url=f"{base}/logout", # browserpost_logout_redirect_uri=post_logout_redirect_uri,
)
The chart's templates/hub-config.yaml would compute backchannel_issuer from keycloak.backchannelURL (defaulting to "", so behavior on existing deploys is byte-identical).
Precedent in the ecosystem
nebari-nebi-pack PR fix: don't invalidate session when IdToken cookie is missing #34 added the analogous pattern for OIDC validation: auth.oidc.issuerURL (external, for iss claim matching) + auth.oidc.discoveryURL (in-cluster, for JWKS fetch). Same two-URL split, same motivation.
Keycloak itself supports host-dynamic backchannel URLs via KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true — but only for clients that auto-discover from .well-known/openid-configuration. jupyterhub-oauthenticator uses hardcoded URLs, so this KC feature doesn't help.
Impact
Retires the ~40-line hub.extraConfig.99-oauth Python override that private-VPC deployments currently maintain out-of-band. All the other things that block does (auth_state, groups claim, scopes, allow_all, refresh) are already handled by the chart's KeyCloakOAuthenticator — the single-issuer design is the last blocker.
Summary
The chart's
KeyCloakOAuthenticator(config/jupyterhub/00-gateway-auth.py) derives all four OIDC endpoints from a single issuer viaKeyCloakConfig.build():On private-VPC deployments where DNS for the external Keycloak hostname is only served outside the cluster (i.e. browsers can resolve it via a private DNS zone / external resolver, but in-cluster pods cannot), the hub pod cannot reach the external URL for the OAuth backchannel —
curlfrom the hub returns exit code 6 (host not resolved). Meanwhile theissclaim in tokens has to remain the external URL (that's what token validators and browsers see), so we can't just point everything at the in-cluster URL.Result: deployers hit this every time on private-VPC clusters and have to override the entire authenticator via
hub.extraConfig— a ~40-line Python block that instantiates a plainGenericOAuthenticatorand hardcodes split URLs (authorize_urlexternal,token_url+userdata_urlin-cluster). That's a lot of load-bearing code that only exists because the chart can't express two-URL configuration.Proposal
Accept an optional backchannel URL that, when set, overrides
token_urlanduserdata_urlwhileauthorize_urlandend_session_urlkeep using the primary (external) issuer.Values shape:
Then in
KeyCloakConfig.build():The chart's
templates/hub-config.yamlwould computebackchannel_issuerfromkeycloak.backchannelURL(defaulting to"", so behavior on existing deploys is byte-identical).Precedent in the ecosystem
nebari-nebi-packPR fix: don't invalidate session when IdToken cookie is missing #34 added the analogous pattern for OIDC validation:auth.oidc.issuerURL(external, forissclaim matching) +auth.oidc.discoveryURL(in-cluster, for JWKS fetch). Same two-URL split, same motivation.KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true— but only for clients that auto-discover from.well-known/openid-configuration.jupyterhub-oauthenticatoruses hardcoded URLs, so this KC feature doesn't help.Impact
Retires the ~40-line
hub.extraConfig.99-oauthPython override that private-VPC deployments currently maintain out-of-band. All the other things that block does (auth_state, groups claim, scopes, allow_all, refresh) are already handled by the chart'sKeyCloakOAuthenticator— the single-issuer design is the last blocker.Environment context
keycloak-keycloakx-http.keycloak.svc.cluster.local:8080(HTTP, no CA needed)nebari-data-science-pack@nebari-data-science-pack-0.1.0