From f3fbe837081546c5bfa194f1e8c89f5769b96d5a Mon Sep 17 00:00:00 2001 From: Pedro Henrique Date: Mon, 21 Jan 2019 12:25:39 +0100 Subject: [PATCH] Add support to OpenID Connect Authentication flow This pull request adds support for the OpenID Connect authentication flow in Keystone and enables both ID and access token authentication flows. The ID token configuration is designed to allow users to authenticate via Horizon using an identity federation; whereas the Access token is used to allow users to authenticate in the OpenStack CLI using a federated user. Without this PR, if one wants to configure OpenStack to use identity federation, he/she needs to do a lot of configurations in the keystone, Horizon, and register quite a good number of different parameters using the CLI such as mappings, identity providers, federated protocols, and so on. Therefore, with this PR, we propose a method for operators to introduce/present the IdP's metadata to Kolla-ansible, and based on the presented metadata, Kolla-ansible takes care of all of the configurations to prepare OpenStack to work in a federated environment. Implements: blueprint add-openid-support Co-Authored-By: Jason Anderson Change-Id: I0203a3470d7f8f2a54d5e126d947f540d93b8210 --- ansible/group_vars/all.yml | 44 ++++ .../roles/haproxy-config/defaults/main.yml | 2 + ansible/roles/horizon/defaults/main.yml | 8 +- .../roles/horizon/templates/local_settings.j2 | 27 +- ansible/roles/keystone/defaults/main.yml | 22 ++ .../keystone/tasks/config-federation-oidc.yml | 86 +++++++ ansible/roles/keystone/tasks/config.yml | 4 + ansible/roles/keystone/tasks/deploy.yml | 4 + .../tasks/register_identity_providers.yml | 238 ++++++++++++++++++ .../roles/keystone/templates/keystone.conf.j2 | 15 ++ .../roles/keystone/templates/keystone.json.j2 | 27 ++ .../keystone/templates/wsgi-keystone.conf.j2 | 45 ++++ doc/source/contributor/index.rst | 1 + .../contributor/setup-identity-provider.rst | 193 ++++++++++++++ .../shared-services/keystone-guide.rst | 238 ++++++++++++++++++ etc/kolla/passwords.yml | 5 + ...rt-to-openid-connect-859b12492f8347fe.yaml | 5 + 17 files changed, 951 insertions(+), 13 deletions(-) create mode 100644 ansible/roles/keystone/tasks/config-federation-oidc.yml create mode 100644 ansible/roles/keystone/tasks/register_identity_providers.yml create mode 100644 doc/source/contributor/setup-identity-provider.rst create mode 100644 releasenotes/notes/add-keystone-support-to-openid-connect-859b12492f8347fe.yaml diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 3dca393799..3787a97124 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -557,6 +557,7 @@ enable_glance: "{{ enable_openstack_core | bool }}" enable_haproxy: "yes" enable_keepalived: "{{ enable_haproxy | bool }}" enable_keystone: "{{ enable_openstack_core | bool }}" +enable_keystone_federation: "{{ (keystone_identity_providers | length > 0) and (keystone_identity_mappings | length > 0) }}" enable_mariadb: "yes" enable_memcached: "yes" enable_neutron: "{{ enable_openstack_core | bool }}" @@ -1011,6 +1012,7 @@ enable_neutron_horizon_policy_file: "{{ enable_neutron }}" enable_nova_horizon_policy_file: "{{ enable_nova }}" horizon_internal_endpoint: "{{ internal_protocol }}://{{ kolla_internal_fqdn | put_address_in_context('url') }}:{{ horizon_tls_port if kolla_enable_tls_internal | bool else horizon_port }}" +horizon_public_endpoint: "{{ public_protocol }}://{{ kolla_external_fqdn | put_address_in_context('url') }}:{{ horizon_tls_port if kolla_enable_tls_external | bool else horizon_port }}" ################### # External Ceph options @@ -1153,3 +1155,45 @@ swift_public_endpoint: "{{ public_protocol }}://{{ swift_external_fqdn | put_add octavia_admin_endpoint: "{{ admin_protocol }}://{{ octavia_internal_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}" octavia_internal_endpoint: "{{ internal_protocol }}://{{ octavia_internal_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}" octavia_public_endpoint: "{{ public_protocol }}://{{ octavia_external_fqdn | put_address_in_context('url') }}:{{ octavia_api_port }}" + +################################### +# Identity federation configuration +################################### +# Here we configure all of the IdPs meta informations that will be required to implement identity federation with OpenStack Keystone. +# We require the administrator to enter the following metadata: +# * name (internal name of the IdP in Keystone); +# * openstack_domain (the domain in Keystone that the IdP belongs to) +# * protocol (the federated protocol used by the IdP; e.g. openid or saml); +# * identifier (the IdP identifier; e.g. https://accounts.google.com); +# * public_name (the public name that will be shown for users in Horizon); +# * attribute_mapping (the attribute mapping to be used for this IdP. This mapping is configured in the "keystone_identity_mappings" configuration); +# * metadata_folder (folder containing all the identity provider metadata as jsons named as the identifier without the protocol +# and with '/' escaped as %2F followed with '.provider' or '.client' or '.conf'; e.g. accounts.google.com.provider; PS, all .conf, +# .provider and .client jsons must be in the folder, even if you dont override any conf in the .conf json, you must leave it as an empty json '{}'); +# * certificate_file (the path to the Identity Provider certificate file, the file must be named as 'certificate-key-id.pem'; +# e.g. LRVweuT51StjMdsna59jKfB3xw0r8Iz1d1J1HeAbmlw.pem; You can find the key-id in the Identity provider '.well-known/openid-configuration' jwks_uri as kid); +# +# The IdPs meta information are to be presented to Kolla-Ansible as the following example: +# keystone_identity_providers: +# - name: "myidp1" +# openstack_domain: "my-domain" +# protocol: "openid" +# identifier: "https://accounts.google.com" +# public_name: "Authenticate via myidp1" +# attribute_mapping: "mappingId1" +# metadata_folder: "path/to/metadata/folder" +# certificate_file: "path/to/certificate/file.pem" +# +# We also need to configure the attribute mapping that is used by IdPs. +# The configuration of attribute mappings is a list of objects, where each +# object must have a 'name' (that mapps to the 'attribute_mapping' to the IdP +# object in the IdPs set), and the 'file' with a full qualified path to a mapping file. +# keystone_identity_mappings: +# - name: "mappingId1" +# file: "/full/qualified/path/to/mapping/json/file/to/mappingId1" +# - name: "mappingId2" +# file: "/full/qualified/path/to/mapping/json/file/to/mappingId2" +# - name: "mappingId3" +# file: "/full/qualified/path/to/mapping/json/file/to/mappingId3" +keystone_identity_providers: [] +keystone_identity_mappings: [] diff --git a/ansible/roles/haproxy-config/defaults/main.yml b/ansible/roles/haproxy-config/defaults/main.yml index 4cfc7b7862..0de7601479 100644 --- a/ansible/roles/haproxy-config/defaults/main.yml +++ b/ansible/roles/haproxy-config/defaults/main.yml @@ -15,3 +15,5 @@ haproxy_backend_tcp_extra: [] haproxy_health_check: "check inter 2000 rise 2 fall 5" haproxy_health_check_ssl: "check check-ssl inter 2000 rise 2 fall 5" + +haproxy_enable_federation_openid: "{{ keystone_identity_providers | selectattr('protocol','equalto','openid') | list | count > 0 }}" diff --git a/ansible/roles/horizon/defaults/main.yml b/ansible/roles/horizon/defaults/main.yml index d0a153da05..5594f6e508 100644 --- a/ansible/roles/horizon/defaults/main.yml +++ b/ansible/roles/horizon/defaults/main.yml @@ -123,7 +123,7 @@ horizon_extra_volumes: "{{ default_extra_volumes }}" # OpenStack #################### horizon_logging_debug: "{{ openstack_logging_debug }}" -horizon_keystone_url: "{{ keystone_internal_url }}/v3" +horizon_keystone_url: "{{ keystone_public_url if horizon_use_keystone_public_url | bool else keystone_internal_url }}/v3" #################### @@ -149,3 +149,9 @@ horizon_murano_source_version: "{{ kolla_source_version }}" # TLS #################### horizon_enable_tls_backend: "{{ kolla_enable_tls_backend }}" + +# This variable was created for administrators to define which one of the Keystone's URLs should be configured in Horizon. +# In some cases, such as when using OIDC, horizon will need to be configured with Keystone's public URL. +# Therefore, instead of overriding the whole "horizon_keystone_url", this change allows an easier integration because +# the Keystone public URL is already defined with variable "keystone_public_url". +horizon_use_keystone_public_url: False diff --git a/ansible/roles/horizon/templates/local_settings.j2 b/ansible/roles/horizon/templates/local_settings.j2 index 136741b8cf..ecaba31d2b 100644 --- a/ansible/roles/horizon/templates/local_settings.j2 +++ b/ansible/roles/horizon/templates/local_settings.j2 @@ -209,8 +209,9 @@ OPENSTACK_HOST = "{{ kolla_internal_fqdn }}" OPENSTACK_KEYSTONE_URL = "{{ horizon_keystone_url }}" OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}" +{% if enable_keystone_federation | bool %} # Enables keystone web single-sign-on if set to True. -#WEBSSO_ENABLED = False +WEBSSO_ENABLED = True # Determines which authentication choice to show as default. #WEBSSO_INITIAL_CHOICE = "credentials" @@ -223,13 +224,13 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}" # Do not remove the mandatory credentials mechanism. # Note: The last two tuples are sample mapping keys to a identity provider # and federation protocol combination (WEBSSO_IDP_MAPPING). -#WEBSSO_CHOICES = ( -# ("credentials", _("Keystone Credentials")), -# ("oidc", _("OpenID Connect")), -# ("saml2", _("Security Assertion Markup Language")), -# ("acme_oidc", "ACME - OpenID Connect"), -# ("acme_saml2", "ACME - SAML2"), -#) +WEBSSO_KEYSTONE_URL = "{{ keystone_public_url }}/v3" +WEBSSO_CHOICES = ( + ("credentials", _("Keystone Credentials")), + {% for idp in keystone_identity_providers %} + ("{{ idp.name }}_{{ idp.protocol }}", "{{ idp.public_name }}"), + {% endfor %} +) # A dictionary of specific identity provider and federation protocol # combinations. From the selected authentication mechanism, the value @@ -238,10 +239,12 @@ OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ keystone_default_user_role }}" # specific WebSSO endpoint in keystone, otherwise it will use the value # as the protocol_id when redirecting to the WebSSO by protocol endpoint. # NOTE: The value is expected to be a tuple formatted as: (, ). -#WEBSSO_IDP_MAPPING = { -# "acme_oidc": ("acme", "oidc"), -# "acme_saml2": ("acme", "saml2"), -#} +WEBSSO_IDP_MAPPING = { +{% for idp in keystone_identity_providers %} + "{{ idp.name }}_{{ idp.protocol }}": ("{{ idp.name }}", "{{ idp.protocol }}"), +{% endfor %} +} +{% endif %} # Disable SSL certificate checks (useful for self-signed certificates): #OPENSTACK_SSL_NO_VERIFY = True diff --git a/ansible/roles/keystone/defaults/main.yml b/ansible/roles/keystone/defaults/main.yml index a8267f6d25..1c75f5790f 100644 --- a/ansible/roles/keystone/defaults/main.yml +++ b/ansible/roles/keystone/defaults/main.yml @@ -18,6 +18,7 @@ keystone_services: tls_backend: "{{ keystone_enable_tls_backend }}" port: "{{ keystone_public_port }}" listen_port: "{{ keystone_public_listen_port }}" + backend_http_extra: "{{ ['balance source'] if enable_keystone_federation | bool else [] }}" keystone_external: enabled: "{{ enable_keystone }}" mode: "http" @@ -25,6 +26,7 @@ keystone_services: tls_backend: "{{ keystone_enable_tls_backend }}" port: "{{ keystone_public_port }}" listen_port: "{{ keystone_public_listen_port }}" + backend_http_extra: "{{ ['balance source'] if enable_keystone_federation | bool else [] }}" keystone_admin: enabled: "{{ enable_keystone }}" mode: "http" @@ -179,3 +181,23 @@ keystone_ks_services: # TLS #################### keystone_enable_tls_backend: "{{ kolla_enable_tls_backend }}" + +############################### +# OpenStack identity federation +############################### +# Default OpenID Connect remote attribute key +keystone_remote_id_attribute_oidc: "HTTP_OIDC_ISS" +keystone_container_federation_oidc_metadata_folder: "{{ '/etc/apache2/metadata' if kolla_base_distro in ['debian', 'ubuntu'] else '/etc/httpd/metadata' }}" +keystone_container_federation_oidc_idp_certificate_folder: "{{ '/etc/apache2/cert' if kolla_base_distro in ['debian', 'ubuntu'] else '/etc/httpd/cert' }}" +keystone_container_federation_oidc_attribute_mappings_folder: "{{ container_config_directory }}/federation/oidc/attribute_maps" +keystone_host_federation_oidc_metadata_folder: "{{ node_config_directory }}/keystone/federation/oidc/metadata" +keystone_host_federation_oidc_idp_certificate_folder: "{{ node_config_directory }}/keystone/federation/oidc/cert" +keystone_host_federation_oidc_attribute_mappings_folder: "{{ node_config_directory }}/keystone/federation/oidc/attribute_maps" + +# These variables are used to define multiple trusted Horizon dashboards. +# keystone_trusted_dashboards: ['', '', ''] +keystone_trusted_dashboards: "{{ ['%s://%s/auth/websso/' % (public_protocol, kolla_external_fqdn), '%s/auth/websso/' % (horizon_public_endpoint)] if enable_horizon | bool else [] }}" +keystone_enable_federation_openid: "{{ enable_keystone_federation | bool and keystone_identity_providers | selectattr('protocol','equalto','openid') | list | count > 0 }}" +keystone_should_remove_attribute_mappings: False +keystone_should_remove_identity_providers: False +keystone_federation_oidc_scopes: "openid email profile" diff --git a/ansible/roles/keystone/tasks/config-federation-oidc.yml b/ansible/roles/keystone/tasks/config-federation-oidc.yml new file mode 100644 index 0000000000..4171283273 --- /dev/null +++ b/ansible/roles/keystone/tasks/config-federation-oidc.yml @@ -0,0 +1,86 @@ +--- +- name: Remove OpenID certificate and metadata files + become: true + vars: + keystone: "{{ keystone_services['keystone'] }}" + file: + state: absent + path: "{{ item }}" + when: + - inventory_hostname in groups[keystone.group] + with_items: + - "{{ keystone_host_federation_oidc_metadata_folder }}" + - "{{ keystone_host_federation_oidc_idp_certificate_folder }}" + - "{{ keystone_host_federation_oidc_attribute_mappings_folder }}" + +- name: Create OpenID configuration directories + vars: + keystone: "{{ keystone_services['keystone'] }}" + file: + dest: "{{ item }}" + state: "directory" + mode: "0770" + become: true + with_items: + - "{{ keystone_host_federation_oidc_metadata_folder }}" + - "{{ keystone_host_federation_oidc_idp_certificate_folder }}" + - "{{ keystone_host_federation_oidc_attribute_mappings_folder }}" + when: + - inventory_hostname in groups[keystone.group] + +- name: Copying OpenID Identity Providers metadata + vars: + keystone: "{{ keystone_services['keystone'] }}" + become: true + copy: + src: "{{ item.metadata_folder }}/" + dest: "{{ keystone_host_federation_oidc_metadata_folder }}" + mode: "0660" + with_items: "{{ keystone_identity_providers }}" + when: + - item.protocol == 'openid' + - inventory_hostname in groups[keystone.group] + +- name: Copying OpenID Identity Providers certificate + vars: + keystone: "{{ keystone_services['keystone'] }}" + become: true + copy: + src: "{{ item.certificate_file }}" + dest: "{{ keystone_host_federation_oidc_idp_certificate_folder }}" + mode: "0660" + with_items: "{{ keystone_identity_providers }}" + when: + - item.protocol == 'openid' + - inventory_hostname in groups[keystone.group] + +- name: Copying OpenStack Identity Providers attribute mappings + vars: + keystone: "{{ keystone_services['keystone'] }}" + become: true + copy: + src: "{{ item.file }}" + dest: "{{ keystone_host_federation_oidc_attribute_mappings_folder }}/{{ item.file | basename }}" + mode: "0660" + with_items: "{{ keystone_identity_mappings }}" + when: + - inventory_hostname in groups[keystone.group] + +- name: Setting the certificates files variable + become: true + vars: + keystone: "{{ keystone_services['keystone'] }}" + find: + path: "{{ keystone_host_federation_oidc_idp_certificate_folder }}" + pattern: "*.pem" + register: certificates_path + when: + - inventory_hostname in groups[keystone.group] + +- name: Setting the certificates variable + vars: + keystone: "{{ keystone_services['keystone'] }}" + set_fact: + keystone_federation_openid_certificate_key_ids: "{{ certificates_path.files | map(attribute='path') | map('regex_replace', '^.*/(.*)\\.pem$', '\\1#' + keystone_container_federation_oidc_idp_certificate_folder + '/\\1.pem') | list }}" # noqa 204 + when: + - inventory_hostname in groups[keystone.group] diff --git a/ansible/roles/keystone/tasks/config.yml b/ansible/roles/keystone/tasks/config.yml index 06ecea3a7c..bec1350a34 100644 --- a/ansible/roles/keystone/tasks/config.yml +++ b/ansible/roles/keystone/tasks/config.yml @@ -144,6 +144,10 @@ notify: - Restart {{ item.key }} container +- include_tasks: config-federation-oidc.yml + when: + - keystone_enable_federation_openid | bool + - name: Copying over wsgi-keystone.conf vars: keystone: "{{ keystone_services.keystone }}" diff --git a/ansible/roles/keystone/tasks/deploy.yml b/ansible/roles/keystone/tasks/deploy.yml index 656e44e312..a6ff99b0e9 100644 --- a/ansible/roles/keystone/tasks/deploy.yml +++ b/ansible/roles/keystone/tasks/deploy.yml @@ -19,3 +19,7 @@ - import_tasks: register.yml - import_tasks: check.yml + +- include_tasks: register_identity_providers.yml + when: + - enable_keystone_federation | bool diff --git a/ansible/roles/keystone/tasks/register_identity_providers.yml b/ansible/roles/keystone/tasks/register_identity_providers.yml new file mode 100644 index 0000000000..befcf41d3f --- /dev/null +++ b/ansible/roles/keystone/tasks/register_identity_providers.yml @@ -0,0 +1,238 @@ +--- +- name: List configured attribute mappings (that can be used by IdPs) + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + mapping list -c ID --format value + run_once: True + become: True + register: existing_mappings_register + +- name: Register existing mappings + set_fact: + existing_mappings: "{{ existing_mappings_register.stdout_lines | map('trim') | list }}" + +- name: Remove unmanaged attribute mappings + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + mapping delete {{ item }} + run_once: True + become: true + with_items: "{{ existing_mappings }}" + when: + - item not in (keystone_identity_mappings | map(attribute='name') | list) + - keystone_should_remove_attribute_mappings + +- name: Create unexisting domains + become: true + kolla_toolbox: + module_name: "os_keystone_domain" + module_args: + name: "{{ item.openstack_domain }}" + auth: "{{ openstack_auth }}" + endpoint_type: "{{ openstack_interface }}" + cacert: "{{ openstack_cacert }}" + region_name: "{{ openstack_region_name }}" + run_once: True + with_items: "{{ keystone_identity_providers }}" + +- name: Register attribute mappings in OpenStack + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + mapping create + --rules "{{ keystone_container_federation_oidc_attribute_mappings_folder }}/{{ item.file | basename }}" + {{ item.name }} + run_once: True + when: + - item.name not in existing_mappings + with_items: "{{ keystone_identity_mappings }}" + +- name: Update existing attribute mappings in OpenStack + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + mapping set + --rules "{{ keystone_container_federation_oidc_attribute_mappings_folder }}/{{ item.file | basename }}" + {{ item.name }} + run_once: True + when: + - item.name in existing_mappings + with_items: "{{ keystone_identity_mappings }}" + +- name: List configured IdPs + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + identity provider list -c ID --format value + run_once: True + register: existing_idps_register + +- name: Register existing idps + set_fact: + existing_idps: "{{ existing_idps_register.stdout.split('\n') | map('trim') | list }}" + +- name: Remove unmanaged identity providers + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + identity provider delete {{ item }} + run_once: True + with_items: "{{ existing_idps }}" + when: + - item not in (keystone_identity_providers | map(attribute='name') | list) + - keystone_should_remove_identity_providers + +- name: Register Identity Providers in OpenStack + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + identity provider create + --description "{{ item.public_name }}" + --remote-id "{{ item.identifier }}" + --domain "{{ item.openstack_domain }}" + {{ item.name }} + run_once: True + when: + - item.name not in existing_idps + with_items: "{{ keystone_identity_providers }}" + +- name: Update Identity Providers in OpenStack according to Kolla-Ansible configuraitons + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + identity provider set + --description "{{ item.public_name }}" + --remote-id "{{ item.identifier }}" + "{{ item.name }}" + run_once: True + when: + - item.name in existing_idps + with_items: "{{ keystone_identity_providers }}" + +- name: Configure attribute mappings for each Identity Provider. (We expect the mappings to be configured by the operator) + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + federation protocol create + --mapping {{ item.attribute_mapping }} + --identity-provider {{ item.name }} + {{ item.protocol }} + run_once: True + when: + - item.name not in existing_idps + with_items: "{{ keystone_identity_providers }}" + +- name: Update attribute mappings for each Identity Provider. (We expect the mappings to be configured by the operator). + become: true + command: > + docker exec -t keystone openstack + --os-auth-url={{ openstack_auth.auth_url }} + --os-password={{ openstack_auth.password }} + --os-username={{ openstack_auth.username }} + --os-project-name={{ openstack_auth.project_name }} + --os-identity-api-version=3 + --os-interface {{ openstack_interface }} + --os-project-domain-name {{ openstack_auth.domain_name }} + --os-user-domain-name {{ openstack_auth.domain_name }} + --os-region-name {{ openstack_region_name }} + {% if openstack_cacert != '' %}--os-cacert {{ openstack_cacert }} {% endif %} + federation protocol set + --identity-provider {{ item.name }} + --mapping {{ item.attribute_mapping }} + {{ item.protocol }} + run_once: True + register: result + failed_when: result.rc not in [0, 1] # This command returns RC 1 on success, so we need to add this to avoid fails. + when: + - item.name in existing_idps + with_items: "{{ keystone_identity_providers }}" diff --git a/ansible/roles/keystone/templates/keystone.conf.j2 b/ansible/roles/keystone/templates/keystone.conf.j2 index 730107eaca..f1e787b6f5 100644 --- a/ansible/roles/keystone/templates/keystone.conf.j2 +++ b/ansible/roles/keystone/templates/keystone.conf.j2 @@ -77,3 +77,18 @@ connection_string = {{ osprofiler_backend_connection_string }} [cors] allowed_origin = {{ grafana_public_endpoint }} {% endif %} + +{% if enable_keystone_federation %} +[federation] +{% for dashboard in keystone_trusted_dashboards %} +trusted_dashboard = {{ dashboard }} +{% endfor %} + +sso_callback_template = /etc/keystone/sso_callback_template.html + +[openid] +remote_id_attribute = {{ keystone_remote_id_attribute_oidc }} + +[auth] +methods = password,token,openid,application_credential +{% endif %} diff --git a/ansible/roles/keystone/templates/keystone.json.j2 b/ansible/roles/keystone/templates/keystone.json.j2 index e5c676190b..2dee915eb7 100644 --- a/ansible/roles/keystone/templates/keystone.json.j2 +++ b/ansible/roles/keystone/templates/keystone.json.j2 @@ -1,4 +1,5 @@ {% set keystone_dir = 'apache2/conf-enabled' if kolla_base_distro in ['ubuntu', 'debian'] else 'httpd/conf.d' %} +{% set apache_user = 'www-data' if kolla_base_distro in ['ubuntu', 'debian'] else 'apache' %} { "command": "/usr/bin/keystone-startup.sh", "config_files": [ @@ -52,6 +53,22 @@ "owner": "keystone", "perm": "0600" }{% endif %} + {% if keystone_enable_federation_openid %}, + { + "source": "{{ container_config_directory }}/federation/oidc/metadata", + "dest": "{{ keystone_container_federation_oidc_metadata_folder }}", + "owner": "{{ apache_user }}:{{ apache_user }}", + "perm": "0600", + "merge": true + }, + { + "source": "{{ container_config_directory }}/federation/oidc/cert", + "dest": "{{ keystone_container_federation_oidc_idp_certificate_folder }}", + "owner": "{{ apache_user }}:{{ apache_user }}", + "perm": "0600", + "merge": true + } + {% endif %} ], "permissions": [ { @@ -61,7 +78,17 @@ { "path": "/var/log/kolla/keystone/keystone.log", "owner": "keystone:keystone" + },{% if keystone_enable_federation_openid %} + { + "path": "{{ keystone_container_federation_oidc_metadata_folder }}", + "owner": "{{ apache_user }}:{{ apache_user }}", + "perm": "0700" }, + { + "path": "{{ keystone_container_federation_oidc_idp_certificate_folder }}", + "owner": "{{ apache_user }}:{{ apache_user }}", + "perm": "0700" + },{% endif %} { "path": "/etc/keystone/fernet-keys", "owner": "keystone:keystone", diff --git a/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 b/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 index 1d62274659..83886415b0 100644 --- a/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 +++ b/ansible/roles/keystone/templates/wsgi-keystone.conf.j2 @@ -51,6 +51,51 @@ LogLevel info SSLCertificateFile /etc/keystone/certs/keystone-cert.pem SSLCertificateKeyFile /etc/keystone/certs/keystone-key.pem {% endif %} + +{% if keystone_enable_federation_openid %} + OIDCClaimPrefix "OIDC-" + OIDCClaimDelimiter ";" + OIDCResponseType "id_token" + OIDCScope "{{ keystone_federation_oidc_scopes }}" + OIDCMetadataDir {{ keystone_container_federation_oidc_metadata_folder }} +{% if keystone_federation_openid_certificate_key_ids | length > 0 %} + OIDCOAuthVerifyCertFiles {{ keystone_federation_openid_certificate_key_ids | join(" ") }} +{% endif %} + OIDCCryptoPassphrase {{ keystone_federation_openid_crypto_password }} + OIDCRedirectURI {{ keystone_public_url }}/redirect_uri + + + Require valid-user + AuthType openid-connect + + + {# WebSSO authentication endpoint -#} + + Require valid-user + AuthType openid-connect + + +{% for idp in keystone_identity_providers %} +{% if idp.protocol == 'openid' %} + + Require valid-user + AuthType openid-connect + +{% endif %} +{% endfor %} + + {# CLI / API authentication endpoint -#} +{% for idp in keystone_identity_providers %} +{% if idp.protocol == 'openid' %} + + Require valid-user + {# Note(jasonanderson): `auth-openidc` is a special auth type that can -#} + {# additionally handle verifying bearer tokens -#} + AuthType auth-openidc + +{% endif %} +{% endfor %} +{% endif %} diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 9681dc966a..fb61fccfd6 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -21,3 +21,4 @@ We welcome everyone to join our project! bug-triage ptl-guide release-management + setup-identity-provider diff --git a/doc/source/contributor/setup-identity-provider.rst b/doc/source/contributor/setup-identity-provider.rst new file mode 100644 index 0000000000..99e5ae7807 --- /dev/null +++ b/doc/source/contributor/setup-identity-provider.rst @@ -0,0 +1,193 @@ +.. _setup-identity-provider: + +============================ +Test Identity Provider setup +============================ + +This guide shows how to create an Identity Provider that handles the OpenID +Connect protocol to authenticate users when +:keystone-doc:`using Federation with OpenStack +` (these configurations must not +be used in a production environment). + +Keycloak +======== + +Keycloak is a Java application that implements an Identity Provider handling +both OpenID Connect and SAML protocols. + +To setup a Keycloak instance for testing is pretty simple with Docker. + +Creating the Docker Keycloak instance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Run the docker command: + +.. code-block:: console + + docker run -p 8080:8080 -p 8443:8443 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin quay.io/keycloak/keycloak:latest + +This will create a Keycloak instance that has the admin credentials as +admin/admin and is listening on port 8080. + +After creating the instance, you will need to log in to the Keycloak as +administrator and setup the first Identity Provider. + +Creating an Identity Provider with Keycloak +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following guide assumes that the steps are executed from the same machine +(localhost), but you can change the hostname if you want to run it from +elsewhere. + +In this guide, we will use the 'new_realm' as the realm name in Keycloak, so, +if you want to use any other realm name, you must to change 'new_realm' in the +URIs used in the guide and replace the 'new_realm' with the realm name that you +are using. + +- Access the admin console on http://localhost:8080/auth/ in the Administration Console option. +- Authenticate using the credentials defined in the creation step. +- Create a new realm in the http://localhost:8080/auth/admin/master/console/#/create/realm page. +- After creating a realm, you will need to create a client to be used by Keystone; to do it, just access http://localhost:8080/auth/admin/master/console/#/create/client/new_realm. +- To create a client, you will need to set the client_id (just choose anyone), + the protocol (must be openid-connect) and the Root Url (you can leave it + blank) +- After creating the client, you will need to update some client's attributes + like: + + - Enable the Implicit flow (this one allows you to use the OpenStack CLI with + oidcv3 plugin) + - Set Access Type to confidential + - Add the Horizon and Keystone URIs to the Valid Redirect URIs. Keystone should be within the '/redirect_uri' path, for example: https://horizon.com/ and https://keystone.com/redirect_uri + - Save the changes + - Access the client's Mappers tab to add the user's attributes that will be + shared with the client (Keystone): + + - In this guide, we will need the following attribute mappers in Keycloak: + + ==================================== ============== + name/user attribute/token claim name mapper type + ==================================== ============== + openstack-user-domain user attribute + openstack-default-project user attribute + ==================================== ============== + +- After creating the client, you will need to create a user in that realm to + log in OpenStack via identity federation +- To create a user, access http://localhost:8080/auth/admin/master/console/#/create/user/new_realm and fill the form with the user's data +- After creating the user, you can access the tab "Credentials" to set the + user's password +- Then, in the tab "Attributes", you must set the authorization attributes to + be used by Keystone, these attributes are defined in the :ref:`attribute + mapping ` in Keystone + +After you create the Identity provider, you will need to get some data from the +Identity Provider to configure in Kolla-Ansible + +Configuring Kolla Ansible to use the Identity Provider +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This section is about how one can get the data needed in +:ref:`Setup OIDC via Kolla Ansible `. + +- name: The realm name, in this case it will be "new_realm" +- identifier: http://localhost:8080/auth/realms/new_realm/ (again, the "new_realm" is the name of the realm) +- certificate_file: This one can be downloaded from http://localhost:8080/auth/admin/master/console/#/realms/new_realm/keys +- metadata_folder: + + - localhost%3A8080%2Fauth%2Frealms%2Fnew_realm.client: + + - client_id: Access http://localhost:8080/auth/admin/master/console/#/realms/new_realm/clients , and access the client you created for Keystone, copy the Client ID displayed in the page + - client_secret: In the same page you got the client_id, access the tab + "Credentials" and copy the secret value + - localhost%3A8080%2Fauth%2Frealms%2Fnew_realm.provider: Copy the json from http://localhost:8080/auth/realms/new_realm/.well-known/openid-configuration (the "new_realm" is the realm name) + - localhost%3A8080%2Fauth%2Frealms%2Fnew_realm.conf: You can leave this file + as an empty json "{}" + + +After you finished the configuration of the Identity Provider, your main +configuration should look something like the following: + +.. code-block:: + + keystone_identity_providers: + - name: "new_realm" + openstack_domain: "new_domain" + protocol: "openid" + identifier: "http://localhost:8080/auth/realms/new_realm" + public_name: "Authenticate via new_realm" + attribute_mapping: "attribute_mapping_keycloak_new_realm" + metadata_folder: "/root/inDev/meta-idp" + certificate_file: "/root/inDev/certs/LRVweuT51StjMdsna59jKfB3xw0r8Iz1d1J1HeAbmlw.pem" + keystone_identity_mappings: + - name: "attribute_mapping_keycloak_new_realm" + file: "/root/inDev/attr_map/attribute_mapping.json" + +Then, after deploying OpenStack, you should be able to log in Horizon +using the "Authenticate using" -> "Authenticate via new_realm", and writing +"new_realm.com" in the "E-mail or domain name" field. After that, you will be +redirected to a new page to choose the Identity Provider in Keystone. Just click in the link +"localhost:8080/auth/realms/new_realm"; this will redirect you to Keycloak (idP) where +you will need to log in with the user that you created. If the user's +attributes in Keycloak are ok, the user will be created in OpenStack and you will +be able to log in Horizon. + +.. _attribute_mapping: + +Attribute mapping +~~~~~~~~~~~~~~~~~ +This section shows how to create the attribute mapping to map an Identity +Provider user to a Keystone user (ephemeral). + +The 'OIDC-' prefix in the remote types is defined in the 'OIDCClaimPrefix' +configuration in the wsgi-keystone.conf file; this prefix must be in the +attribute mapping as the mod-oidc-wsgi is adding the prefix in the user's +attributes before sending it to Keystone. The attribute 'openstack-user-domain' +will define the user's domain in OpenStack and the attribute +'openstack-default-project' will define the user's project in the OpenStack +(the user will be assigned with the role 'member' in the project) + +.. code-block:: json + + [ + { + "local": [ + { + "user": { + "name": "{0}", + "email": "{1}", + "domain": { + "name": "{2}" + } + }, + "domain": { + "name": "{2}" + }, + "projects": [ + { + "name": "{3}", + "roles": [ + { + "name": "member" + } + ] + } + ] + } + ], + "remote": [ + { + "type": "OIDC-preferred_username" + }, + { + "type": "OIDC-email" + }, + { + "type": "OIDC-openstack-user-domain" + }, + { + "type": "OIDC-openstack-default-project" + } + ] + } + ] diff --git a/doc/source/reference/shared-services/keystone-guide.rst b/doc/source/reference/shared-services/keystone-guide.rst index 2012b868f6..126e53c3d9 100644 --- a/doc/source/reference/shared-services/keystone-guide.rst +++ b/doc/source/reference/shared-services/keystone-guide.rst @@ -40,3 +40,241 @@ be configured in Keystone as necessary. Further infomation on Fernet tokens is available in the :keystone-doc:`Keystone documentation `. + +Federated identity +------------------ + +Keystone allows users to be authenticated via identity federation. This means +integrating OpenStack Keystone with an identity provider. The use of identity +federation allows users to access OpenStack services without the necessity of +an account in the OpenStack environment per se. The authentication is then +off-loaded to the identity provider of the federation. + +To enable identity federation, you will need to execute a set of configurations +in multiple OpenStack systems. Therefore, it is easier to use Kolla Ansible +to execute this process for operators. + +For upstream documentations, please see +:keystone-doc:`Configuring Keystone for Federation +` + +Supported protocols +~~~~~~~~~~~~~~~~~~~ + +OpenStack supports both OpenID Connect and SAML protocols for federated +identity, but for now, kolla Ansible supports only OpenID Connect. +Therefore, if you desire to use SAML in your environment, you will need +to set it up manually or extend Kolla Ansible to also support it. + +.. _setup-oidc-kolla-ansible: + +Setting up OpenID Connect via Kolla Ansible +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +First, you will need to register the OpenStack (Keystone) in your Identity +provider as a Service Provider. + +After registering Keystone, you will need to add the Identity Provider +configurations in your kolla-ansible globals configuration as the example +below: + +.. code-block:: yaml + + keystone_identity_providers: + - name: "myidp1" + openstack_domain: "my-domain" + protocol: "openid" + identifier: "https://accounts.google.com" + public_name: "Authenticate via myidp1" + attribute_mapping: "mappingId1" + metadata_folder: "path/to/metadata/folder" + certificate_file: "path/to/certificate/file.pem" + + keystone_identity_mappings: + - name: "mappingId1" + file: "/full/qualified/path/to/mapping/json/file/to/mappingId1" + +Identity providers configurations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +name +**** + +The internal name of the Identity provider in OpenStack. + +openstack_domain +**************** + +The OpenStack domain that the Identity Provider belongs. + +protocol +******** + +The federated protocol used by the IdP; e.g. openid or saml. We support only +OpenID connect right now. + +identifier +********** + +The Identity provider URL; e.g. https://accounts.google.com . + +public_name +*********** + +The Identity provider public name that will be shown for users in the Horizon +login page. + +attribute_mapping +***************** + +The attribute mapping to be used for the Identity Provider. This mapping is +expected to already exist in OpenStack or be configured in the +`keystone_identity_mappings` property. + +metadata_folder +*************** + +Path to the folder containing all of the identity provider metadata as JSON +files. + +The metadata folder must have all your Identity Providers configurations, +the name of the files will be the name (with path) of the Issuer configuration. +Such as: + +.. code-block:: + + - + - keycloak.example.org%2Fauth%2Frealms%2Fidp.client + | + - keycloak.example.org%2Fauth%2Frealms%2Fidp.conf + | + - keycloak.example.org%2Fauth%2Frealms%2Fidp.provider + +.. note:: + + The name of the file must be URL-encoded if needed. For example, if you have + an Issuer with ``/`` in the URL, then you need to escape it to ``%2F`` by + applying a URL escape in the file name. + +The content of these files must be a JSON + +``client``: + +The ``.client`` file handles the Service Provider credentials in the Issuer. + +During the first step, when you registered the OpenStack as a +Service Provider in the Identity Provider, you submitted a `cliend_id` and +generated a `client_secret`, so these are the values you must use in this +JSON file. + +.. code-block:: json + + { + "client_id":"", + "client_secret":"" + } + +``conf``: + +This file will be a JSON that overrides some of the OpenID Connect options. The +options that can be overridden are listed in the +`OpenID Connect Apache2 plugin documentation`_. +.. _`OpenID Connect Apache2 plugin documentation`: https://github.com/zmartzone/mod_auth_openidc/wiki/Multiple-Providers#opclient-configuration + +If you do not want to override the config values, you can leave this file as +an empty JSON file such as ``{}``. + +``provider``: + +This file will contain all specifications about the IdentityProvider. To +simplify, you can just use the JSON returned in the ``.well-known`` +Identity provider's endpoint: + +.. code-block:: json + + { + "issuer": "https://accounts.google.com", + "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth", + "token_endpoint": "https://oauth2.googleapis.com/token", + "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo", + "revocation_endpoint": "https://oauth2.googleapis.com/revoke", + "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs", + "response_types_supported": [ + "code", + "token", + "id_token", + "code token", + "code id_token", + "token id_token", + "code token id_token", + "none" + ], + "subject_types_supported": [ + "public" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "scopes_supported": [ + "openid", + "email", + "profile" + ], + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "client_secret_basic" + ], + "claims_supported": [ + "aud", + "email", + "email_verified", + "exp", + "family_name", + "given_name", + "iat", + "iss", + "locale", + "name", + "picture", + "sub" + ], + "code_challenge_methods_supported": [ + "plain", + "S256" + ] + } + +certificate_file +**************** + +Path to the Identity Provider certificate file, the file must be named as +'certificate-key-id.pem'. E.g. + +.. code-block:: + + - fb8ca5b7d8d9a5c6c6788071e866c6c40f3fc1f9.pem + +You can find the key-id in the Identity provider +`.well-known/openid-configuration` `jwks_uri` like in +`https://www.googleapis.com/oauth2/v3/certs` : + +.. code-block:: json + + { + "keys": [ + { + "e": "AQAB", + "use": "sig", + "n": "zK8PHf_6V3G5rU-viUOL1HvAYn7q--dxMoU...", + "kty": "RSA", + "kid": "fb8ca5b7d8d9a5c6c6788071e866c6c40f3fc1f9", + "alg": "RS256" + } + ] + } + +.. note:: + + The public key is different from the certificate, the file in this + configuration must be the Identity provider's certificate and not the + Identity provider's public key. diff --git a/etc/kolla/passwords.yml b/etc/kolla/passwords.yml index 73d1765de5..2ed54f9f57 100644 --- a/etc/kolla/passwords.yml +++ b/etc/kolla/passwords.yml @@ -252,3 +252,8 @@ redis_master_password: #################### prometheus_mysql_exporter_database_password: prometheus_alertmanager_password: + +############################### +# OpenStack identity federation +############################### +keystone_federation_openid_crypto_password: diff --git a/releasenotes/notes/add-keystone-support-to-openid-connect-859b12492f8347fe.yaml b/releasenotes/notes/add-keystone-support-to-openid-connect-859b12492f8347fe.yaml new file mode 100644 index 0000000000..956c3cb5cc --- /dev/null +++ b/releasenotes/notes/add-keystone-support-to-openid-connect-859b12492f8347fe.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for the OpenID Connect authentication protocol in Keystone and + enables both ID and access token authentication flows.