diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 8deda4476c..1454e826e7 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 @@ -1158,3 +1160,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.