diff --git a/etc/kayobe/ansible/secret-store-deploy-barbican.yml b/etc/kayobe/ansible/secret-store-deploy-barbican.yml new file mode 100644 index 0000000000..4324ff2ef3 --- /dev/null +++ b/etc/kayobe/ansible/secret-store-deploy-barbican.yml @@ -0,0 +1,107 @@ +--- +- name: Configure AppRole + any_errors_fatal: true + gather_facts: true + hosts: controllers[0] + vars: + secret_store_api_address: https://{{ internal_net_name | net_ip }}:8200 + secret_store_ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + tasks: + - name: Assert that secrets_barbican_approle_secret_id is defined + ansible.builtin.assert: + that: + - secrets_barbican_approle_secret_id is defined + fail_msg: Please define secrets_barbican_approle_secret_id in your secrets.yml + + - name: Include secret store keys + ansible.builtin.include_vars: + file: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/overcloud-{{ stackhpc_ca_secret_store }}-keys.json" + name: secret_store_keys + + - name: Ensure hvac is installed + ansible.builtin.pip: + name: hvac + state: present + extra_args: "{% if pip_upper_constraints_file %}-c {{ pip_upper_constraints_file }}{% endif %}" + virtualenv: "{{ virtualenv_path }}/kayobe" + + - name: Ensure AppRole is configured + environment: + https_proxy: "" + block: + - name: Enable AppRole auth module + hashivault_auth_method: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ secret_store_ca_cert }}" + token: "{{ secret_store_keys.root_token }}" + method_type: approle + state: enabled + + - name: Enable barbican kv store + hashivault_secret_engine: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ secret_store_ca_cert }}" + token: "{{ secret_store_keys.root_token }}" + name: barbican + backend: kv + description: Barbican kv store + + - name: Ensure barbican policy is defined + hashivault_policy: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ secret_store_ca_cert }}" + token: "{{ secret_store_keys.root_token }}" + name: barbican-policy + state: present + rules: | + path "barbican/*" { + capabilities = ["create", "read", "update", "delete", "list"] + } + + - name: Ensure barbican AppRole is defined + hashivault_approle_role: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ secret_store_ca_cert }}" + token: "{{ secret_store_keys.root_token }}" + bind_secret_id: true + secret_id_bound_cidrs: "{{ internal_net_name | net_cidr }}" + secret_id_ttl: 0 + token_policies: barbican-policy + name: barbican + + - name: Get barbican Approle ID + hashivault_approle_role_id: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ secret_store_ca_cert }}" + token: "{{ secret_store_keys.root_token }}" + name: barbican + register: barbican_role_id + + - name: Print barbican Approle ID + ansible.builtin.debug: + msg: barbican role id is {{ barbican_role_id.id }} + + - name: Write barbican Approle ID to file if requested + delegate_to: localhost + ansible.builtin.copy: + content: "{{ barbican_role_id.id }}" + dest: "{{ stackhpc_barbican_role_id_file_path | default('~/barbican-role-id') }}" + when: stackhpc_write_barbican_role_id_to_file | default(false) | bool + + - name: Check if barbican Approle Secret ID is defined + hashivault_approle_role_secret_get: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ secret_store_ca_cert }}" + token: "{{ secret_store_keys.root_token }}" + secret: "{{ secrets_barbican_approle_secret_id }}" + name: barbican + register: barbican_approle_secret_get + + - name: Ensure barbican AppRole Secret ID is defined + hashivault_approle_role_secret: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ secret_store_ca_cert }}" + token: "{{ secret_store_keys.root_token }}" + secret: "{{ secrets_barbican_approle_secret_id }}" + name: barbican + when: barbican_approle_secret_get.status == "absent" diff --git a/etc/kayobe/ansible/secret-store-deploy-overcloud.yml b/etc/kayobe/ansible/secret-store-deploy-overcloud.yml new file mode 100644 index 0000000000..aa9a200d1f --- /dev/null +++ b/etc/kayobe/ansible/secret-store-deploy-overcloud.yml @@ -0,0 +1,172 @@ +--- +# Required for uri module to work with self-signed certificates and for systems to trust +# the self-signed CA +- name: Install CA on controllers + hosts: controllers + tasks: + - name: Copy the intermediate CA + ansible.builtin.copy: + src: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/OS-TLS-ROOT.pem" + dest: "{{ '/etc/pki/ca-trust/source/anchors/OS-TLS-ROOT.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' + }}" + mode: "0644" + become: true + + - name: Update system CA + become: true + ansible.builtin.command: "{{ 'update-ca-trust' if ansible_facts.os_family == 'RedHat' else 'update-ca-certificates' }}" + +- name: Deploy Secret store on the overcloud + any_errors_fatal: true + gather_facts: true + hosts: controllers + vars: + secret_store_bind_interface: "{{ internal_net_name | net_interface }}" + secret_store_bind_address: "{{ internal_net_name | net_ip }}" + # This is the IP address of the first controller and therefore the leader within + # OpenBao. This could be replaced with the VIP address of the internal network if + # HAProxy has been configured to load balance the OpenBao API. + openbao_raft_leaders: + - "{{ internal_net_name | net_ip(inventory_hostname=groups['controllers'][0]) }}" + tasks: + - name: Set a fact about the virtualenv on the remote system + ansible.builtin.set_fact: + virtualenv: "{{ ansible_python_interpreter | dirname | dirname }}" + when: + - ansible_python_interpreter is defined + - not ansible_python_interpreter.startswith('/bin/') + - not ansible_python_interpreter.startswith('/usr/bin/') + + - name: Ensure Python hvac module is installed + ansible.builtin.pip: + name: hvac + state: latest + extra_args: "{% if pip_upper_constraints_file %}-c {{ pip_upper_constraints_file }}{% endif %}" + virtualenv: "{{ virtualenv is defined | ternary(virtualenv, omit) }}" + become: "{{ virtualenv is not defined }}" + + - name: Ensure /opt/kayobe/{{ stackhpc_ca_secret_store }} exists + ansible.builtin.file: + path: /opt/kayobe/{{ stackhpc_ca_secret_store }} + state: directory + + - name: Template out TLS key and cert + ansible.builtin.copy: + # Within the OpenBao container these uids & gids map to the openbao user + src: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/{{ item }}" + dest: /opt/kayobe/{{ stackhpc_ca_secret_store }}/{{ item }} + owner: 100 + group: 1000 + mode: "0600" + loop: + - "{% if kolla_internal_fqdn != kolla_internal_vip_address %}{{ kolla_internal_fqdn }}{% else %}overcloud{% endif %}.crt" + - "{% if kolla_internal_fqdn != kolla_internal_vip_address %}{{ kolla_internal_fqdn }}{% else %}overcloud{% endif %}.key" + - "OS-TLS-INT.crt" + become: true + + - name: Apply vault role + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault + vars: + hashicorp_registry_url: "{{ overcloud_hashicorp_registry_url }}" + hashicorp_registry_username: "{{ overcloud_hashicorp_registry_username }}" + hashicorp_registry_password: "{{ overcloud_hashicorp_registry_password }}" + consul_docker_image: "{{ overcloud_consul_docker_image }}" + consul_docker_tag: "{{ overcloud_consul_docker_tag }}" + consul_bind_interface: "{{ secret_store_bind_interface }}" + vault_bind_address: "{{ secret_store_bind_address }}" + vault_config_dir: /opt/kayobe/vault + vault_cluster_name: overcloud + vault_ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + vault_docker_image: "{{ overcloud_vault_docker_image }}" + vault_docker_tag: "{{ overcloud_vault_docker_tag }}" + vault_tls_cert: "{% if kolla_internal_fqdn != kolla_internal_vip_address %}{{ kolla_internal_fqdn }}{% else %}overcloud{% endif %}.crt" + vault_tls_key: "{% if kolla_internal_fqdn != kolla_internal_vip_address %}{{ kolla_internal_fqdn }}{% else %}overcloud{% endif %}.key" + copy_self_signed_ca: true + vault_api_addr: https://{{ internal_net_name | net_ip }}:8200 + vault_write_keys_file: true + vault_write_keys_file_path: "{{ kayobe_env_config_path }}/vault/overcloud-vault-keys.json" + when: stackhpc_ca_secret_store == "vault" + + - name: Apply OpenBao role + ansible.builtin.import_role: + name: stackhpc.hashicorp.openbao + vars: + openbao_bind_addr: "{{ secret_store_bind_address }}" + openbao_registry_url: "{{ overcloud_openbao_registry_url }}" + openbao_registry_username: "{{ overcloud_openbao_registry_username }}" + openbao_registry_password: "{{ overcloud_openbao_registry_password }}" + openbao_config_dir: /opt/kayobe/openbao + openbao_cluster_name: overcloud + openbao_ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + openbao_docker_image: "{{ overcloud_openbao_docker_image }}" + openbao_docker_tag: "{{ overcloud_openbao_docker_tag }}" + openbao_tls_cert: "{% if kolla_internal_fqdn != kolla_internal_vip_address %}{{ kolla_internal_fqdn }}{% else %}overcloud{% endif %}.crt" + openbao_tls_key: "{% if kolla_internal_fqdn != kolla_internal_vip_address %}{{ kolla_internal_fqdn }}{% else %}overcloud{% endif %}.key" + openbao_tls_ca: "OS-TLS-INT.crt" + copy_self_signed_ca: true + openbao_api_addr: https://{{ internal_net_name | net_ip }}:8200 + openbao_write_keys_file: true + openbao_write_keys_file_path: "{{ kayobe_env_config_path }}/openbao/overcloud-openbao-keys.json" + when: stackhpc_ca_secret_store == "openbao" + + - name: Include secret store keys + ansible.builtin.include_vars: + file: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/overcloud-{{ stackhpc_ca_secret_store }}-keys.json" + name: secret_store_keys + + - name: Unseal first secret store instance + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault_unseal + vars: + vault_api_addr: https://{{ internal_net_name | net_ip }}:8200 + vault_unseal_token: "{{ secret_store_keys.root_token }}" + vault_unseal_ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + vault_unseal_keys: "{{ secret_store_keys.keys_base64 }}" + environment: + https_proxy: "" + run_once: true + + # As the first instance is now unsealed the other instances will now need some + # time to connect before we can proceed. + - name: Wait for OpenBao Raft peers to connect + ansible.builtin.wait_for: + timeout: 30 + delegate_to: localhost + + # Raft peers take few seconds before they report an unsealed state therefore + # we must wait. + - name: Unseal all secret store instances + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault_unseal + vars: + vault_api_addr: https://{{ internal_net_name | net_ip }}:8200 + vault_unseal_token: "{{ secret_store_keys.root_token }}" + vault_unseal_ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + vault_unseal_keys: "{{ secret_store_keys.keys_base64 }}" + vault_unseal_timeout: 10 + environment: + https_proxy: "" + +- name: Configure PKI + any_errors_fatal: true + gather_facts: true + hosts: controllers[0] + tasks: + - name: Apply pki role + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault_pki + vars: + vault_token: "{{ secret_store_keys.root_token }}" + vault_api_addr: https://{{ internal_net_name | net_ip }}:8200 + vault_ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + vault_pki_root_create: false + vault_pki_intermediate_import: true + vault_pki_intermediate_ca_name: OS-TLS-INT + vault_pki_intermediate_ca_bundle: "{{ lookup('file', kayobe_env_config_path + '/' + stackhpc_ca_secret_store + '/OS-TLS-INT.pem') }}" + vault_pki_intermediate_ca_cert: "{{ lookup('file', kayobe_env_config_path + '/' + stackhpc_ca_secret_store + '/OS-TLS-INT.crt') }}" + vault_pki_intermediate_roles: "{{ overcloud_vault_pki_roles if stackhpc_ca_secret_store == 'vault' else overcloud_openbao_pki_roles }}" + vault_pki_write_certificate_files: true + vault_pki_certificates_directory: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}" + environment: + https_proxy: "" diff --git a/etc/kayobe/ansible/secret-store-deploy-seed.yml b/etc/kayobe/ansible/secret-store-deploy-seed.yml new file mode 100644 index 0000000000..44073edf6d --- /dev/null +++ b/etc/kayobe/ansible/secret-store-deploy-seed.yml @@ -0,0 +1,102 @@ +--- +- name: Deploy CA secret store on the seed + any_errors_fatal: true + gather_facts: true + hosts: seed + vars: + secret_store_bind_interface: lo + secret_store_bind_address: "{{ ansible_facts[secret_store_bind_interface].ipv4.address }}" + secret_store_api_address: "http://{{ secret_store_bind_address }}:8200" + tasks: + - name: Set a fact about the virtualenv on the remote system + ansible.builtin.set_fact: + virtualenv: "{{ ansible_python_interpreter | dirname | dirname }}" + when: + - ansible_python_interpreter is defined + - not ansible_python_interpreter.startswith('/bin/') + - not ansible_python_interpreter.startswith('/usr/bin/') + + - name: Ensure Python PyYAML and hvac modules are installed + ansible.builtin.pip: + name: + - PyYAML + - hvac + state: latest + extra_args: "{% if pip_upper_constraints_file %}-c {{ pip_upper_constraints_file }}{% endif %}" + virtualenv: "{{ virtualenv is defined | ternary(virtualenv, omit) }}" + become: "{{ virtualenv is not defined }}" + + - name: Ensure secret store directory exists in Kayobe configuration + ansible.builtin.file: + path: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/" + state: directory + delegate_to: localhost + run_once: true + + - name: Apply vault role + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault + vars: + hashicorp_registry_url: "{{ seed_hashicorp_registry_url }}" + hashicorp_registry_username: "{{ seed_hashicorp_registry_username }}" + hashicorp_registry_password: "{{ seed_hashicorp_registry_password }}" + consul_docker_image: "{{ seed_consul_docker_image }}" + consul_docker_tag: "{{ seed_consul_docker_tag }}" + consul_bind_interface: "{{ secret_store_bind_interface }}" + vault_bind_address: "{{ secret_store_bind_address }}" + vault_api_addr: "{{ secret_store_api_address }}" + vault_config_dir: /opt/kayobe/vault + vault_cluster_name: seed + vault_docker_image: "{{ seed_vault_docker_image }}" + vault_docker_tag: "{{ seed_vault_docker_tag }}" + vault_write_keys_file: true + vault_write_keys_file_path: "{{ kayobe_env_config_path }}/vault/seed-vault-keys.json" + when: stackhpc_ca_secret_store == "vault" + + - name: Apply OpenBao role + ansible.builtin.import_role: + name: stackhpc.hashicorp.openbao + vars: + openbao_bind_addr: "{{ secret_store_bind_address }}" + openbao_api_addr: "{{ secret_store_api_address }}" + openbao_registry_url: "{{ seed_openbao_registry_url }}" + openbao_registry_username: "{{ seed_openbao_registry_username }}" + openbao_registry_password: "{{ seed_openbao_registry_password }}" + openbao_config_dir: /opt/kayobe/openbao + openbao_cluster_name: seed + openbao_docker_image: "{{ seed_openbao_docker_image }}" + openbao_docker_tag: "{{ seed_openbao_docker_tag }}" + openbao_write_keys_file: true + openbao_write_keys_file_path: "{{ kayobe_env_config_path }}/openbao/seed-openbao-keys.json" + when: stackhpc_ca_secret_store == "openbao" + + - name: Include {{ stackhpc_ca_secret_store }} keys + ansible.builtin.include_vars: + file: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/seed-{{ stackhpc_ca_secret_store }}-keys.json" + name: secret_store_keys + + - name: Unseal {{ stackhpc_ca_secret_store }} + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault_unseal + vars: + vault_api_addr: "{{ secret_store_api_address }}" + vault_unseal_keys: "{{ secret_store_keys.keys_base64 }}" + + - name: Apply PKI role + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault_pki + vars: + vault_api_addr: "{{ secret_store_api_address }}" + vault_token: "{{ secret_store_keys.root_token }}" + vault_pki_root_ca_name: OS-TLS-ROOT + vault_pki_write_root_ca_to_file: true + vault_pki_intermediate_ca_name: OS-TLS-INT + vault_pki_intermediate_export: true + vault_pki_intermediate_roles: "{{ seed_vault_pki_roles if stackhpc_ca_secret_store == 'vault' else seed_openbao_pki_roles }}" + vault_pki_certificates_directory: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}" + vault_pki_generate_certificates: true + vault_pki_write_certificates: true + vault_pki_certificate_subject: "{{ seed_vault_pki_certificate_subject if stackhpc_ca_secret_store == 'vault' else seed_openbao_pki_certificate_subject }}" + vault_pki_write_certificate_files: true + vault_pki_write_pem_bundle: false + vault_pki_write_int_ca_to_file: true diff --git a/etc/kayobe/ansible/secret-store-generate-backend-tls.yml b/etc/kayobe/ansible/secret-store-generate-backend-tls.yml new file mode 100644 index 0000000000..4b0eb87eed --- /dev/null +++ b/etc/kayobe/ansible/secret-store-generate-backend-tls.yml @@ -0,0 +1,83 @@ +--- +# Required for uri module to work with self-signed certificates and for systems to trust +# the self-signed CA +- name: Install CA + hosts: controllers:network + tasks: + - name: Copy the intermediate CA + ansible.builtin.copy: + src: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/OS-TLS-ROOT.pem" + dest: "{{ '/etc/pki/ca-trust/source/anchors/OS-TLS-ROOT.crt' if ansible_facts.os_family == 'RedHat' \ + else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + mode: "0644" + become: true + + - name: Update system CA + become: true + ansible.builtin.command: "{{ 'update-ca-trust' if ansible_facts.os_family == 'RedHat' else 'update-ca-certificates' }}" + +- name: Generate backend API certificates + hosts: controllers:network + vars: + secret_store_api_address: https://{{ internal_net_name | net_ip(groups['controllers'][0]) }}:8200 + secret_store_intermediate_ca_name: OS-TLS-INT + tasks: + - name: Set a fact about the virtualenv on the remote system + ansible.builtin.set_fact: + virtualenv: "{{ ansible_python_interpreter | dirname | dirname }}" + when: + - ansible_python_interpreter is defined + - not ansible_python_interpreter.startswith('/bin/') + - not ansible_python_interpreter.startswith('/usr/bin/') + + - name: Ensure Python hvac module is installed + ansible.builtin.pip: + name: hvac + state: latest + extra_args: "{% if pip_upper_constraints_file %}-c {{ pip_upper_constraints_file }}{% endif %}" + virtualenv: "{{ virtualenv is defined | ternary(virtualenv, omit) }}" + become: "{{ virtualenv is not defined }}" + + - name: Include secret store keys + ansible.builtin.include_vars: + file: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/overcloud-{{ stackhpc_ca_secret_store }}-keys.json" + name: secret_store_keys + + - name: Issue a certificate for backend TLS + hashivault_pki_cert_issue: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + token: "{{ secret_store_keys.root_token }}" + mount_point: "{{ secret_store_intermediate_ca_name }}" + role: "{{ overcloud_vault_pki_backend_tls_role_name if stackhpc_ca_secret_store == 'vault' else overcloud_openbao_pki_backend_tls_role_name }}" + common_name: "" + extra_params: + ip_sans: "{{ internal_net_name | net_ip }}" + register: backend_cert + environment: + https_proxy: "" + + - name: Ensure certificates directory exists + ansible.builtin.file: + path: "{{ kayobe_env_config_path }}/kolla/certificates" + state: directory + delegate_to: localhost + + - name: Copy backend cert + no_log: true + ansible.builtin.copy: + dest: "{{ kayobe_env_config_path }}/kolla/certificates/{{ inventory_hostname }}-cert.pem" + content: | + {{ backend_cert.data.certificate }} + {{ backend_cert.data.issuing_ca }} + mode: "0600" + delegate_to: localhost + + - name: Copy backend key + no_log: true + ansible.builtin.copy: + dest: "{{ kayobe_env_config_path }}/kolla/certificates/{{ inventory_hostname }}-key.pem" + content: | + {{ backend_cert.data.private_key }} + mode: "0600" + delegate_to: localhost diff --git a/etc/kayobe/ansible/secret-store-generate-internal-tls.yml b/etc/kayobe/ansible/secret-store-generate-internal-tls.yml new file mode 100644 index 0000000000..ea9dfd9290 --- /dev/null +++ b/etc/kayobe/ansible/secret-store-generate-internal-tls.yml @@ -0,0 +1,56 @@ +--- +- name: Generate internal API certificate + hosts: controllers + run_once: true + vars: + secret_store_api_address: https://{{ internal_net_name | net_ip }}:8200 + secret_store_intermediate_ca_name: OS-TLS-INT + tasks: + - name: Include secret store keys + ansible.builtin.include_vars: + file: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/overcloud-{{ stackhpc_ca_secret_store }}-keys.json" + name: secret_store_keys + + - name: Issue a certificate for internal TLS + hashivault_pki_cert_issue: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + token: "{{ secret_store_keys.root_token }}" + mount_point: "{{ secret_store_intermediate_ca_name }}" + role: "{{ overcloud_vault_pki_internal_tls_role_name if stackhpc_ca_secret_store == 'vault' else overcloud_openbao_pki_internal_tls_role_name }}" + common_name: "{% if kolla_internal_fqdn != kolla_internal_vip_address %}{{ kolla_internal_fqdn }}{% endif %}" + extra_params: + ip_sans: "{{ kolla_internal_vip_address }}" + register: internal_cert + environment: + https_proxy: "" + + - name: Ensure certificates directory exists + ansible.builtin.file: + path: "{{ kayobe_env_config_path }}/kolla/certificates" + state: directory + delegate_to: localhost + + - name: Ensure CA certificates directory exists + ansible.builtin.file: + path: "{{ kayobe_env_config_path }}/kolla/certificates/ca" + state: directory + delegate_to: localhost + + - name: Copy internal API PEM bundle + no_log: true + ansible.builtin.copy: + dest: "{{ kayobe_env_config_path }}/kolla/certificates/haproxy-internal.pem" + content: | + {{ internal_cert.data.certificate }} + {{ internal_cert.data.issuing_ca }} + {{ internal_cert.data.private_key }} + mode: "0600" + delegate_to: localhost + + - name: Copy root CA + ansible.builtin.copy: + src: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/OS-TLS-ROOT.pem" + dest: "{{ kayobe_env_config_path }}/kolla/certificates/ca/{{ stackhpc_ca_secret_store }}.crt" + mode: "0600" + delegate_to: localhost diff --git a/etc/kayobe/ansible/secret-store-generate-test-external-tls.yml b/etc/kayobe/ansible/secret-store-generate-test-external-tls.yml new file mode 100644 index 0000000000..206bf2b7ac --- /dev/null +++ b/etc/kayobe/ansible/secret-store-generate-test-external-tls.yml @@ -0,0 +1,57 @@ +--- +- name: Generate external API certificate (for testing only) + hosts: controllers + run_once: true + vars: + secret_store_api_address: https://{{ internal_net_name | net_ip }}:8200 + # NOTE: Using the same CA as internal TLS. + secret_store_intermediate_ca_name: OS-TLS-INT + tasks: + - name: Include secret store keys + ansible.builtin.include_vars: + file: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/overcloud-{{ stackhpc_ca_secret_store }}-keys.json" + name: secret_store_keys + + - name: Issue a certificate for external TLS + hashivault_pki_cert_issue: # noqa: fqcn + url: "{{ secret_store_api_address }}" + ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + token: "{{ secret_store_keys.root_token }}" + mount_point: "{{ secret_store_intermediate_ca_name }}" + role: "{{ overcloud_vault_pki_external_tls_role_name if stackhpc_ca_secret_store == 'vault' else overcloud_openbao_pki_external_tls_role_name }}" + common_name: "{% if kolla_external_fqdn != kolla_external_vip_address %}{{ kolla_external_fqdn }}{% endif %}" + extra_params: + ip_sans: "{{ kolla_external_vip_address }}" + register: external_cert + environment: + https_proxy: "" + + - name: Ensure certificates directory exists + ansible.builtin.file: + path: "{{ kayobe_env_config_path }}/kolla/certificates" + state: directory + delegate_to: localhost + + - name: Ensure CA certificates directory exists + ansible.builtin.file: + path: "{{ kayobe_env_config_path }}/kolla/certificates/ca" + state: directory + delegate_to: localhost + + - name: Copy external API PEM bundle + no_log: true + ansible.builtin.copy: + dest: "{{ kayobe_env_config_path }}/kolla/certificates/haproxy.pem" + content: | + {{ external_cert.data.certificate }} + {{ external_cert.data.issuing_ca }} + {{ external_cert.data.private_key }} + mode: "0600" + delegate_to: localhost + + - name: Copy root CA + ansible.builtin.copy: + src: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/OS-TLS-ROOT.pem" + dest: "{{ kayobe_env_config_path }}/kolla/certificates/ca/{{ stackhpc_ca_secret_store }}.crt" + mode: "0600" + delegate_to: localhost diff --git a/etc/kayobe/ansible/secret-store-unseal-overcloud.yml b/etc/kayobe/ansible/secret-store-unseal-overcloud.yml new file mode 100644 index 0000000000..634e7e029d --- /dev/null +++ b/etc/kayobe/ansible/secret-store-unseal-overcloud.yml @@ -0,0 +1,37 @@ +--- +- name: Unseal secret store on the overcloud + any_errors_fatal: true + gather_facts: true + hosts: controllers + tasks: + - name: Set a fact about the virtualenv on the remote system + ansible.builtin.set_fact: + virtualenv: "{{ ansible_python_interpreter | dirname | dirname }}" + when: + - ansible_python_interpreter is defined + - not ansible_python_interpreter.startswith('/bin/') + - not ansible_python_interpreter.startswith('/usr/bin/') + + - name: Ensure Python hvac module is installed + ansible.builtin.pip: + name: hvac + state: latest + extra_args: "{% if pip_upper_constraints_file %}-c {{ pip_upper_constraints_file }}{% endif %}" + virtualenv: "{{ virtualenv is defined | ternary(virtualenv, omit) }}" + become: "{{ virtualenv is not defined }}" + + - name: Include secret store keys + ansible.builtin.include_vars: + file: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/overcloud-{{ stackhpc_ca_secret_store }}-keys.json" + name: secret_store_keys + + - name: Apply secret store unseal role + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault_unseal + vars: + vault_api_addr: https://{{ internal_net_name | net_ip }}:8200 + vault_unseal_token: "{{ secret_store_keys.root_token }}" + vault_unseal_ca_cert: "{{ '/etc/pki/tls/certs/ca-bundle.crt' if ansible_facts.os_family == 'RedHat' else '/usr/local/share/ca-certificates/OS-TLS-ROOT.crt' }}" + vault_unseal_keys: "{{ secret_store_keys.keys_base64 }}" + environment: + https_proxy: "" diff --git a/etc/kayobe/ansible/secret-store-unseal-seed.yml b/etc/kayobe/ansible/secret-store-unseal-seed.yml new file mode 100644 index 0000000000..a404101ed6 --- /dev/null +++ b/etc/kayobe/ansible/secret-store-unseal-seed.yml @@ -0,0 +1,34 @@ +--- +- name: Unseal secret store on the seed + any_errors_fatal: true + gather_facts: true + hosts: seed + vars: + vault_api_addr: http://127.0.0.1:8200 + tasks: + - name: Set a fact about the virtualenv on the remote system + ansible.builtin.set_fact: + virtualenv: "{{ ansible_python_interpreter | dirname | dirname }}" + when: + - ansible_python_interpreter is defined + - not ansible_python_interpreter.startswith('/bin/') + - not ansible_python_interpreter.startswith('/usr/bin/') + + - name: Ensure Python hvac module is installed + ansible.builtin.pip: + name: hvac + state: latest + extra_args: "{% if pip_upper_constraints_file %}-c {{ pip_upper_constraints_file }}{% endif %}" + virtualenv: "{{ virtualenv is defined | ternary(virtualenv, omit) }}" + become: "{{ virtualenv is not defined }}" + + - name: Include secret store keys + ansible.builtin.include_vars: + file: "{{ kayobe_env_config_path }}/{{ stackhpc_ca_secret_store }}/seed-{{ stackhpc_ca_secret_store }}-keys.json" + name: secret_store_keys + + - name: Apply unseal role + ansible.builtin.import_role: + name: stackhpc.hashicorp.vault_unseal + vars: + vault_unseal_keys: "{{ secret_store_keys.keys_base64 }}" diff --git a/etc/kayobe/environments/ci-multinode/stackhpc-monitoring.yml b/etc/kayobe/environments/ci-multinode/stackhpc-monitoring.yml index 93ce650b4f..d9693381ee 100644 --- a/etc/kayobe/environments/ci-multinode/stackhpc-monitoring.yml +++ b/etc/kayobe/environments/ci-multinode/stackhpc-monitoring.yml @@ -1,3 +1,3 @@ --- # Path to a CA certificate file to trust in the OpenStack Capacity exporter. -stackhpc_os_capacity_openstack_cacert: "{{ kayobe_env_config_path }}/kolla/certificates/ca/vault.crt" +stackhpc_os_capacity_openstack_cacert: "{{ kayobe_env_config_path }}/kolla/certificates/ca/{{ stackhpc_ca_secret_store }}.crt" diff --git a/etc/kayobe/environments/ci-multinode/tempest.yml b/etc/kayobe/environments/ci-multinode/tempest.yml index 0657946bb4..327fbef345 100644 --- a/etc/kayobe/environments/ci-multinode/tempest.yml +++ b/etc/kayobe/environments/ci-multinode/tempest.yml @@ -3,4 +3,4 @@ rally_no_sensitive_log: false # Add the Vault CA certificate to the rally container when running tempest. -tempest_cacert: "{{ kayobe_env_config_path }}/kolla/certificates/ca/vault.crt" +tempest_cacert: "{{ kayobe_env_config_path }}/kolla/certificates/ca/{{ stackhpc_ca_secret_store }}.crt" diff --git a/etc/kayobe/inventory/group_vars/all/vault b/etc/kayobe/inventory/group_vars/all/vault index 22e89a4558..57d0d192a7 100644 --- a/etc/kayobe/inventory/group_vars/all/vault +++ b/etc/kayobe/inventory/group_vars/all/vault @@ -83,3 +83,9 @@ overcloud_vault_pki_roles: locality: ["Bristol"] organization: ["StackHPC"] ou: ["OpenStack"] + +seed_vault_pki_certificate_subject: + - common_name: "{% if kolla_internal_fqdn != kolla_internal_vip_address %}{{ kolla_internal_fqdn }}{% else %}overcloud{% endif %}" + role: "{{ seed_vault_pki_role_name }}" + extra_params: + ip_sans: "{% for host in groups['controllers'] %}{{ internal_net_name | net_ip(host) }}{% if not loop.last %},{% endif %}{% endfor %},{{ kolla_internal_vip_address }}" diff --git a/etc/kayobe/stackhpc.yml b/etc/kayobe/stackhpc.yml index 9c77d81ac6..0c0bf75a6f 100644 --- a/etc/kayobe/stackhpc.yml +++ b/etc/kayobe/stackhpc.yml @@ -203,3 +203,10 @@ download_amphora_from_ark: true # Octavia Amphora image version stackhpc_amphora_image_version: "2024.1-20250117T092645" + +################################################################################ +# Certificate Authority + +# Secret store to deploy as a Certificate Authority. +# Valid options are "vault" and "openbao". Default is "openbao". +stackhpc_ca_secret_store: openbao diff --git a/releasenotes/notes/merge-openbao-vault-playbooks-0c78364f4a86f6e0.yaml b/releasenotes/notes/merge-openbao-vault-playbooks-0c78364f4a86f6e0.yaml new file mode 100644 index 0000000000..ef377665bd --- /dev/null +++ b/releasenotes/notes/merge-openbao-vault-playbooks-0c78364f4a86f6e0.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Created new set of playbooks with name prefix of "secret-store". + These playbooks merge OpenBao playbooks and Hashicorp Vault + playbooks. + By default, the playbooks will deploy OpenBao. + To use Hashicorp Vault, set ``stackhpc_ca_secret_store: vault``.