Scenario 8: Keycloak Setup & Cluster Reconfiguration

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.

Overall Scenario 8: PASS — All 8 checks verified.
StepCheckResultEvidence
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

Commands & Outputs

Step 1: Deploy Keycloak on management cluster

# 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

Step 2: Create edge route and verify OIDC endpoint

# 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

Step 3: Configure Keycloak clients, users, and groups

Configuration method: Client definitions, group mapper, and a setup script are mounted into the Keycloak pod via a ConfigMap. A 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!

Step 4 & 5: Create CA ConfigMap and console secret in HC namespace

# 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

Step 6: Patch HostedCluster to use Keycloak OIDC provider

$ 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

Step 7: Verify HostedCluster remains Available

$ 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

Step 8: Verify KAS auth-config updated with Keycloak JWT authenticator

$ 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"
        }
      }
    }
  ]
}
Key observations:
← Scenario 7 Scenario 9 →