Objective: Deploy Keycloak on the management cluster as a self-hosted OIDC provider, then reconfigure the HostedCluster to use Keycloak instead of Azure AD.
Approach: Adapted from CI step registry script at ci-operator/step-registry/idp/external-oidc/keycloak/server/ in the openshift/release repo.
| Step | Check | Result | Evidence |
|---|---|---|---|
| 1 | Keycloak deployed on management cluster | PASS | Pod keycloak-0 Ready 1/1 in keycloak namespace |
| 2 | Keycloak OIDC endpoint accessible via edge route | PASS | HTTP/1.1 200 OK from .well-known/openid-configuration |
| 3 | Keycloak clients, users, and groups configured | PASS | postStart log: Keycloak setup done! |
| 4 | Router CA ConfigMap created in HC namespace | PASS | configmap/keycloak-oidc-ca created in clusters namespace |
| 5 | Console client secret created in HC namespace | PASS | secret/console-secret created in clusters namespace |
| 6 | HostedCluster patched with Keycloak OIDC provider | PASS | hostedcluster.hypershift.openshift.io/brcox-sm-dev-hc patched |
| 7 | HostedCluster remains Available after reconfiguration | PASS | Available=True, KubeAPIServerAvailable=True |
| 8 | KAS auth-config updated with Keycloak JWT authenticator | PASS | Issuer URL: https://keycloak-keycloak.apps.brcox-mgmt.../realms/master, audiences: console-test, oc-cli-test |
# Create namespace
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc create ns keycloak
namespace/keycloak created
# Deploy from upstream quickstart (single replica, no DB)
$ curl -sS https://raw.githubusercontent.com/keycloak/keycloak-quickstarts/refs/heads/main/kubernetes/keycloak.yaml \
| sed -e "/- name: .*KC_DB/, +1 d" -e "s/replicas: .*/replicas: 1/" \
| KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc apply -n keycloak -f -
service/keycloak created
service/keycloak-discovery created
statefulset.apps/keycloak created
# Remove postgres (embedded DB only)
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc delete deployment/postgres -n keycloak
deployment.apps "postgres" deleted
# Set admin credentials
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc set env sts/keycloak \
KC_BOOTSTRAP_ADMIN_USERNAME=admin-****** \
KC_BOOTSTRAP_ADMIN_PASSWORD=************ -n keycloak
statefulset.apps/keycloak updated
# Create edge route for TLS termination at the router
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc create route edge keycloak \
--service=keycloak -n keycloak
route/keycloak created
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc get route keycloak -n keycloak \
-o jsonpath='{.spec.host}'
keycloak-keycloak.apps.brcox-mgmt.brcox.hypershift.devcluster.openshift.com
# Extract management cluster router CA
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc extract cm/default-ingress-cert \
-n openshift-config-managed --to=/tmp/router-ca --confirm
/tmp/router-ca/ca-bundle.crt
# Verify OIDC discovery endpoint is accessible
$ curl -sSI --cacert /tmp/router-ca/ca-bundle.crt \
"https://keycloak-keycloak.apps.brcox-mgmt.brcox.hypershift.devcluster.openshift.com\
/realms/master/.well-known/openid-configuration" | head -1
HTTP/1.1 200 OK
postStart lifecycle hook runs the setup script on pod start, which uses kcadm.sh
to configure the Keycloak realm.
# Clients configured:
# oc-cli-test (public client, for CLI direct access grants)
# console-test (confidential client, secret=paznw...f92, redirect to guest console /auth/callback)
#
# Group mapper: adds "groups" claim to ID tokens
# Test group: keycloak-testgroup-1
# Test user: keycloak-testuser-1 (email: keycloak-testuser-1@example.com, in keycloak-testgroup-1)
# Create ConfigMap with client definitions and setup script
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc create configmap setup-script -n keycloak \
--from-file=client-oc-cli-test.json --from-file=client-console-test.json \
--from-file=groupmapper-for-clients.json --from-literal=testusers="keycloak-testuser-1:************" \
--from-file=setup-script.sh
configmap/setup-script created
# Mount ConfigMap and add postStart lifecycle hook
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc set volumes sts/keycloak -n keycloak \
--add --type=configmap --configmap-name=setup-script --mount-path=/tmp/.keycloak --name=setup-script
statefulset.apps/keycloak volume updated
# Wait for pod rollout
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc get pods -n keycloak -L controller-revision-hash
NAME READY STATUS RESTARTS AGE CONTROLLER-REVISION-HASH
keycloak-0 1/1 Running 0 109s keycloak-6bf88bd768
# Verify postStart setup completed
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc rsh -n keycloak sts/keycloak cat /tmp/postStart.log
Waiting for Keycloak server to start...
Setting session timeout
Creating clients
Creating group mapper for clients
Creating group
Creating users
Checking group membership
Keycloak setup done!
# Create issuerCertificateAuthority ConfigMap with management cluster router CA
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc create configmap keycloak-oidc-ca \
--from-file=ca-bundle.crt=/tmp/router-ca/ca-bundle.crt -n clusters
configmap/keycloak-oidc-ca created
# Create console client secret
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc create secret generic console-secret \
--from-literal=clientSecret=paznw...f92 -n clusters
secret/console-secret created
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc patch hostedcluster brcox-sm-dev-hc \
-n clusters --type=merge -p '{
"spec": {
"configuration": {
"authentication": {
"type": "OIDC",
"oidcProviders": [{
"name": "keycloak-oidc-server",
"issuer": {
"issuerURL": "https://keycloak-keycloak.apps.brcox-mgmt.brcox.hypershift.\
devcluster.openshift.com/realms/master",
"audiences": ["console-test", "oc-cli-test"],
"issuerCertificateAuthority": {"name": "keycloak-oidc-ca"}
},
"claimMappings": {
"username": {"claim": "email", "prefixPolicy": "NoPrefix"},
"groups": {"claim": "groups", "prefix": "kc:"},
"uid": {"claim": "sub"}
},
"oidcClients": [
{"clientID": "oc-cli-test", "componentName": "cli",
"componentNamespace": "openshift-console"},
{"clientID": "console-test", "componentName": "console",
"componentNamespace": "openshift-console",
"clientSecret": {"name": "console-secret"}}
]
}]
}
}
}
}'
hostedcluster.hypershift.openshift.io/brcox-sm-dev-hc patched
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc get hostedcluster brcox-sm-dev-hc \
-n clusters -o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}\
{.message}{"\n"}{end}' | grep -E 'Available|Progressing'
ClusterVersionProgressing False Cluster version is 4.22.0-rc.3
ClusterVersionAvailable True Done applying 4.22.0-rc.3
EtcdAvailable True
KubeAPIServerAvailable True Kube APIServer deployment is available
Available True The hosted control plane is available
Progressing False HostedCluster is at expected version
$ KUBECONFIG=/Users/brcox/aws_dev_kubeconfig oc get cm auth-config \
-n clusters-brcox-sm-dev-hc -o jsonpath='{.data.auth\.json}' | jq .
{
"kind": "AuthenticationConfiguration",
"apiVersion": "apiserver.config.k8s.io/v1alpha1",
"jwt": [
{
"issuer": {
"url": "https://keycloak-keycloak.apps.brcox-mgmt.brcox.hypershift.devcluster\
.openshift.com/realms/master",
"certificateAuthority": "-----BEGIN CERTIFICATE-----\nMIID+zCCAu...m54R\n-----END CERTIFICATE-----\n",
"audiences": [
"console-test",
"oc-cli-test"
],
"audienceMatchPolicy": "MatchAny"
},
"claimMappings": {
"username": {
"claim": "email",
"prefix": ""
},
"groups": {
"claim": "groups",
"prefix": "kc:"
},
"uid": {
"claim": "sub"
}
}
}
]
}
issuerCertificateAuthority since the Keycloak route uses edge TLS termination with the router's certificateemail → username (NoPrefix), groups → groups (kc: prefix), sub → UID