diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 30508643cc..3c58363a48 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -406,6 +406,7 @@ grafana_server_listen_port: "{{ grafana_server_port }}" haproxy_stats_port: "1984" haproxy_monitor_port: "61313" +haproxy_ssh_port: "2985" heat_internal_fqdn: "{{ kolla_internal_fqdn }}" heat_external_fqdn: "{{ kolla_external_fqdn }}" @@ -450,6 +451,8 @@ keystone_ssh_port: "8023" kuryr_port: "23750" +letsencrypt_webserver_port: "8081" + magnum_internal_fqdn: "{{ kolla_internal_fqdn }}" magnum_external_fqdn: "{{ kolla_external_fqdn }}" magnum_api_port: "9511" @@ -834,6 +837,7 @@ enable_ironic_pxe_uefi: "no" enable_ironic_prometheus_exporter: "{{ enable_ironic | bool and enable_prometheus | bool }}" enable_iscsid: "{{ enable_cinder | bool and enable_cinder_backend_iscsi | bool }}" enable_kuryr: "no" +enable_letsencrypt: "no" enable_magnum: "no" enable_manila: "no" enable_manila_backend_generic: "no" @@ -994,7 +998,8 @@ kolla_tls_backend_key: "{{ kolla_certificates_dir }}/backend-key.pem" ##################### # ACME client options ##################### -acme_client_servers: [] +acme_client_lego: "server lego {{ api_interface_address }}:{{ letsencrypt_webserver_port }}" +acme_client_servers: "{% set arr = [] %}{% if enable_letsencrypt | bool %}{{ arr.append(acme_client_lego) }}{% endif %}{{ arr }}" #################### # Keystone options diff --git a/ansible/inventory/all-in-one b/ansible/inventory/all-in-one index ad831ab1cf..138022e73b 100644 --- a/ansible/inventory/all-in-one +++ b/ansible/inventory/all-in-one @@ -191,6 +191,9 @@ control [venus:children] monitoring +[letsencrypt:children] +loadbalancer + # Additional control implemented here. These groups allow you to control which # services run on which hosts at a per-service level. # @@ -687,3 +690,9 @@ venus [venus-manager:children] venus + +[letsencrypt-webserver:children] +letsencrypt + +[letsencrypt-lego:children] +letsencrypt diff --git a/ansible/inventory/multinode b/ansible/inventory/multinode index 32bdd31611..eaa077c37d 100644 --- a/ansible/inventory/multinode +++ b/ansible/inventory/multinode @@ -209,6 +209,9 @@ control [venus:children] monitoring +[letsencrypt:children] +loadbalancer + # Additional control implemented here. These groups allow you to control which # services run on which hosts at a per-service level. # @@ -706,3 +709,9 @@ venus [venus-manager:children] venus + +[letsencrypt-webserver:children] +letsencrypt + +[letsencrypt-lego:children] +letsencrypt diff --git a/ansible/roles/certificates/tasks/generate.yml b/ansible/roles/certificates/tasks/generate.yml index b38f8ab41f..cf24d969ef 100644 --- a/ansible/roles/certificates/tasks/generate.yml +++ b/ansible/roles/certificates/tasks/generate.yml @@ -67,6 +67,7 @@ dest: "{{ kolla_external_fqdn_cert }}" mode: "0660" when: + - not enable_letsencrypt | bool - kolla_enable_tls_external | bool - block: @@ -77,6 +78,7 @@ remote_src: yes mode: "0660" when: + - not enable_letsencrypt | bool - kolla_enable_tls_external | bool - kolla_enable_tls_internal | bool - kolla_same_external_internal_vip | bool @@ -137,5 +139,6 @@ dest: "{{ kolla_internal_fqdn_cert }}" mode: "0660" when: + - not enable_letsencrypt | bool - kolla_enable_tls_internal | bool - not kolla_same_external_internal_vip | bool diff --git a/ansible/roles/common/templates/cron-logrotate-letsencrypt.conf.j2 b/ansible/roles/common/templates/cron-logrotate-letsencrypt.conf.j2 new file mode 100644 index 0000000000..fea08e0163 --- /dev/null +++ b/ansible/roles/common/templates/cron-logrotate-letsencrypt.conf.j2 @@ -0,0 +1,3 @@ +"/var/log/kolla/letsencrypt/*.log" +{ +} diff --git a/ansible/roles/haproxy-config/defaults/main.yml b/ansible/roles/haproxy-config/defaults/main.yml index d6456bd2da..ea72d59906 100644 --- a/ansible/roles/haproxy-config/defaults/main.yml +++ b/ansible/roles/haproxy-config/defaults/main.yml @@ -5,6 +5,7 @@ haproxy_service_template: "haproxy_single_service_split.cfg.j2" haproxy_frontend_http_extra: - "option httplog" - "option forwardfor" +haproxy_frontend_redirect_extra: [] haproxy_frontend_tcp_extra: - "option tcplog" haproxy_backend_http_extra: [] diff --git a/ansible/roles/haproxy-config/templates/haproxy_single_service_split.cfg.j2 b/ansible/roles/haproxy-config/templates/haproxy_single_service_split.cfg.j2 index 127ff911df..124c04dc92 100644 --- a/ansible/roles/haproxy-config/templates/haproxy_single_service_split.cfg.j2 +++ b/ansible/roles/haproxy-config/templates/haproxy_single_service_split.cfg.j2 @@ -1,7 +1,7 @@ #jinja2: lstrip_blocks: True -{%- set external_tls_bind_info = 'ssl crt /etc/haproxy/haproxy.pem' if kolla_enable_tls_external|bool else '' %} +{%- set external_tls_bind_info = 'ssl crt /etc/haproxy/certificates/haproxy.pem' if kolla_enable_tls_external|bool else '' %} {%- set external_tls_bind_info = "%s %s" % (external_tls_bind_info, haproxy_http2_protocol) if kolla_enable_tls_external|bool and haproxy_enable_http2|bool else external_tls_bind_info %} -{%- set internal_tls_bind_info = 'ssl crt /etc/haproxy/haproxy-internal.pem' if kolla_enable_tls_internal|bool else '' %} +{%- set internal_tls_bind_info = 'ssl crt /etc/haproxy/certificates/haproxy-internal.pem' if kolla_enable_tls_internal|bool else '' %} {%- set internal_tls_bind_info = "%s %s" % (internal_tls_bind_info, haproxy_http2_protocol) if kolla_enable_tls_internal|bool and haproxy_enable_http2|bool else internal_tls_bind_info %} {%- macro userlist_macro(service_name, auth_user, auth_pass) %} @@ -10,7 +10,7 @@ userlist {{ service_name }}-user {% endmacro %} {%- macro frontend_macro(service_name, service_port, service_mode, external, - frontend_http_extra, frontend_tcp_extra) %} + frontend_http_extra, frontend_redirect_extra, frontend_tcp_extra) %} frontend {{ service_name }}_front {% if service_mode == 'redirect' %} mode http @@ -50,7 +50,10 @@ frontend {{ service_name }}_front {{ "bind %s:%s %s"|e|format(vip_address, service_port, tls_option)|trim() }} {# Redirect mode sets a redirect scheme instead of a backend #} {% if service_mode == 'redirect' %} - redirect scheme https code 301 if !{ ssl_fc } + redirect scheme https code 301 if !{ ssl_fc } !{ path_reg ^/.well-known/acme-challenge/.+ } + {% for redirect_option in frontend_redirect_extra %} + {{ redirect_option }} + {% endfor %} {% else %} default_backend {{ service_name }}_back {% endif %} @@ -133,6 +136,7 @@ backend {{ service_name }}_back {% set frontend_tcp_extra = haproxy_service.frontend_tcp_extra|default([]) + haproxy_frontend_tcp_extra %} {% set backend_tcp_extra = haproxy_service.backend_tcp_extra|default([]) %} {% set frontend_http_extra = haproxy_service.frontend_http_extra|default([]) + haproxy_frontend_http_extra %} + {% set frontend_redirect_extra = haproxy_service.frontend_redirect_extra|default([]) + haproxy_frontend_redirect_extra %} {% set backend_http_extra = haproxy_service.backend_http_extra|default([]) %} {% set tls_backend = haproxy_service.tls_backend|default(false) %} {# Allow for basic auth #} @@ -144,7 +148,7 @@ backend {{ service_name }}_back {% if with_frontend %} {% if not (external|bool and haproxy_single_external_frontend|bool and mode == 'http') %} {{ frontend_macro(haproxy_name, haproxy_service.port, mode, external, - frontend_http_extra, frontend_tcp_extra) }} + frontend_http_extra, frontend_redirect_extra, frontend_tcp_extra) }} {% endif %} {% endif %} {# Redirect (to https) is a special case, as it does not include a backend #} diff --git a/ansible/roles/horizon/defaults/main.yml b/ansible/roles/horizon/defaults/main.yml index 80bab28b01..ca60afac1c 100644 --- a/ansible/roles/horizon/defaults/main.yml +++ b/ansible/roles/horizon/defaults/main.yml @@ -49,6 +49,8 @@ horizon_services: external: false port: "{{ horizon_port }}" listen_port: "{{ horizon_listen_port }}" + frontend_redirect_extra: + - "use_backend acme_client_back if { path_reg ^/.well-known/acme-challenge/.+ }" horizon_external: enabled: "{{ enable_horizon }}" mode: "http" @@ -68,6 +70,8 @@ horizon_services: external_fqdn: "{{ horizon_external_fqdn }}" port: "{{ horizon_port }}" listen_port: "{{ horizon_listen_port }}" + frontend_redirect_extra: + - "use_backend acme_client_back if { path_reg ^/.well-known/acme-challenge/.+ }" acme_client: enabled: "{{ enable_horizon }}" with_frontend: false diff --git a/ansible/roles/letsencrypt/defaults/main.yml b/ansible/roles/letsencrypt/defaults/main.yml new file mode 100644 index 0000000000..4d41fe9d08 --- /dev/null +++ b/ansible/roles/letsencrypt/defaults/main.yml @@ -0,0 +1,60 @@ +--- +letsencrypt_services: + letsencrypt-lego: + container_name: letsencrypt_lego + group: letsencrypt-lego + enabled: true + image: "{{ letsencrypt_lego_image_full }}" + volumes: "{{ letsencrypt_lego_default_volumes + letsencrypt_lego_extra_volumes }}" + dimensions: "{{ letsencrypt_lego_dimensions }}" + letsencrypt-webserver: + container_name: letsencrypt_webserver + group: letsencrypt-webserver + enabled: true + image: "{{ letsencrypt_webserver_image_full }}" + volumes: "{{ letsencrypt_webserver_default_volumes + letsencrypt_webserver_extra_volumes }}" + dimensions: "{{ letsencrypt_webserver_dimensions }}" + + +############## +# LetsEncrypt +############## +letsencrypt_tag: "{{ openstack_tag }}" +letsencrypt_logging_debug: "{{ openstack_logging_debug }}" + +letsencrypt_lego_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/letsencrypt-lego" +letsencrypt_lego_tag: "{{ letsencrypt_tag }}" +letsencrypt_lego_image_full: "{{ letsencrypt_lego_image }}:{{ letsencrypt_lego_tag }}" + +letsencrypt_webserver_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/letsencrypt-webserver" +letsencrypt_webserver_tag: "{{ letsencrypt_tag }}" +letsencrypt_webserver_image_full: "{{ letsencrypt_webserver_image }}:{{ letsencrypt_webserver_tag }}" + +letsencrypt_lego_dimensions: "{{ default_container_dimensions }}" +letsencrypt_webserver_dimensions: "{{ default_container_dimensions }}" + +letsencrypt_lego_default_volumes: + - "{{ node_config_directory }}/letsencrypt-lego/:{{ container_config_directory }}/:ro" + - "/etc/localtime:/etc/localtime:ro" + - "letsencrypt:/etc/letsencrypt" + - "kolla_logs:/var/log/kolla/" +letsencrypt_lego_extra_volumes: "{{ default_extra_volumes }}" + +letsencrypt_webserver_default_volumes: + - "{{ node_config_directory }}/letsencrypt-webserver/:{{ container_config_directory }}/:ro" + - "/etc/localtime:/etc/localtime:ro" + - "letsencrypt:/etc/letsencrypt" + - "kolla_logs:/var/log/kolla/" +letsencrypt_webserver_extra_volumes: "{{ default_extra_volumes }}" + +letsencrypt_cert_server: "https://acme-v02.api.letsencrypt.org/directory" +# attempt to renew Let's Encrypt certificate every 4 hours +letsencrypt_cron_renew_schedule: "0 */4 * * *" +# The email used for certificate registration and recovery contact. Required. +letsencrypt_email: "" +letsencrypt_cert_valid_days: "30" + +letsencrypt_external_fqdns: + - "{{ kolla_external_fqdn }}" +letsencrypt_internal_fqdns: + - "{{ kolla_internal_fqdn }}" diff --git a/ansible/roles/letsencrypt/handlers/main.yml b/ansible/roles/letsencrypt/handlers/main.yml new file mode 100644 index 0000000000..9e2610c8ce --- /dev/null +++ b/ansible/roles/letsencrypt/handlers/main.yml @@ -0,0 +1,34 @@ +--- +- name: Restart letsencrypt-webserver container + vars: + service_name: "letsencrypt-webserver" + service: "{{ letsencrypt_services[service_name] }}" + become: true + kolla_docker: + action: "recreate_or_restart_container" + common_options: "{{ docker_common_options }}" + name: "{{ service.container_name }}" + image: "{{ service.image }}" + volumes: "{{ service.volumes }}" + dimensions: "{{ service.dimensions }}" + healthcheck: "{{ service.healthcheck | default(omit) }}" + environment: "{{ service.environment | default(omit) }}" + when: + - kolla_action != "config" + +- name: Restart letsencrypt-lego container + vars: + service_name: "letsencrypt-lego" + service: "{{ letsencrypt_services[service_name] }}" + become: true + kolla_docker: + action: "recreate_or_restart_container" + common_options: "{{ docker_common_options }}" + name: "{{ service.container_name }}" + image: "{{ service.image }}" + volumes: "{{ service.volumes }}" + dimensions: "{{ service.dimensions }}" + healthcheck: "{{ service.healthcheck | default(omit) }}" + environment: "{{ service.environment | default(omit) }}" + when: + - kolla_action != "config" diff --git a/ansible/roles/letsencrypt/tasks/check-containers.yml b/ansible/roles/letsencrypt/tasks/check-containers.yml new file mode 100644 index 0000000000..6ca67c510c --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/check-containers.yml @@ -0,0 +1,18 @@ +--- +- name: Check LetsEncrypt containers + become: true + kolla_docker: + action: "compare_container" + common_options: "{{ docker_common_options }}" + name: "{{ item.value.container_name }}" + image: "{{ item.value.image }}" + volumes: "{{ item.value.volumes }}" + dimensions: "{{ item.value.dimensions }}" + healthcheck: "{{ item.value.healthcheck | default(omit) }}" + environment: "{{ item.value.environment | default(omit) }}" + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ letsencrypt_services }}" + notify: + - "Restart {{ item.key }} container" diff --git a/ansible/roles/letsencrypt/tasks/config.yml b/ansible/roles/letsencrypt/tasks/config.yml new file mode 100644 index 0000000000..44011ee7eb --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/config.yml @@ -0,0 +1,66 @@ +--- +- name: Ensuring config directories exist + file: + path: "{{ node_config_directory }}/{{ item.key }}" + state: "directory" + owner: "{{ config_owner_user }}" + group: "{{ config_owner_group }}" + mode: "0770" + become: true + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ letsencrypt_services }}" + +- name: Copying over config.json files for services + template: + src: "{{ item.key }}.json.j2" + dest: "{{ node_config_directory }}/{{ item.key }}/config.json" + mode: "0660" + become: true + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ letsencrypt_services }}" + notify: + - "Restart {{ item.key }} container" + +- name: Copying over letsencrypt-webserver.conf + vars: + service: "{{ letsencrypt_services['letsencrypt-webserver'] }}" + become: true + template: + src: "{{ item }}" + dest: "{{ node_config_directory }}/letsencrypt-webserver/letsencrypt-webserver.conf" + mode: "0660" + with_first_found: + - "{{ node_custom_config }}/letsencrypt/{{ inventory_hostname }}/letsencrypt-webserver.conf" + - "{{ node_custom_config }}/letsencrypt/letsencrypt-webserver.conf" + - "letsencrypt-webserver.conf.j2" + when: + - inventory_hostname in groups[service.group] + - service.enabled | bool + notify: + - Restart letsencrypt-webserver container + +- name: Copying files for letsencrypt-lego + vars: + service: "{{ letsencrypt_services['letsencrypt-lego'] }}" + template: + src: "{{ item.src }}" + dest: "{{ node_config_directory }}/letsencrypt-lego/{{ item.dest }}" + mode: "0660" + become: true + with_items: + - { src: "crontab.j2", dest: "crontab" } + - { src: "id_rsa.j2", dest: "id_rsa" } + - { src: "letsencrypt-lego-run.sh.j2", dest: "letsencrypt-lego-run.sh" } + when: + - inventory_hostname in groups[service.group] + - service.enabled | bool + notify: + - Restart letsencrypt-lego container + +- include_tasks: copy-certs.yml + when: + - kolla_copy_ca_into_containers | bool diff --git a/ansible/roles/letsencrypt/tasks/config_validate.yml b/ansible/roles/letsencrypt/tasks/config_validate.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/config_validate.yml @@ -0,0 +1 @@ +--- diff --git a/ansible/roles/letsencrypt/tasks/copy-certs.yml b/ansible/roles/letsencrypt/tasks/copy-certs.yml new file mode 100644 index 0000000000..567b23612e --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/copy-certs.yml @@ -0,0 +1,6 @@ +--- +- name: "Copy certificates and keys for {{ project_name }}" + import_role: + role: service-cert-copy + vars: + project_services: "{{ letsencrypt_services }}" diff --git a/ansible/roles/letsencrypt/tasks/deploy-containers.yml b/ansible/roles/letsencrypt/tasks/deploy-containers.yml new file mode 100644 index 0000000000..eb24ab5c7a --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/deploy-containers.yml @@ -0,0 +1,2 @@ +--- +- import_tasks: check-containers.yml diff --git a/ansible/roles/letsencrypt/tasks/deploy.yml b/ansible/roles/letsencrypt/tasks/deploy.yml new file mode 100644 index 0000000000..49edff81e3 --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/deploy.yml @@ -0,0 +1,7 @@ +--- +- import_tasks: config.yml + +- import_tasks: check-containers.yml + +- name: Flush handlers + meta: flush_handlers diff --git a/ansible/roles/letsencrypt/tasks/loadbalancer.yml b/ansible/roles/letsencrypt/tasks/loadbalancer.yml new file mode 100644 index 0000000000..a9a2a5c4bc --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/loadbalancer.yml @@ -0,0 +1,7 @@ +--- +- name: "Configure loadbalancer for {{ project_name }}" + import_role: + name: loadbalancer-config + vars: + project_services: "{{ letsencrypt_services }}" + tags: always diff --git a/ansible/roles/letsencrypt/tasks/main.yml b/ansible/roles/letsencrypt/tasks/main.yml new file mode 100644 index 0000000000..bc5d1e6257 --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/main.yml @@ -0,0 +1,2 @@ +--- +- include_tasks: "{{ kolla_action }}.yml" diff --git a/ansible/roles/letsencrypt/tasks/precheck.yml b/ansible/roles/letsencrypt/tasks/precheck.yml new file mode 100644 index 0000000000..6ad18cb535 --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/precheck.yml @@ -0,0 +1,33 @@ +--- +- name: Get container facts + become: true + kolla_container_facts: + container_engine: "{{ kolla_container_engine }}" + name: + - letsencrypt_webserver + register: container_facts + +- name: Checking free port for LetsEncrypt server + vars: + service: "{{ letsencrypt_services['letsencrypt-webserver'] }}" + wait_for: + host: "{{ api_interface_address }}" + port: "{{ letsencrypt_webserver_port }}" + connect_timeout: 1 + timeout: 1 + state: stopped + when: + - container_facts['letsencrypt_webserver'] is not defined + - inventory_hostname in groups[service.group] + - service.enabled | bool + +- name: Validating letsencrypt email variable + run_once: true + vars: + replace: "valid" + assert: + that: letsencrypt_email | regex_replace('.*@.*$', replace) == "valid" + fail_msg: "Letsencrypt contact email value didn't pass validation." + when: + - enable_letsencrypt | bool + - kolla_enable_tls_external | bool diff --git a/ansible/roles/letsencrypt/tasks/pull.yml b/ansible/roles/letsencrypt/tasks/pull.yml new file mode 100644 index 0000000000..03283078d6 --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/pull.yml @@ -0,0 +1,11 @@ +--- +- name: Pulling LetsEncrypt images + become: true + kolla_docker: + action: "pull_image" + common_options: "{{ docker_common_options }}" + image: "{{ item.value.image }}" + when: + - inventory_hostname in groups[item.value.group] + - item.value.enabled | bool + with_dict: "{{ letsencrypt_services }}" diff --git a/ansible/roles/letsencrypt/tasks/reconfigure.yml b/ansible/roles/letsencrypt/tasks/reconfigure.yml new file mode 100644 index 0000000000..5b10a7e111 --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/reconfigure.yml @@ -0,0 +1,2 @@ +--- +- import_tasks: deploy.yml diff --git a/ansible/roles/letsencrypt/tasks/stop.yml b/ansible/roles/letsencrypt/tasks/stop.yml new file mode 100644 index 0000000000..9fbda55f16 --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/stop.yml @@ -0,0 +1,6 @@ +--- +- import_role: + role: service-stop + vars: + project_services: "{{ letsencrypt_services }}" + service_name: "{{ project_name }}" diff --git a/ansible/roles/letsencrypt/tasks/upgrade.yml b/ansible/roles/letsencrypt/tasks/upgrade.yml new file mode 100644 index 0000000000..5b10a7e111 --- /dev/null +++ b/ansible/roles/letsencrypt/tasks/upgrade.yml @@ -0,0 +1,2 @@ +--- +- import_tasks: deploy.yml diff --git a/ansible/roles/letsencrypt/templates/crontab.j2 b/ansible/roles/letsencrypt/templates/crontab.j2 new file mode 100644 index 0000000000..83209f5b46 --- /dev/null +++ b/ansible/roles/letsencrypt/templates/crontab.j2 @@ -0,0 +1,8 @@ +PATH=/usr/local/bin:/usr/bin:/bin + +{% if kolla_external_vip_address != kolla_internal_vip_address and kolla_external_fqdn != kolla_external_vip_address %} +{{ letsencrypt_cron_renew_schedule }} /usr/bin/letsencrypt-certificates --external --fqdns {% for fqdn in letsencrypt_external_fqdns %}{{ fqdn }}{% if not loop.last %},{% endif %}{% endfor %} --days {{ letsencrypt_cert_valid_days }} --port {{ letsencrypt_webserver_port }} --mail {{ letsencrypt_email }} --acme {{ letsencrypt_cert_server }} --vips {% if not kolla_same_external_internal_vip %}{{ kolla_external_vip_address }},{% endif %}{{ kolla_internal_vip_address }} --haproxies-ssh {% for host in groups['loadbalancer'] %}{{ 'api' | kolla_address(host) | put_address_in_context('url') }}:{{ haproxy_ssh_port }}{% if not loop.last %},{% endif %}{% endfor %} 2>&1 | tee -a /var/log/kolla/letsencrypt/letsencrypt-lego.log +{% endif %} +{% if kolla_external_vip_address == kolla_internal_vip_address and kolla_internal_fqdn != kolla_internal_vip_address %} +{{ letsencrypt_cron_renew_schedule }} /usr/bin/letsencrypt-certificates --internal --fqdns {% for fqdn in letsencrypt_internal_fqdns %}{{ fqdn }}{% if not loop.last %},{% endif %}{% endfor %} --days {{ letsencrypt_cert_valid_days }} --port {{ letsencrypt_webserver_port }} --mail {{ letsencrypt_email }} --acme {{ letsencrypt_cert_server }} --vips {% if not kolla_same_external_internal_vip %}{{ kolla_external_vip_address }},{% endif %}{{ kolla_internal_vip_address }} --haproxies-ssh {% for host in groups['loadbalancer'] %}{{ 'api' | kolla_address(host) | put_address_in_context('url') }}:{{ haproxy_ssh_port }}{% if not loop.last %},{% endif %}{% endfor %} 2>&1 | tee -a /var/log/kolla/letsencrypt/letsencrypt-lego.log +{% endif %} diff --git a/ansible/roles/letsencrypt/templates/id_rsa.j2 b/ansible/roles/letsencrypt/templates/id_rsa.j2 new file mode 100644 index 0000000000..9e42a132a3 --- /dev/null +++ b/ansible/roles/letsencrypt/templates/id_rsa.j2 @@ -0,0 +1 @@ +{{ haproxy_ssh_key.private_key }} diff --git a/ansible/roles/letsencrypt/templates/letsencrypt-lego-run.sh.j2 b/ansible/roles/letsencrypt/templates/letsencrypt-lego-run.sh.j2 new file mode 100644 index 0000000000..3f1282f80c --- /dev/null +++ b/ansible/roles/letsencrypt/templates/letsencrypt-lego-run.sh.j2 @@ -0,0 +1,12 @@ +#!/bin/bash + +{% set cron_cmd = 'cron -f' if kolla_base_distro in ['ubuntu', 'debian'] else 'crond -s -n' %} + +{% if kolla_external_vip_address != kolla_internal_vip_address and kolla_external_fqdn != kolla_external_vip_address %} +/usr/bin/letsencrypt-certificates --external --fqdns {% for fqdn in letsencrypt_external_fqdns %}{{ fqdn }}{% if not loop.last %},{% endif %}{% endfor %} --days {{ letsencrypt_cert_valid_days }} --port {{ letsencrypt_webserver_port }} --mail {{ letsencrypt_email }} --acme {{ letsencrypt_cert_server }} --vips {% if not kolla_same_external_internal_vip %}{{ kolla_external_vip_address }},{% endif %}{{ kolla_internal_vip_address }} --haproxies-ssh {% for host in groups['loadbalancer'] %}{{ 'api' | kolla_address(host) | put_address_in_context('url') }}:{{ haproxy_ssh_port }}{% if not loop.last %},{% endif %}{% endfor %} 2>&1 | tee -a /var/log/kolla/letsencrypt/letsencrypt-lego.log +{% endif %} +{% if kolla_external_vip_address == kolla_internal_vip_address and kolla_internal_fqdn != kolla_internal_vip_address %} +/usr/bin/letsencrypt-certificates --internal --fqdns {% for fqdn in letsencrypt_internal_fqdns %}{{ fqdn }}{% if not loop.last %},{% endif %}{% endfor %} --days {{ letsencrypt_cert_valid_days }} --port {{ letsencrypt_webserver_port }} --mail {{ letsencrypt_email }} --acme {{ letsencrypt_cert_server }} --vips {% if not kolla_same_external_internal_vip %}{{ kolla_external_vip_address }},{% endif %}{{ kolla_internal_vip_address }} --haproxies-ssh {% for host in groups['loadbalancer'] %}{{ 'api' | kolla_address(host) | put_address_in_context('url') }}:{{ haproxy_ssh_port }}{% if not loop.last %},{% endif %}{% endfor %} 2>&1 | tee -a /var/log/kolla/letsencrypt/letsencrypt-lego.log +{% endif %} + +{{ cron_cmd }} diff --git a/ansible/roles/letsencrypt/templates/letsencrypt-lego.json.j2 b/ansible/roles/letsencrypt/templates/letsencrypt-lego.json.j2 new file mode 100644 index 0000000000..174f20bdad --- /dev/null +++ b/ansible/roles/letsencrypt/templates/letsencrypt-lego.json.j2 @@ -0,0 +1,26 @@ +{% set cron_cmd = 'cron -f' if kolla_base_distro in ['ubuntu', 'debian'] else 'crond -s -n' %} +{% set cron_path = '/var/spool/cron/crontabs/root' if kolla_base_distro in ['ubuntu', 'debian'] else '/var/spool/cron/root' %} +{ + "command": "/usr/local/bin/letsencrypt-lego-run.sh", + "config_files": [ + { + "source": "{{ container_config_directory }}/letsencrypt-lego-run.sh", + "dest": "/usr/local/bin/letsencrypt-lego-run.sh", + "owner": "root", + "perm": "0700" + }, + { + "source": "{{ container_config_directory }}/crontab", + "dest": "{{ cron_path }}", + "owner": "root", + "perm": "0600" + }, + { + "source": "{{ container_config_directory }}/id_rsa", + "dest": "/var/lib/letsencrypt/.ssh/id_rsa", + "owner": "letsencrypt", + "perm": "0600" + } + ] +} + diff --git a/ansible/roles/letsencrypt/templates/letsencrypt-webserver.conf.j2 b/ansible/roles/letsencrypt/templates/letsencrypt-webserver.conf.j2 new file mode 100644 index 0000000000..f2555caad9 --- /dev/null +++ b/ansible/roles/letsencrypt/templates/letsencrypt-webserver.conf.j2 @@ -0,0 +1,19 @@ +Listen {{ api_interface_address }}:8081 + +ServerSignature Off +ServerTokens Prod +TraceEnable off +KeepAliveTimeout 60 + + + DocumentRoot /etc/letsencrypt/http-01 + ErrorLog "/var/log/kolla/letsencrypt/letsencrypt-webserver-error.log" + CustomLog "/var/log/kolla/letsencrypt/letsencrypt-webserver-access.log" common + + + Options None + AllowOverride None + Require all granted + + + diff --git a/ansible/roles/letsencrypt/templates/letsencrypt-webserver.json.j2 b/ansible/roles/letsencrypt/templates/letsencrypt-webserver.json.j2 new file mode 100644 index 0000000000..5284241643 --- /dev/null +++ b/ansible/roles/letsencrypt/templates/letsencrypt-webserver.json.j2 @@ -0,0 +1,14 @@ +{% set letsencrypt_apache_dir = 'apache2/conf-enabled' if kolla_base_distro in ['ubuntu', 'debian'] else 'httpd/conf.d' %} +{% set apache_binary = 'apache2' if kolla_base_distro in ['ubuntu', 'debian'] else 'httpd' %} + +{ + "command": "/usr/sbin/{{ apache_binary }} -DFOREGROUND", + "config_files": [ + { + "source": "{{ container_config_directory }}/letsencrypt-webserver.conf", + "dest": "/etc/{{ letsencrypt_apache_dir }}/letsencrypt-webserver.conf", + "owner": "letsencrypt", + "perm": "0600" + } + ] +} diff --git a/ansible/roles/letsencrypt/vars/main.yml b/ansible/roles/letsencrypt/vars/main.yml new file mode 100644 index 0000000000..66b02925f9 --- /dev/null +++ b/ansible/roles/letsencrypt/vars/main.yml @@ -0,0 +1,2 @@ +--- +project_name: "letsencrypt" diff --git a/ansible/roles/loadbalancer/defaults/main.yml b/ansible/roles/loadbalancer/defaults/main.yml index bb38946a74..787ae7cb2c 100644 --- a/ansible/roles/loadbalancer/defaults/main.yml +++ b/ansible/roles/loadbalancer/defaults/main.yml @@ -26,6 +26,14 @@ loadbalancer_services: privileged: True volumes: "{{ keepalived_default_volumes + keepalived_extra_volumes }}" dimensions: "{{ keepalived_dimensions }}" + haproxy-ssh: + container_name: "haproxy_ssh" + group: loadbalancer + enabled: "{{ enable_letsencrypt | bool }}" + image: "{{ haproxy_ssh_image_full }}" + volumes: "{{ haproxy_ssh_default_volumes }}" + dimensions: "{{ haproxy_ssh_dimensions }}" + healthcheck: "{{ haproxy_ssh_healthcheck }}" #################### @@ -43,6 +51,10 @@ proxysql_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker proxysql_tag: "{{ openstack_tag }}" proxysql_image_full: "{{ proxysql_image }}:{{ proxysql_tag }}" +haproxy_ssh_image: "{{ docker_registry ~ '/' if docker_registry else '' }}{{ docker_namespace }}/haproxy-ssh" +haproxy_ssh_tag: "{{ haproxy_tag }}" +haproxy_ssh_image_full: "{{ haproxy_ssh_image }}:{{ haproxy_ssh_tag }}" + syslog_server: "{{ api_interface_address }}" syslog_haproxy_facility: "local1" @@ -59,6 +71,7 @@ haproxy_defaults_max_connections: 10000 haproxy_dimensions: "{{ default_container_dimensions }}" proxysql_dimensions: "{{ default_container_dimensions }}" keepalived_dimensions: "{{ default_container_dimensions }}" +haproxy_ssh_dimensions: "{{ default_container_dimensions }}" haproxy_enable_healthchecks: "{{ enable_container_healthchecks }}" haproxy_healthcheck_interval: "{{ default_container_healthcheck_interval }}" @@ -86,11 +99,27 @@ proxysql_healthcheck: test: "{% if proxysql_enable_healthchecks | bool %}{{ proxysql_healthcheck_test }}{% else %}NONE{% endif %}" timeout: "{{ proxysql_healthcheck_timeout }}" +haproxy_ssh_enable_healthchecks: "{{ enable_container_healthchecks }}" +haproxy_ssh_healthcheck_interval: "{{ default_container_healthcheck_interval }}" +haproxy_ssh_healthcheck_retries: "{{ default_container_healthcheck_retries }}" +haproxy_ssh_healthcheck_start_period: "{{ default_container_healthcheck_start_period }}" +haproxy_ssh_healthcheck_test: ["CMD-SHELL", "healthcheck_listen sshd {{ haproxy_ssh_port }}"] +haproxy_ssh_healthcheck_timeout: "{{ default_container_healthcheck_timeout }}" +haproxy_ssh_healthcheck: + interval: "{{ haproxy_ssh_healthcheck_interval }}" + retries: "{{ haproxy_ssh_healthcheck_retries }}" + start_period: "{{ haproxy_ssh_healthcheck_start_period }}" + test: "{% if haproxy_ssh_enable_healthchecks | bool %}{{ haproxy_ssh_healthcheck_test }}{% else %}NONE{% endif %}" + timeout: "{{ haproxy_ssh_healthcheck_timeout }}" + + haproxy_default_volumes: - "{{ node_config_directory }}/haproxy/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" - "{{ '/etc/timezone:/etc/timezone:ro' if ansible_facts.os_family == 'Debian' else '' }}" - "haproxy_socket:/var/lib/kolla/haproxy/" + - "letsencrypt_certificates:/etc/haproxy/certificates" + proxysql_default_volumes: - "{{ node_config_directory }}/proxysql/:{{ container_config_directory }}/:ro" - "/etc/localtime:/etc/localtime:ro" @@ -105,6 +134,13 @@ keepalived_default_volumes: - "/lib/modules:/lib/modules:ro" - "{{ 'haproxy_socket:/var/lib/kolla/haproxy/' if enable_haproxy | bool else '' }}" - "{{ 'proxysql_socket:/var/lib/kolla/proxysql/' if enable_proxysql | bool else '' }}" +haproxy_ssh_default_volumes: + - "{{ node_config_directory }}/haproxy-ssh/:{{ container_config_directory }}/:ro" + - "/etc/localtime:/etc/localtime:ro" + - "{{ '/etc/timezone:/etc/timezone:ro' if ansible_facts.os_family == 'Debian' else '' }}" + - "haproxy_socket:/var/lib/kolla/haproxy/" + - "{{ 'letsencrypt:/etc/letsencrypt' if enable_letsencrypt | bool else omit }}" + - "{{ 'letsencrypt_certificates:/etc/haproxy/certificates' if enable_letsencrypt | bool else omit }}" haproxy_extra_volumes: "{{ default_extra_volumes }}" proxysql_extra_volumes: "{{ default_extra_volumes }}" @@ -143,8 +179,7 @@ haproxy_defaults_balance: "roundrobin" haproxy_host_ipv4_tcp_retries2: "KOLLA_UNSET" # HAProxy socket admin permissions enable -haproxy_socket_level_admin: "no" - +haproxy_socket_level_admin: "{{ enable_letsencrypt | bool }}" kolla_externally_managed_cert: False # Allow to disable keepalived tracking script (e.g. for single node environments diff --git a/ansible/roles/loadbalancer/handlers/main.yml b/ansible/roles/loadbalancer/handlers/main.yml index 6aeb61e235..f78e3d59c5 100644 --- a/ansible/roles/loadbalancer/handlers/main.yml +++ b/ansible/roles/loadbalancer/handlers/main.yml @@ -333,3 +333,19 @@ - service.enabled | bool listen: - Wait for virtual IP to appear + +- name: Restart haproxy-ssh container + vars: + service_name: "haproxy-ssh" + service: "{{ loadbalancer_services[service_name] }}" + become: true + kolla_docker: + action: "recreate_or_restart_container" + common_options: "{{ docker_common_options }}" + name: "{{ service.container_name }}" + image: "{{ service.image }}" + volumes: "{{ service.volumes | reject('equalto', '') | list }}" + dimensions: "{{ service.dimensions }}" + healthcheck: "{{ service.healthcheck | default(omit) }}" + when: + - kolla_action != "config" diff --git a/ansible/roles/loadbalancer/tasks/config.yml b/ansible/roles/loadbalancer/tasks/config.yml index 7c81b4c8be..c2270cbee0 100644 --- a/ansible/roles/loadbalancer/tasks/config.yml +++ b/ansible/roles/loadbalancer/tasks/config.yml @@ -80,8 +80,10 @@ become: true with_dict: "{{ loadbalancer_services }}" when: + - keepalived_track_script_enabled | bool - inventory_hostname in groups[service.group] - item.key != 'keepalived' + - item.key != 'haproxy-ssh' - not item.value.enabled | bool or not inventory_hostname in groups[item.value.group] - service.enabled | bool @@ -102,6 +104,7 @@ - inventory_hostname in groups[service.group] - inventory_hostname in groups[item.value.group] - item.key != 'keepalived' + - item.key != 'haproxy-ssh' - item.value.enabled | bool - service.enabled | bool notify: @@ -214,6 +217,7 @@ mode: "0660" become: true when: + - not enable_letsencrypt | bool - kolla_enable_tls_external | bool - not kolla_externally_managed_cert | bool - inventory_hostname in groups[service.group] @@ -232,6 +236,7 @@ mode: "0660" become: true when: + - not enable_letsencrypt | bool - kolla_enable_tls_internal | bool - not kolla_externally_managed_cert | bool - inventory_hostname in groups[service.group] @@ -280,3 +285,20 @@ - "proxysql/proxysql_run.sh.j2" notify: - Restart proxysql container + +- name: Copying files for haproxy-ssh + vars: + haproxy_ssh: "{{ loadbalancer_services['haproxy-ssh'] }}" + template: + src: "{{ item.src }}" + dest: "{{ node_config_directory }}/haproxy-ssh/{{ item.dest }}" + mode: "0600" + become: true + with_items: + - { src: "haproxy-ssh/sshd_config.j2", dest: "sshd_config" } + - { src: "haproxy-ssh/id_rsa.pub", dest: "id_rsa.pub" } + when: + - inventory_hostname in groups[haproxy_ssh.group] + - haproxy_ssh.enabled | bool + notify: + - Restart haproxy-ssh container diff --git a/ansible/roles/loadbalancer/tasks/precheck.yml b/ansible/roles/loadbalancer/tasks/precheck.yml index 0ee8232edb..18b822cc11 100644 --- a/ansible/roles/loadbalancer/tasks/precheck.yml +++ b/ansible/roles/loadbalancer/tasks/precheck.yml @@ -63,6 +63,7 @@ changed_when: false when: - not kolla_externally_managed_cert | bool + - not enable_letsencrypt | bool - kolla_enable_tls_external | bool - name: Assert that external haproxy certificate exists @@ -72,6 +73,7 @@ fail_msg: "External haproxy certificate file is not found. It is configured via 'kolla_external_fqdn_cert'" when: - not kolla_externally_managed_cert | bool + - not enable_letsencrypt | bool - kolla_enable_tls_external | bool - name: Checking if internal haproxy certificate exists @@ -83,6 +85,7 @@ changed_when: false when: - not kolla_externally_managed_cert | bool + - not enable_letsencrypt | bool - kolla_enable_tls_internal | bool - name: Assert that internal haproxy certificate exists @@ -92,6 +95,7 @@ fail_msg: "Internal haproxy certificate file is not found. It is configured via 'kolla_internal_fqdn_cert'" when: - not kolla_externally_managed_cert | bool + - not enable_letsencrypt | bool - kolla_enable_tls_internal | bool - name: Checking the kolla_external_vip_interface is present diff --git a/ansible/roles/loadbalancer/templates/haproxy-ssh/haproxy-ssh.json.j2 b/ansible/roles/loadbalancer/templates/haproxy-ssh/haproxy-ssh.json.j2 new file mode 100644 index 0000000000..418be139d7 --- /dev/null +++ b/ansible/roles/loadbalancer/templates/haproxy-ssh/haproxy-ssh.json.j2 @@ -0,0 +1,17 @@ +{ + "command": "/usr/sbin/sshd -D", + "config_files": [ + { + "source": "{{ container_config_directory }}/sshd_config", + "dest": "/etc/ssh/sshd_config", + "owner": "root", + "perm": "0600" + }, + { + "source": "{{ container_config_directory }}/id_rsa.pub", + "dest": "/var/lib/haproxy/.ssh/authorized_keys", + "owner": "haproxy", + "perm": "0600" + } + ] +} diff --git a/ansible/roles/loadbalancer/templates/haproxy-ssh/id_rsa.pub b/ansible/roles/loadbalancer/templates/haproxy-ssh/id_rsa.pub new file mode 100644 index 0000000000..e7b2ce1c99 --- /dev/null +++ b/ansible/roles/loadbalancer/templates/haproxy-ssh/id_rsa.pub @@ -0,0 +1 @@ +{{ haproxy_ssh_key.public_key }} diff --git a/ansible/roles/loadbalancer/templates/haproxy-ssh/sshd_config.j2 b/ansible/roles/loadbalancer/templates/haproxy-ssh/sshd_config.j2 new file mode 100644 index 0000000000..287fd195a9 --- /dev/null +++ b/ansible/roles/loadbalancer/templates/haproxy-ssh/sshd_config.j2 @@ -0,0 +1,5 @@ +Port {{ haproxy_ssh_port }} +ListenAddress {{ api_interface_address }} + +SyslogFacility AUTHPRIV +UsePAM yes diff --git a/ansible/roles/loadbalancer/templates/haproxy/haproxy.json.j2 b/ansible/roles/loadbalancer/templates/haproxy/haproxy.json.j2 index 7babdf5d3a..adc7e343a8 100644 --- a/ansible/roles/loadbalancer/templates/haproxy/haproxy.json.j2 +++ b/ansible/roles/loadbalancer/templates/haproxy/haproxy.json.j2 @@ -18,7 +18,7 @@ "dest": "/etc/haproxy/services.d", "owner": "root", "perm": "0700" - }, + }{% if kolla_enable_tls_external | bool and not enable_letsencrypt | bool %}, { "source": "{{ container_config_directory }}/external-frontend-map", "dest": "/etc/haproxy/external-frontend-map", @@ -28,17 +28,19 @@ }, { "source": "{{ container_config_directory }}/haproxy.pem", - "dest": "/etc/haproxy/haproxy.pem", - "owner": "root", + "dest": "/etc/haproxy/certificates/haproxy.pem", + "owner": "haproxy", "perm": "0600", "optional": {{ (not kolla_enable_tls_external | bool) | string | lower }} - }, + }{% endif %} + {% if kolla_enable_tls_internal | bool and not enable_letsencrypt | bool %}, { "source": "{{ container_config_directory }}/haproxy-internal.pem", - "dest": "/etc/haproxy/haproxy-internal.pem", - "owner": "root", + "dest": "/etc/haproxy/certificates/haproxy-internal.pem", + "owner": "haproxy", "perm": "0600", "optional": {{ (not kolla_enable_tls_internal | bool) | string | lower }} } + {% endif %} ] } diff --git a/ansible/roles/loadbalancer/templates/haproxy/haproxy_run.sh.j2 b/ansible/roles/loadbalancer/templates/haproxy/haproxy_run.sh.j2 index 91cf78f4a9..692b39e0e3 100644 --- a/ansible/roles/loadbalancer/templates/haproxy/haproxy_run.sh.j2 +++ b/ansible/roles/loadbalancer/templates/haproxy/haproxy_run.sh.j2 @@ -1,9 +1,40 @@ #!/bin/bash -x -# We need to run haproxy with one `-f` for each service, because including an -# entire config directory was not a feature until version 1.7 of HAProxy. -# So, append "-f $cfg" to the haproxy command for each service file. -# This will run haproxy_cmd *exactly once*. +{% if kolla_enable_tls_internal | bool or kolla_enable_tls_external | bool %} +{% if kolla_enable_tls_external | bool %} +if [ ! -e "/etc/haproxy/certificates/haproxy.pem" ]; then + # Generate temporary self-signed cert + # This means external tls is enabled but the certificate was not copied + # to the container - so letsencrypt is enabled + # + # Let's generate certificate to make haproxy happy, lego will + # replace it in a while + ssl_tmp_dir=$(mktemp -d) + openssl req -x509 -newkey rsa:2048 -sha256 -days 1 -nodes -keyout ${ssl_tmp_dir}/haproxy$$.key -out ${ssl_tmp_dir}/haproxy$$.crt -subj "/CN={{ kolla_external_fqdn }}" + cat ${ssl_tmp_dir}/haproxy$$.crt ${ssl_tmp_dir}/haproxy$$.key> /etc/haproxy/certificates/haproxy.pem + rm -rf ${ssl_tmp_dir} + chown haproxy:haproxy /etc/haproxy/certificates/haproxy.pem + chmod 0660 /etc/haproxy/certificates/haproxy.pem +fi +{% endif %} +{% if kolla_enable_tls_internal | bool %} +if [ ! -e "/etc/haproxy/certificates/haproxy-internal.pem" ]; then + # Generate temporary self-signed cert + # This means external tls is enabled but the certificate was not copied + # to the container - so letsencrypt is enabled + # + # Let's generate certificate to make haproxy happy, lego will + # replace it in a while + ssl_tmp_dir=$(mktemp -d) + openssl req -x509 -newkey rsa:2048 -sha256 -days 1 -nodes -keyout ${ssl_tmp_dir}/haproxy-internal$$.key -out ${ssl_tmp_dir}/haproxy-internal$$.crt -subj "/CN={{ kolla_internal_fqdn }}" + cat ${ssl_tmp_dir}/haproxy-internal$$.crt ${ssl_tmp_dir}/haproxy-internal$$.key> /etc/haproxy/certificates/haproxy-internal.pem + rm -rf ${ssl_tmp_dir} + chown haproxy:haproxy /etc/haproxy/certificates/haproxy-internal.pem + chmod 0660 /etc/haproxy/certificates/haproxy-internal.pem +fi +{% endif %} +{% endif %} + find /etc/haproxy/services.d/ -mindepth 1 -print0 | \ xargs -0 -Icfg echo -f cfg | \ xargs /usr/sbin/haproxy -W -db -p /run/haproxy.pid -f /etc/haproxy/haproxy.cfg diff --git a/ansible/site.yml b/ansible/site.yml index 4ba77cdc71..144b608cd5 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -41,6 +41,7 @@ - enable_iscsid_{{ enable_iscsid | bool }} - enable_keystone_{{ enable_keystone | bool }} - enable_kuryr_{{ enable_kuryr | bool }} + - enable_letsencrypt_{{ enable_letsencrypt | bool }} - enable_loadbalancer_{{ enable_loadbalancer | bool }} - enable_magnum_{{ enable_magnum | bool }} - enable_manila_{{ enable_manila | bool }} @@ -200,6 +201,11 @@ tasks_from: loadbalancer tags: keystone when: enable_keystone | bool + - include_role: + name: letsencrypt + tasks_from: loadbalancer + tags: letsencrypt + when: enable_letsencrypt | bool - include_role: name: magnum tasks_from: loadbalancer @@ -340,6 +346,16 @@ - enable_haproxy | bool - kolla_action in ['deploy', 'reconfigure', 'upgrade', 'config'] +- name: Apply role letsencrypt + gather_facts: false + hosts: + - letsencrypt + - '&enable_letsencrypt_True' + serial: '{{ kolla_serial|default("0") }}' + roles: + - { role: letsencrypt, + tags: letsencrypt } + - name: Apply role collectd gather_facts: false hosts: diff --git a/doc/source/admin/tls.rst b/doc/source/admin/tls.rst index f2acb9bb3d..2810ecdc83 100644 --- a/doc/source/admin/tls.rst +++ b/doc/source/admin/tls.rst @@ -288,6 +288,35 @@ disable verification of the backend certificate: .. _admin-tls-generating-a-private-ca: +Generating TLS certificates with Let's Encrypt +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's Encrypt is a free, automated, and open certificate authority. + +To enable OpenStack to deploy the Let's Encrypt container to fetch +certificates from the Let's Encrypt certificate authority, the following +must be configured in ``globals.yml``: + +.. code-block:: yaml + + enable_letsencrypt: "yes" + letsencrypt_email: "" + +The Let's Encrypt container will attempt to renew your certificates every 12 +hours. If the certificates are renewed, they will automatically be deployed +to the HAProxy containers using SSH. + +.. note:: + + If ``letsencrypt_email`` is not valid email, letsencrypt role will + not work correctly. + +.. note:: + + If ``enable_letsencrypt`` is set to true, haproxy's socket will run with + admin access level. This is needed so Let's Encrypt can interact + with HAProxy. + Generating a Private Certificate Authority ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/etc/kolla/globals.yml b/etc/kolla/globals.yml index ca90c96f6b..223ec3bfa6 100644 --- a/etc/kolla/globals.yml +++ b/etc/kolla/globals.yml @@ -270,6 +270,19 @@ workaround_ansible_issue_8743: yes # Please read the docs for more details. #acme_client_servers: [] +#################### +# LetsEncrypt options +#################### +# This option is required for letsencrypt role to work properly. +#letsencrypt_email: "" + +#################### +# LetsEncrypt certificate server options +#################### +#letsencrypt_cert_server: "https://acme-v02.api.letsencrypt.org/directory" +# attempt to renew Let's Encrypt certificate every 12 hours +#letsencrypt_cron_renew_schedule: "0 */12 * * *" + ################ # Region options ################ diff --git a/etc/kolla/passwords.yml b/etc/kolla/passwords.yml index 9b5871a8f5..3bca88f9fc 100644 --- a/etc/kolla/passwords.yml +++ b/etc/kolla/passwords.yml @@ -206,6 +206,10 @@ neutron_ssh_key: private_key: public_key: +haproxy_ssh_key: + private_key: + public_key: + #################### # Gnocchi options #################### diff --git a/kolla_ansible/cmd/genpwd.py b/kolla_ansible/cmd/genpwd.py index e523372e22..55dab376e5 100755 --- a/kolla_ansible/cmd/genpwd.py +++ b/kolla_ansible/cmd/genpwd.py @@ -137,8 +137,9 @@ def main(): # SSH key pair ssh_keys = ['kolla_ssh_key', 'nova_ssh_key', - 'keystone_ssh_key', 'bifrost_ssh_key', 'octavia_amp_ssh_key', - 'neutron_ssh_key'] + 'keystone_ssh_key', 'bifrost_ssh_key', + 'octavia_amp_ssh_key', 'neutron_ssh_key', + 'haproxy_ssh_key'] # If these keys are None, leave them as None blank_keys = ['docker_registry_password'] diff --git a/releasenotes/notes/add-lets-encrypt-intergration-9e5f9846536379af.yaml b/releasenotes/notes/add-lets-encrypt-intergration-9e5f9846536379af.yaml new file mode 100644 index 0000000000..47687f627d --- /dev/null +++ b/releasenotes/notes/add-lets-encrypt-intergration-9e5f9846536379af.yaml @@ -0,0 +1,10 @@ +--- +features: + - Add Lets Encrypt TLS certificate service integration into Openstack + deployment. Enables trusted TLS certificate generation option for + secure communcation with OpenStack HAProxy instances using + ``letsencrypt_email``, ``kolla_internal_fqdn`` and/or + ``kolla_external_fqdn`` is required. One container runs an Apache + ACME client webserver and one runs Lego for certificate retrieval + and renewal. The Lego container starts a cron job which attempts + to renew certificates every 12 hours. diff --git a/tests/deploy.sh b/tests/deploy.sh index 7405d43a30..ca419c9dfa 100755 --- a/tests/deploy.sh +++ b/tests/deploy.sh @@ -6,18 +6,63 @@ set -o errexit # Enable unbuffered output for Ansible in Jenkins. export PYTHONUNBUFFERED=1 +function init_pebble { + + sudo echo "[i] Pulling letsencrypt/pebble" > /tmp/logs/ansible/certificates + sudo docker pull letsencrypt/pebble &>> /tmp/logs/ansible/certificates + + sudo echo "[i] Force removing old pebble container" &>> /tmp/logs/ansible/certificates + sudo docker rm -f pebble &>> /tmp/logs/ansible/certificates + + sudo echo "[i] Run new pebble container" &>> /tmp/logs/ansible/certificates + sudo docker run --name pebble --rm -d -e "PEBBLE_VA_NOSLEEP=1" -e "PEBBLE_VA_ALWAYS_VALID=1" --net=host letsencrypt/pebble &>> /tmp/logs/ansible/certificates + + sudo echo "[i] Wait for pebble container be up" &>> /tmp/logs/ansible/certificates + # wait until pebble starts + while ! sudo docker logs pebble | grep -q "Listening on"; do + sleep 1 + done + sudo echo "[i] Wait for pebble container done" &>> /tmp/logs/ansible/certificates + + sudo echo "[i] Pebble container logs" &>> /tmp/logs/ansible/certificates + sudo docker logs pebble &>> /tmp/logs/ansible/certificates +} + +function pebble_cacert { + + sudo docker cp pebble:/test/certs/pebble.minica.pem /etc/kolla/certificates/ca/pebble-root.crt + sudo curl -k -s -o /etc/kolla/certificates/ca/pebble.crt -v https://127.0.0.1:15000/roots/0 +} + +function certificates { -function deploy { RAW_INVENTORY=/etc/kolla/inventory - source $KOLLA_ANSIBLE_VENV_PATH/bin/activate - #TODO(inc0): Post-deploy complains that /etc/kolla is not writable. Probably we need to include become there - sudo chmod -R 777 /etc/kolla # generate self-signed certificates for the optional internal TLS tests if [[ "$TLS_ENABLED" = "True" ]]; then kolla-ansible -i ${RAW_INVENTORY} -vvv certificates > /tmp/logs/ansible/certificates fi + if [[ "$LE_ENABLED" = "True" ]]; then + init_pebble + pebble_cacert + fi + + #TODO(inc0): Post-deploy complains that /etc/kolla is not writable. Probably we need to include become there + sudo chmod -R 777 /etc/kolla +} + + +function deploy { + + RAW_INVENTORY=/etc/kolla/inventory + source $KOLLA_ANSIBLE_VENV_PATH/bin/activate + + #TODO(inc0): Post-deploy complains that /etc/kolla is not writable. Probably we need to include become there + sudo chmod -R 777 /etc/kolla + + certificates + # Actually do the deployment kolla-ansible -i ${RAW_INVENTORY} -vvv prechecks &> /tmp/logs/ansible/deploy-prechecks kolla-ansible -i ${RAW_INVENTORY} -vvv pull &> /tmp/logs/ansible/pull diff --git a/tests/run.yml b/tests/run.yml index a396c85738..bf52178396 100644 --- a/tests/run.yml +++ b/tests/run.yml @@ -21,11 +21,12 @@ need_build_image: "{{ kolla_build_images | default(false) }}" build_image_tag: "change_{{ zuul.change | default('none') }}" openstack_core_enabled: "{{ openstack_core_enabled }}" - openstack_core_tested: "{{ scenario in ['core', 'cephadm', 'zun', 'cells', 'swift', 'ovn'] }}" + openstack_core_tested: "{{ scenario in ['core', 'cephadm', 'zun', 'cells', 'swift', 'ovn', 'lets-encrypt'] }}" dashboard_enabled: "{{ openstack_core_enabled }}" upper_constraints_file: "{{ ansible_env.HOME }}/src/opendev.org/openstack/requirements/upper-constraints.txt" docker_image_tag_suffix: "{{ '-aarch64' if ansible_architecture == 'aarch64' else '' }}" kolla_ansible_venv_path: "{{ ansible_env.HOME }}/kolla-ansible-venv" + kolla_internal_fqdn: "kolla.example.com" - name: Install dig for Designate testing become: true @@ -46,6 +47,18 @@ vars: disk_type: "{{ 'ceph-lvm' if scenario in ['cephadm'] else scenario }}" + - name: Update /etc/hosts with internal API FQDN + blockinfile: + dest: /etc/hosts + marker: "# {mark} ANSIBLE GENERATED INTERNAL API FQDN" + block: | + {{ kolla_internal_vip_address }} {{ kolla_internal_fqdn }} + 192.0.2.1 pebble + become: True + when: + - scenario == "lets-encrypt" + + - hosts: primary any_errors_fatal: true vars: @@ -397,6 +410,7 @@ chdir: "{{ kolla_ansible_src_dir }}" environment: TLS_ENABLED: "{{ tls_enabled }}" + LE_ENABLED: "{{ le_enabled }}" KOLLA_ANSIBLE_VENV_PATH: "{{ kolla_ansible_venv_path }}" HAS_UPGRADE: "{{ is_upgrade | bool | ternary('yes', 'no') }}" @@ -410,6 +424,7 @@ chdir: "{{ kolla_ansible_src_dir }}" environment: TLS_ENABLED: "{{ tls_enabled }}" + LE_ENABLED: "{{ le_enabled }}" when: dashboard_enabled - name: Run init-core-openstack.sh script diff --git a/tests/setup_gate.sh b/tests/setup_gate.sh index 5ace112365..7dc9c92a96 100755 --- a/tests/setup_gate.sh +++ b/tests/setup_gate.sh @@ -94,6 +94,10 @@ function prepare_images { GATE_IMAGES="^cron,^fluentd,^haproxy,^keepalived,^kolla-toolbox,^mariadb" fi + if [[ $SCENARIO == "lets-encrypt" ]]; then + GATE_IMAGES+=",^letsencrypt,^haproxy" + fi + if [[ $SCENARIO == "prometheus-opensearch" ]]; then GATE_IMAGES="^cron,^fluentd,^grafana,^haproxy,^keepalived,^kolla-toolbox,^mariadb,^memcached,^opensearch,^prometheus,^rabbitmq" fi diff --git a/tests/templates/globals-default.j2 b/tests/templates/globals-default.j2 index 09f3806d9a..86cdb67f42 100644 --- a/tests/templates/globals-default.j2 +++ b/tests/templates/globals-default.j2 @@ -208,3 +208,13 @@ keepalived_track_script_enabled: "no" neutron_modules_extra: - name: 'nf_conntrack_tftp' - name: 'nf_nat_tftp' + +{% if scenario == "lets-encrypt" %} +enable_letsencrypt: "yes" +rabbitmq_enable_tls: "yes" +letsencrypt_email: "usero@openstack.test" +letsencrypt_cert_server: "https://pebble:14000/dir" +kolla_internal_fqdn: "{{ kolla_internal_fqdn }}" +kolla_enable_tls_backend: "no" +kolla_admin_openrc_cacert: "{% raw %}{{ kolla_certificates_dir }}{% endraw %}/ca/pebble.crt" +{% endif %} diff --git a/tests/templates/inventory.j2 b/tests/templates/inventory.j2 index 468e095629..e4c33c08cf 100644 --- a/tests/templates/inventory.j2 +++ b/tests/templates/inventory.j2 @@ -258,6 +258,9 @@ control [venus:children] monitoring +[letsencrypt:children] +loadbalancer + # Additional control implemented here. These groups allow you to control which # services run on which hosts at a per-service level. # @@ -749,3 +752,9 @@ venus [venus-manager:children] venus + +[letsencrypt-webserver:children] +letsencrypt + +[letsencrypt-lego:children] +letsencrypt diff --git a/zuul.d/base.yaml b/zuul.d/base.yaml index 5d2869b273..612a82b093 100644 --- a/zuul.d/base.yaml +++ b/zuul.d/base.yaml @@ -47,6 +47,7 @@ neutron_tenant_network_prefix_length: "24" neutron_tenant_network_dns_server: "8.8.8.8" tls_enabled: false + le_enabled: false configure_swap_size: 0 roles: - zuul: zuul/zuul-jobs @@ -245,3 +246,18 @@ - ^kolla_ansible/ - ^tests/run-hashi-vault.yml - ^tests/test-hashicorp-vault-passwords.sh + +- job: + name: kolla-ansible-lets-encrypt-base + parent: kolla-ansible-base + voting: false + files: + - ^ansible/roles/letsencrypt/ + - ^ansible/roles/loadbalancer/ + - ^tests/test-core-openstack.sh + - ^tests/test-dashboard.sh + - ^tests/deploy.sh + vars: + scenario: lets-encrypt + tls_enabled: true + le_enabled: true diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml index b751d80b15..6b62a95533 100644 --- a/zuul.d/jobs.yaml +++ b/zuul.d/jobs.yaml @@ -405,6 +405,22 @@ vars: base_distro: ubuntu +- job: + name: kolla-ansible-ubuntu-lets-encrypt + parent: kolla-ansible-lets-encrypt-base + nodeset: kolla-ansible-jammy-multi + vars: + base_distro: ubuntu + install_type: source + +- job: + name: kolla-ansible-rocky9-lets-encrypt + parent: kolla-ansible-lets-encrypt-base + nodeset: kolla-ansible-rocky9-multi + vars: + base_distro: rocky + install_type: source + - job: name: kolla-ansible-rocky9-prometheus-opensearch parent: kolla-ansible-prometheus-opensearch-base diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml index c08e09abc7..83fd33ceaf 100644 --- a/zuul.d/project.yaml +++ b/zuul.d/project.yaml @@ -62,6 +62,8 @@ - kolla-ansible-rocky9-upgrade-cephadm - kolla-ansible-ubuntu-upgrade-cephadm - kolla-ansible-rocky9-hashi-vault + - kolla-ansible-ubuntu-lets-encrypt + - kolla-ansible-rocky9-lets-encrypt check-arm64: jobs: - kolla-ansible-debian-aarch64