Skip to content

fix(auth/keycloak): use external Keycloak URL for browser-facing OIDC endpoints#111

Merged
dcmcand merged 3 commits into
mainfrom
fix/oauth-browser-facing-endpoints-public
Apr 28, 2026
Merged

fix(auth/keycloak): use external Keycloak URL for browser-facing OIDC endpoints#111
dcmcand merged 3 commits into
mainfrom
fix/oauth-browser-facing-endpoints-public

Conversation

@dcmcand

@dcmcand dcmcand commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Split SecurityPolicy OIDC endpoint overrides by audience: Token stays on the in-cluster URL (Envoy back-channel), Authorization and EndSession move to the public URL (browser front-channel).
  • Adds an externalRealmURL() helper mirroring internalRealmURL().
  • When KEYCLOAK_EXTERNAL_URL is unset, leaves Authorization/EndSession unset; Envoy Gateway then triggers OIDC discovery against the in-cluster issuer. Discovery only produces public URLs if Keycloak's own frontendUrl is configured. Operator now logs a startup warning so this trap is visible.
  • Updated TestKeycloakProvider_GetEndpointOverrides with five cases covering the split, trailing-slash normalization, and the no-ExternalURL fallback.

Why

On a clean NIC deploy, browsers hitting any NebariApp protected by Keycloak OAuth2 are redirected to http://keycloak-keycloakx-http.keycloak.svc.cluster.local:8080 - which the browser cannot resolve. The OAuth2 flow dead-ends and no UI is reachable.

