--- # Configure OIDC authentication on SNO OpenShift using Keycloak as identity provider. # # This playbook creates a Keycloak client for OpenShift and configures the # cluster's OAuth resource to use Keycloak as an OpenID Connect identity provider. # # Prerequisites: # - SNO cluster is installed and accessible (see deploy_openshift.yml) # - Keycloak server is running and accessible # - oc binary available in the EE PATH (override with oc_binary if running outside EE) # - middleware_automation.keycloak collection installed (see collections/requirements.yml) # # Inventory requirements: # openshift group (e.g. sno.openshift.toal.ca) # host_vars: ocp_cluster_name, ocp_base_domain, sno_install_dir # secrets: vault_keycloak_admin_password # vault_oidc_client_secret (optional — auto-generated if omitted; save the output!) # # Key variables: # keycloak_url - Keycloak base URL (e.g. https://keycloak.toal.ca) # keycloak_realm - Keycloak realm name (e.g. toallab) # keycloak_admin_user - Keycloak admin username (default: admin) # keycloak_context - URL prefix for Keycloak API: # "" for Quarkus/modern Keycloak (default) # "/auth" for legacy JBoss/WildFly Keycloak # vault_keycloak_admin_password - Keycloak admin password (vaulted) # vault_oidc_client_secret - OIDC client secret (vaulted, optional — auto-generated if omitted) # oidc_provider_name - IdP name shown in OpenShift login (default: keycloak) # oidc_client_id - Client ID in Keycloak (default: openshift) # # Optional variables: # oidc_admin_groups - List of Keycloak groups to grant cluster-admin (default: []) # oidc_ca_cert_file - Local path to CA cert PEM for Keycloak TLS (private CA only) # oc_binary - Path to oc binary (default: oc from EE PATH) # oc_kubeconfig - Path to kubeconfig (default: sno_install_dir/auth/kubeconfig) # # Usage: # op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml # op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml --tags keycloak # op run --env-file=~/.ansible.zshenv -- ansible-navigator run playbooks/configure_sno_oidc.yml --tags openshift # --------------------------------------------------------------------------- # Play 1: Configure Keycloak OIDC client for OpenShift # --------------------------------------------------------------------------- - name: Configure Keycloak OIDC client for OpenShift hosts: openshift gather_facts: false connection: local tags: keycloak vars: # Set to "/auth" for legacy JBoss/WildFly-based Keycloak; leave empty for Quarkus (v17+) keycloak_context: "" oidc_provider_name: keycloak oidc_client_id: openshift oidc_redirect_uri: "https://oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}/oauth2callback/{{ oidc_provider_name }}" __oidc_keycloak_api_url: "{{ keycloak_url }}{{ keycloak_context }}" module_defaults: middleware_automation.keycloak.keycloak_realm: auth_client_id: admin-cli auth_keycloak_url: "{{ __oidc_keycloak_api_url }}" auth_realm: master auth_username: "{{ keycloak_admin_user }}" auth_password: "{{ vault_keycloak_admin_password }}" validate_certs: "{{ keycloak_validate_certs | default(true) }}" middleware_automation.keycloak.keycloak_client: auth_client_id: admin-cli auth_keycloak_url: "{{ __oidc_keycloak_api_url }}" auth_realm: master auth_username: "{{ keycloak_admin_user }}" auth_password: "{{ vault_keycloak_admin_password }}" validate_certs: "{{ keycloak_validate_certs | default(true) }}" tasks: # Generate a random 32-char alphanumeric secret if vault_oidc_client_secret is not supplied. # The generated value is stored in __oidc_client_secret and displayed after the play so it # can be vaulted and re-used on subsequent runs. - name: Set OIDC client secret (use vault value or generate random) ansible.builtin.set_fact: __oidc_client_secret: "{{ vault_oidc_client_secret | default(lookup('community.general.random_string', length=32, special=false)) }}" __oidc_secret_generated: "{{ vault_oidc_client_secret is not defined }}" no_log: true - name: Ensure Keycloak realm exists middleware_automation.keycloak.keycloak_realm: realm: "{{ keycloak_realm }}" id: "{{ keycloak_realm }}" display_name: "{{ keycloak_realm_display_name | default(keycloak_realm | title) }}" enabled: true state: present no_log: "{{ keycloak_no_log | default(true) }}" - name: Create OpenShift OIDC client in Keycloak middleware_automation.keycloak.keycloak_client: realm: "{{ keycloak_realm }}" client_id: "{{ oidc_client_id }}" name: "OpenShift - {{ ocp_cluster_name }}" description: "OIDC client for OpenShift cluster {{ ocp_cluster_name }}.{{ ocp_base_domain }}" enabled: true protocol: openid-connect public_client: false standard_flow_enabled: true implicit_flow_enabled: false direct_access_grants_enabled: false service_accounts_enabled: false secret: "{{ __oidc_client_secret }}" redirect_uris: - "{{ oidc_redirect_uri }}" web_origins: - "+" protocol_mappers: - name: groups protocol: openid-connect protocolMapper: oidc-group-membership-mapper config: full.path: "false" id.token.claim: "true" access.token.claim: "true" userinfo.token.claim: "true" claim.name: groups state: present no_log: "{{ keycloak_no_log | default(true) }}" - name: Display generated client secret (save this to vault!) ansible.builtin.debug: msg: - "*** GENERATED OIDC CLIENT SECRET — SAVE THIS TO VAULT ***" - "vault_oidc_client_secret: {{ __oidc_client_secret }}" - "" - "Set this in host_vars or pass as --extra-vars on future runs." when: __oidc_secret_generated | bool - name: Display Keycloak configuration summary ansible.builtin.debug: msg: - "Keycloak OIDC client configured:" - " Realm : {{ keycloak_realm }}" - " Client : {{ oidc_client_id }}" - " Issuer : {{ keycloak_url }}{{ keycloak_context }}/realms/{{ keycloak_realm }}" - " Redirect: {{ oidc_redirect_uri }}" verbosity: 1 # --------------------------------------------------------------------------- # Play 2: Configure OpenShift OAuth with Keycloak OIDC # --------------------------------------------------------------------------- - name: Configure OpenShift OAuth with Keycloak OIDC hosts: sno.openshift.toal.ca gather_facts: false connection: local tags: openshift vars: # Set to "/auth" for legacy JBoss/WildFly-based Keycloak; leave empty for Quarkus (v17+) keycloak_context: "" oidc_provider_name: keycloak oidc_client_id: openshift oidc_admin_groups: [] __oidc_secret_name: keycloak-oidc-client-secret __oidc_ca_configmap_name: keycloak-oidc-ca-bundle __oidc_oc: "{{ oc_binary | default('oc') }}" # Prefer the fact set by Play 1; fall back to vault var when running --tags openshift alone __oidc_client_secret_value: "{{ hostvars[inventory_hostname]['__oidc_client_secret'] | default(vault_oidc_client_secret) }}" tasks: - name: Create temp directory for manifests ansible.builtin.tempfile: state: directory suffix: .oidc register: __oidc_tmpdir # ------------------------------------------------------------------ # Secret: Keycloak client secret in openshift-config namespace # ------------------------------------------------------------------ - name: Write Keycloak client secret manifest ansible.builtin.copy: dest: "{{ __oidc_tmpdir.path }}/keycloak-secret.yaml" mode: "0600" content: | apiVersion: v1 kind: Secret metadata: name: {{ __oidc_secret_name }} namespace: openshift-config type: Opaque stringData: clientSecret: {{ __oidc_client_secret_value }} no_log: true - name: Apply Keycloak client secret ansible.builtin.command: cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/keycloak-secret.yaml" register: __oidc_secret_apply changed_when: "'configured' in __oidc_secret_apply.stdout or 'created' in __oidc_secret_apply.stdout" # ------------------------------------------------------------------ # CA bundle: only needed when Keycloak uses a private/internal CA # ------------------------------------------------------------------ - name: Configure CA bundle ConfigMap for Keycloak TLS when: oidc_ca_cert_file is defined block: - name: Write CA bundle ConfigMap manifest ansible.builtin.copy: dest: "{{ __oidc_tmpdir.path }}/keycloak-ca.yaml" mode: "0644" content: | apiVersion: v1 kind: ConfigMap metadata: name: {{ __oidc_ca_configmap_name }} namespace: openshift-config data: ca.crt: | {{ lookup('ansible.builtin.file', oidc_ca_cert_file) | indent(4) }} - name: Apply CA bundle ConfigMap ansible.builtin.command: cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/keycloak-ca.yaml" register: __oidc_ca_apply changed_when: "'configured' in __oidc_ca_apply.stdout or 'created' in __oidc_ca_apply.stdout" # ------------------------------------------------------------------ # OAuth cluster resource: add/replace Keycloak IdP entry # Reads the current config, replaces any existing entry with the same # name, and applies the merged result — preserving other IdPs. # ------------------------------------------------------------------ - name: Get current OAuth cluster configuration ansible.builtin.command: cmd: "{{ __oidc_oc }} get oauth cluster -o json" register: __oidc_current_oauth changed_when: false - name: Parse current OAuth configuration ansible.builtin.set_fact: __oidc__oidc_current_oauth_obj: "{{ __oidc_current_oauth.stdout | from_json }}" - name: Build Keycloak OIDC identity provider definition ansible.builtin.set_fact: __oidc_new_idp: >- {{ { 'name': oidc_provider_name, 'mappingMethod': 'claim', 'type': 'OpenID', 'openID': ( { 'clientID': oidc_client_id, 'clientSecret': {'name': __oidc_secret_name}, 'issuer': keycloak_url ~ keycloak_context ~ '/realms/' ~ keycloak_realm, 'claims': { 'preferredUsername': ['preferred_username'], 'name': ['name'], 'email': ['email'], 'groups': ['groups'] } } | combine( oidc_ca_cert_file is defined | ternary( {'ca': {'name': __oidc_ca_configmap_name}}, {} ) ) ) } }} - name: Build updated identity providers list ansible.builtin.set_fact: __oidc_updated_idps: >- {{ (__oidc__oidc_current_oauth_obj.spec.identityProviders | default([]) | selectattr('name', '!=', oidc_provider_name) | list) + [__oidc_new_idp] }} - name: Write updated OAuth cluster manifest ansible.builtin.copy: dest: "{{ __oidc_tmpdir.path }}/oauth-cluster.yaml" mode: "0644" content: "{{ __oidc__oidc_current_oauth_obj | combine({'spec': {'identityProviders': __oidc_updated_idps}}, recursive=true) | to_nice_yaml }}" - name: Apply updated OAuth cluster configuration ansible.builtin.command: cmd: "{{ __oidc_oc }} apply -f {{ __oidc_tmpdir.path }}/oauth-cluster.yaml" register: __oidc_oauth_apply changed_when: "'configured' in __oidc_oauth_apply.stdout or 'created' in __oidc_oauth_apply.stdout" - name: Wait for OAuth deployment to roll out ansible.builtin.command: cmd: "{{ __oidc_oc }} rollout status deployment/oauth-openshift -n openshift-authentication --timeout=300s" changed_when: false # ------------------------------------------------------------------ # Optional: grant cluster-admin to specified Keycloak groups # ------------------------------------------------------------------ - name: Grant cluster-admin to OIDC admin groups ansible.builtin.command: cmd: "{{ __oidc_oc }} adm policy add-cluster-role-to-group cluster-admin {{ item }}" loop: "{{ oidc_admin_groups }}" changed_when: true when: oidc_admin_groups | length > 0 - name: Remove temp directory ansible.builtin.file: path: "{{ __oidc_tmpdir.path }}" state: absent - name: Display post-configuration summary ansible.builtin.debug: msg: - "OpenShift OIDC configuration complete!" - " Provider : {{ oidc_provider_name }}" - " Issuer : {{ keycloak_url }}{{ keycloak_context }}/realms/{{ keycloak_realm }}" - " Console : https://console-openshift-console.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}" - " Login : https://oauth-openshift.apps.{{ ocp_cluster_name }}.{{ ocp_base_domain }}" - "" - "Note: OAuth pods are restarting — login may be unavailable for ~2 minutes." verbosity: 1