Nextcloud OIDC Fixes
Root Cause
The Nextcloud user_oidc app fetches the OIDC provider configuration via --discoveryuri. Since the app does not support individual endpoint overrides, it uses the URLs from the discovery response for both the browser redirect (authorization_endpoint) and server-side calls (token_endpoint, userinfo_endpoint, jwks_uri).
The old discovery URI was https://id.127.0.0.1.sslip.io/realms/mijnbureau/.well-known/openid-configuration which:
- Resolves (via CoreDNS rewrite) to the ingress controller’s ClusterIP — works
- But the ingress controller’s TLS certificate uses a mkcert CA that the Nextcloud pod does not trust — TLS verification fails → “Could not reach the OpenID Connect provider”
Solution
Leverage Keycloak’s KC_HOSTNAME_BACKCHANNEL_DYNAMIC to return different URLs in the discovery response for frontchannel vs. backchannel endpoints:
| Endpoint | Type | URL | Purpose |
|---|---|---|---|
authorization_endpoint | frontchannel | https://id.127.0.0.1.sslip.io/... | Browser redirect (public) |
token_endpoint | backchannel | http://keycloak-keycloak/... | Server-to-server (internal, no TLS) |
userinfo_endpoint | backchannel | http://keycloak-keycloak/... | Server-to-server (internal, no TLS) |
jwks_uri | backchannel | http://keycloak-keycloak/... | Server-to-server (internal, no TLS) |
This avoids TLS entirely for server-side communication.
Files Modified
1. helmfile/apps/keycloak/keycloak.yaml.gotmpl (line 57)
Wired through extraEnvVars from the environment config so that KC_HOSTNAME_BACKCHANNEL_DYNAMIC: true takes effect.
Change:
extraEnvVars: {{ .Values.application.keycloak.extraEnvVars | default list | toYaml | nindent 2 }}
The existing application.keycloak.extraEnvVars in the demo environment already contained:
- name: KC_HOSTNAME_BACKCHANNEL_DYNAMIC
value: "true"
- name: KC_HOSTNAME_STRICT
value: "false"
The Bitnami Keycloak chart already sets KC_HOSTNAME_URL to https://id.127.0.0.1.sslip.io (via production: true + ingress hostname). With KC_HOSTNAME_BACKCHANNEL_DYNAMIC: true, Keycloak uses the frontend URL for frontchannel endpoints and the request URL for backchannel endpoints in the discovery response.
2. helmfile/apps/nextcloud/values.yaml.gotmpl (line 160)
Made discoveryUri configurable with a fallback to the old authentication.oidc.issuer-based URL.
Before:
discoveryUri: "{{ .Values.authentication.oidc.issuer }}/.well-known/openid-configuration"
After:
discoveryUri: "{{ .Values.application.nextcloud.oidc.discoveryUri | default (printf "%s/.well-known/openid-configuration" .Values.authentication.oidc.issuer) }}"
3. helmfile/environments/demo/mijnbureau.yaml.gotmpl (lines 63-65, 108-109)
Added authentication.client.nextcloud:
authentication:
client:
nextcloud:
client_id: "nextcloud"
client_secret: "f57f464dc535fba69c1fea65eb5722e02999da43"
Added application.nextcloud.oidc.discoveryUri:
application:
nextcloud:
oidc:
discoveryUri: "http://keycloak-keycloak/realms/mijnbureau/.well-known/openid-configuration"
How It Works
- Nextcloud pod fetches discovery document from
http://keycloak-keycloak/realms/mijnbureau/.well-known/openid-configuration(internal HTTP, no TLS) - Keycloak (
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: true+KC_HOSTNAME_URLset to public URL) returns:authorization_endpoint:https://id.127.0.0.1.sslip.io/realms/mijnbureau/protocol/openid-connect/authtoken_endpoint:http://keycloak-keycloak/realms/mijnbureau/protocol/openid-connect/tokenuserinfo_endpoint:http://keycloak-keycloak/realms/mijnbureau/protocol/openid-connect/userinfojwks_uri:http://keycloak-keycloak/realms/mijnbureau/protocol/openid-connect/certs
- User clicks “Login with Keycloak” → browser redirected to public HTTPS authorization URL → Keycloak login page works
- After login, Nextcloud exchanges auth code via internal HTTP token endpoint → no TLS issues
- Nextcloud fetches user info via internal HTTP userinfo endpoint → no TLS issues
4. helmfile/apps/nextcloud/values.yaml.gotmpl (lines 163-180)
Added lifecycleHooks.postStart that runs on every container start. The post-install hook (which configures the OIDC provider via occ user_oidc:provider) only runs once on first install. On helm upgrade or pod restart, the stored provider endpoints would retain old HTTPS URLs. The postStart hook deletes and recreates the provider on every start so it always picks up the current discovery URI.
lifecycleHooks:
postStart:
exec:
command:
- /bin/bash
- -c
- |
for ((i=0; i<30; i++)); do
if php /var/www/html/occ status --output json 2>/dev/null | grep -q '"installed":true'; then
break
fi
sleep 2
done
php /var/www/html/occ user_oidc:provider keycloak --delete 2>/dev/null || true
php /var/www/html/occ user_oidc:provider keycloak \
--clientid="{{ .Values.authentication.client.nextcloud.client_id }}" \
--clientsecret="{{ .Values.authentication.client.nextcloud.client_secret }}" \
--discoveryuri="{{ .Values.application.nextcloud.oidc.discoveryUri | default (printf "%s/.well-known/openid-configuration" .Values.authentication.oidc.issuer) }}"
The retry loop waits up to 60 s for Nextcloud to report "installed":true before running the OIDC commands.
Post-Install Provider Registration Does Not Re-Run
The Bitnami entrypoint’s post-installation hooks run only once on first install. On helm upgrade or pod restart the occ user_oidc:provider command from the post-install script is never re-executed, so even after changing discoveryUri the stored provider endpoints keep old URLs.
The lifecycleHooks.postStart fix above ensures the provider is always refreshed on pod start.
Deploy
Run helmfile apply to deploy the changes.
After deploy, verify the provider endpoints by checking the Nextcloud database:
kubectl exec deploy/nextcloud -- php /var/www/html/occ user_oidc:provider keycloak --output=json
The token_endpoint, userinfo_endpoint, and jwks_uri should show http://keycloak-keycloak/... while authorization_endpoint should show https://id.127.0.0.1.sslip.io/....
Test OIDC Login
- Open
https://nextcloud.127.0.0.1.sslip.ioin a browser - Click “Login with Keycloak”
- Should redirect to Keycloak login page (HTTPS, works)
- After login, should redirect back to Nextcloud (auth code exchange via internal HTTP, no TLS error)