Surfaced during fresh-install validation of the Nebari LLM Serving Pack (nebari-dev/nebari-llm-serving-pack#65), which loads the key-manager UI as a NebariApp.

Closes #110

Follow-ups (deliberately out of scope here)

Test plan

  • go test ./internal/controller/reconcilers/auth/providers/... passes - five cases covering the split, trailing-slash normalization, no-ExternalURL fallback
  • go test ./... no regressions vs main (pre-existing TestControllers HTTPRoute scheme failure unrelated)
  • go build ./... clean
  • Validation against the llmd-validate cluster: build the operator image, swap it in via a temporary override on NIC's nebari-operator Application, confirm the rendered SecurityPolicy now uses https://keycloak.<base> for Authorization+EndSession, confirm browser login completes

dcmcand added 2 commits April 27, 2026 18:29
… endpoints

The SecurityPolicy generated for a NebariApp with Keycloak auth had
all four oidc.provider endpoints populated from the in-cluster Keycloak
service URL. The Authorization and EndSession endpoints are
browser-facing - the user's browser is redirected to them - so the
in-cluster URL fails DNS resolution for the browser and the entire
OAuth2 flow dead-ends.

Split the override generation by who actually hits each endpoint:

  - Token: Envoy proxy back-channel (server-side). Keep on the
    in-cluster URL: lower latency, avoids requiring Envoy to trust the
    public TLS chain.
  - Authorization, EndSession: browser front-channel. Use the publicly
    routable URL from KEYCLOAK_EXTERNAL_URL.

When KEYCLOAK_EXTERNAL_URL is unset, leave Authorization and EndSession
as nil overrides; Envoy then falls back to the values from the OIDC
discovery document fetched at the issuer URL, which works as long as
Keycloak's frontendUrl is set correctly.

Adds a new externalRealmURL() helper that mirrors internalRealmURL()
and trims the optional trailing slash on KEYCLOAK_EXTERNAL_URL.

Updated TestKeycloakProvider_GetEndpointOverrides to cover:
  - the new split (Token internal, Authorization+EndSession external)
  - the trailing-slash normalization
  - the fallback when ExternalURL is empty

Surfaced during fresh-install validation of the Nebari LLM Serving
Pack on a clean NIC AWS deploy: browsers hitting the key-manager UI
were 302'd to http://keycloak-keycloakx-http.keycloak.svc.cluster.local:8080
which they cannot resolve.

Closes #110
Code review pointed out the previous comment was misleading: 'Envoy
falls back to OIDC discovery' is mechanically accurate but the
discovery doc returned by an in-cluster Keycloak only contains public
URLs if Keycloak itself has frontendUrl configured. Otherwise the
discovery doc round-trips the same in-cluster URLs the override would
have used, and the browser-facing redirect still dead-ends.

Two changes:

  1. Tighten the GetEndpointOverrides doc comment to spell out the
     fallback semantics: it only works when Keycloak's frontendUrl /
     KC_HOSTNAME_URL is set, otherwise discovery will return in-cluster
     URLs.
  2. Log a one-line WARNING at operator startup when ExternalURL is
     empty, telling the operator to either set KEYCLOAK_EXTERNAL_URL
     or configure Keycloak frontendUrl. Visible in operator-manager
     logs immediately on startup; no action at reconcile time so it
     doesn't spam per-NebariApp.
@github-actions

Copy link
Copy Markdown

Docker Images Built

Images pushed to Quay.io for branch fix-oauth-browser-facing-endpoints-public:

Image Tag Platforms
Operator quay.io/nebari/nebari-operator:fix-oauth-browser-facing-endpoints-public linux/amd64 + linux/arm64

Test the operator:

kubectl apply -k https://github.com/nebari-dev/nebari-operator.git/config/default?ref=fix/oauth-browser-facing-endpoints-public
kubectl set image deployment/nebari-operator-controller-manager manager=quay.io/nebari/nebari-operator:fix-oauth-browser-facing-endpoints-public -n nebari-operator-system

@marcelovilla marcelovilla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @dcmcand. Just a small comment but approving nonetheless

Comment on lines +62 to +72
// externalRealmURL returns the publicly routable base URL for the Keycloak realm,
// or empty string when KEYCLOAK_EXTERNAL_URL is not configured.
func (p *KeycloakProvider) externalRealmURL() string {
if p.Config.ExternalURL == "" {
return ""
}
return fmt.Sprintf("%s/realms/%s",
strings.TrimRight(p.Config.ExternalURL, "/"),
p.Config.Realm)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker but this seems to be the same URL being built by the GetExternalIssuerURL function:

// GetExternalIssuerURL returns the publicly routable Keycloak issuer URL.
func (p *KeycloakProvider) GetExternalIssuerURL(ctx context.Context, nebariApp *appsv1.NebariApp) (string, error) {
if p.Config.ExternalURL == "" {
return "", fmt.Errorf("KEYCLOAK_EXTERNAL_URL not configured; required for external issuer URL")
}
return fmt.Sprintf("%s/realms/%s", strings.TrimRight(p.Config.ExternalURL, "/"), p.Config.Realm), nil
}

Not sure if it's intended because of the difference in handling the p.Config.ExternalURL == "" case but just flagging it in case it's not.

Removes duplicated URL construction flagged in PR #111 review.
@dcmcand dcmcand merged commit 9fa505a into main Apr 28, 2026
8 checks passed
@dcmcand dcmcand deleted the fix/oauth-browser-facing-endpoints-public branch April 28, 2026 09:40
@github-actions

Copy link
Copy Markdown

Docker Images Built

Images pushed to Quay.io for branch fix-oauth-browser-facing-endpoints-public:

Image Tag Platforms
Operator quay.io/nebari/nebari-operator:fix-oauth-browser-facing-endpoints-public linux/amd64 + linux/arm64

Test the operator:

kubectl apply -k https://github.com/nebari-dev/nebari-operator.git/config/default?ref=fix/oauth-browser-facing-endpoints-public
kubectl set image deployment/nebari-operator-controller-manager manager=quay.io/nebari/nebari-operator:fix-oauth-browser-facing-endpoints-public -n nebari-operator-system

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SecurityPolicy generated for NebariApp uses in-cluster Keycloak URL for browser-facing endpoints (authorizationEndpoint, endSessionEndpoint)

2 participants