From f14ba917986c28023fcc7673573d66774b9e29bb Mon Sep 17 00:00:00 2001 From: Dmitriy Rabotyagov Date: Thu, 17 Jun 2021 21:31:50 +0300 Subject: [PATCH] Generate self-signed SSL per listen IP We're providing an option to have an IP address per VIP address. Currently it's used only for creating self-signed SSLs signed with internal CA per each VIP. With follow-up patches that will also allow to provide user certificates per VIP, making possible to cover internal and external endpoints with different non-wildcard certs. Change-Id: I0a9eb7689eb42b50daf5c94c874bb7429b271efe --- defaults/main.yml | 29 +------ handlers/main.yml | 3 +- .../notes/cert_per_ip-e473f853dbe4047d.yaml | 16 ++++ tasks/haproxy_pre_install.yml | 7 +- tasks/haproxy_ssl.yml | 35 --------- tasks/main.yml | 1 - templates/haproxy.cfg.j2 | 2 +- templates/service.j2 | 25 ++---- vars/main.yml | 78 +++++++++++++++++++ 9 files changed, 112 insertions(+), 84 deletions(-) create mode 100644 releasenotes/notes/cert_per_ip-e473f853dbe4047d.yaml delete mode 100644 tasks/haproxy_ssl.yml create mode 100644 vars/main.yml diff --git a/defaults/main.yml b/defaults/main.yml index 03b4b5d..59cf7b1 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -84,10 +84,7 @@ haproxy_bind_on_non_local: False haproxy_ssl: true haproxy_ssl_all_vips: false haproxy_ssl_dh_param: 2048 -haproxy_ssl_cert: /etc/ssl/certs/haproxy.cert -haproxy_ssl_key: /etc/ssl/private/haproxy.key -haproxy_ssl_pem: /etc/ssl/private/haproxy.pem -haproxy_ssl_ca_cert: /etc/ssl/certs/haproxy-ca.pem +haproxy_ssl_cert_path: /etc/haproxy/ssl haproxy_ssl_cipher_suite: "{{ ssl_cipher_suite | default('ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS') }}" haproxy_ssl_bind_options: "force-tlsv12" @@ -142,30 +139,10 @@ haproxy_pki_certs_path: "{{ haproxy_pki_dir ~ '/certs/certs/' }}" haproxy_pki_intermediate_cert_name: "{{ openstack_pki_service_intermediate_cert_name | default('HAProxyIntermediate') }}" haproxy_pki_intermediate_cert_path: "{{ haproxy_pki_dir ~ '/roots/' ~ haproxy_pki_intermediate_cert_name ~ '/certs/' ~ haproxy_pki_intermediate_cert_name ~ '.crt' }}" haproxy_pki_regen_cert: '' -haproxy_pki_certificates: - - name: "haproxy_{{ ansible_facts['hostname'] }}" - provider: ownca - cn: "{{ ansible_facts['hostname'] }}" - san: "{{ 'DNS:' ~ ansible_facts['hostname'] ~ ',DNS:' ~ ansible_facts['fqdn'] ~ ',IP:' ~ haproxy_bind_external_lb_vip_address }}" - signed_by: "{{ haproxy_pki_intermediate_cert_name }}" +haproxy_pki_certificates: "{{ _haproxy_pki_certificates }}" # Installation details for SSL certificates -haproxy_pki_install_certificates: - - src: "{{ haproxy_user_ssl_cert | default(haproxy_pki_certs_path ~ 'haproxy_' ~ ansible_facts['hostname'] ~ '.crt') }}" - dest: "{{ haproxy_ssl_cert }}" - owner: "root" - group: "root" - mode: "0644" - - src: "{{ haproxy_user_ssl_key | default(haproxy_pki_keys_path ~ 'haproxy_' ~ ansible_facts['hostname'] ~ '.key.pem') }}" - dest: "{{ haproxy_ssl_key }}" - owner: "haproxy" - group: "haproxy" - mode: "0600" - - src: "{{ haproxy_user_ssl_ca_cert | default(haproxy_pki_intermediate_cert_path) }}" - dest: "{{ haproxy_ssl_ca_cert }}" - owner: "haproxy" - group: "haproxy" - mode: "0644" +haproxy_pki_install_certificates: "{{ _haproxy_pki_install_certificates }}" # activate letsencrypt option haproxy_ssl_letsencrypt_enable: false diff --git a/handlers/main.yml b/handlers/main.yml index 27d9838..dceed8f 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -15,8 +15,9 @@ - name: regen pem shell: > - cat {{ haproxy_ssl_cert }} {{ haproxy_user_ssl_ca_cert is defined | ternary(haproxy_ssl_ca_cert,'') }} {{ haproxy_ssl_key }} > {{ haproxy_ssl_pem }} + cat {{ haproxy_ssl_cert_path ~ '/haproxy_' ~ ansible_facts['hostname'] ~ '-' ~ item ~ '.crt' }} {{ haproxy_ssl_cert_path ~ '/haproxy_' ~ ansible_facts['hostname'] ~ '-' ~ item ~ '-ca.crt' }} {{ haproxy_ssl_cert_path ~ '/haproxy_' ~ ansible_facts['hostname'] ~ '-' ~ item ~ '.key' }} > {{ haproxy_ssl_cert_path ~ '/haproxy_' ~ ansible_facts['hostname'] ~ '-' ~ item ~ '.pem' }} notify: Reload haproxy + with_items: "{{ _haproxy_tls_vip_binds }}" listen: - cert installed diff --git a/releasenotes/notes/cert_per_ip-e473f853dbe4047d.yaml b/releasenotes/notes/cert_per_ip-e473f853dbe4047d.yaml new file mode 100644 index 0000000..2c1274e --- /dev/null +++ b/releasenotes/notes/cert_per_ip-e473f853dbe4047d.yaml @@ -0,0 +1,16 @@ +--- +deprecations: + - | + The following variables have been deprecated and will have no effect: + + * ``haproxy_ssl_cert_path`` + * ``haproxy_ssl_key`` + * ``haproxy_ssl_pem`` + * ``haproxy_ssl_ca_cert`` + + These variables were responsible for the path haproxy looked for + certificates on the destination hosts. + + Variables were replaced in favor of ``haproxy_ssl_cert_path`` since the exact + path to certificates will be dynamically set based on the VIP that is used + for the frontend diff --git a/tasks/haproxy_pre_install.yml b/tasks/haproxy_pre_install.yml index 41a2c9d..5f4d4a2 100644 --- a/tasks/haproxy_pre_install.yml +++ b/tasks/haproxy_pre_install.yml @@ -45,6 +45,9 @@ - name: Create haproxy conf.d dir file: - path: "/etc/haproxy/conf.d" + path: "{{ item }}" state: directory - mode: "0755" \ No newline at end of file + mode: "0755" + with_items: + - /etc/haproxy/conf.d + - "{{ haproxy_ssl_cert_path }}" diff --git a/tasks/haproxy_ssl.yml b/tasks/haproxy_ssl.yml deleted file mode 100644 index ee6a4ec..0000000 --- a/tasks/haproxy_ssl.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -# Copyright 2015, Rackspace US, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - #NOTE (jrosser) the self signed certificate is also needed for bootstrapping - #letsencrypt, as haproxy will not start with ssl config but a missing certificate -- name: Create and install SSL certificates - include_role: - name: pki - tasks_from: "{{ openstack_pki_authorities is defined | ternary('main_certs.yml', 'main.yml') }}" - vars: - pki_setup_host: "{{ haproxy_pki_setup_host }}" - pki_dir: "{{ haproxy_pki_dir }}" - pki_create_ca: "{{ haproxy_pki_create_ca }}" - pki_regen_ca: "{{ haproxy_pki_regen_ca }}" - pki_authorities: "{{ haproxy_pki_authorities }}" - pki_install_ca: "{{ haproxy_pki_install_ca }}" - pki_create_certificates: "{{ haproxy_user_ssl_cert is not defined and haproxy_user_ssl_key is not defined }}" - pki_regen_cert: "{{ haproxy_pki_regen_cert }}" - pki_certificates: "{{ haproxy_pki_certificates }}" - pki_install_certificates: "{{ haproxy_pki_install_certificates }}" - when: - - haproxy_ssl | bool - - haproxy_user_ssl_cert is not defined or haproxy_user_ssl_key is not defined \ No newline at end of file diff --git a/tasks/main.yml b/tasks/main.yml index 3a81118..3c3ffb9 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -47,7 +47,6 @@ pki_install_certificates: "{{ haproxy_pki_install_certificates }}" when: - haproxy_ssl | bool - - haproxy_user_ssl_cert is not defined or haproxy_user_ssl_key is not defined - import_tasks: haproxy_post_install.yml tags: diff --git a/templates/haproxy.cfg.j2 b/templates/haproxy.cfg.j2 index 2dd4335..b82c83e 100644 --- a/templates/haproxy.cfg.j2 +++ b/templates/haproxy.cfg.j2 @@ -40,7 +40,7 @@ defaults {% if haproxy_stats_enabled | bool %} listen stats - bind {{ haproxy_stats_bind_address }}:{{ haproxy_stats_port }} {% if haproxy_ssl | bool %}ssl crt {{ haproxy_ssl_pem }} ciphers {{ haproxy_ssl_cipher_suite }}{% endif %} + bind {{ haproxy_stats_bind_address }}:{{ haproxy_stats_port }} {% if haproxy_ssl | bool %}ssl crt {{ haproxy_ssl_cert_path }}/haproxy_{{ ansible_facts['hostname'] }}-{{ haproxy_bind_internal_lb_vip_address }}.pem ciphers {{ haproxy_ssl_cipher_suite }}{% endif %} {% if haproxy_stats_process is defined %} bind-process {{ haproxy_stats_process }} diff --git a/templates/service.j2 b/templates/service.j2 index 122b750..76d8a60 100644 --- a/templates/service.j2 +++ b/templates/service.j2 @@ -12,22 +12,11 @@ {% set haproxy_check_port = item.service.haproxy_check_port %} {% endif -%} -{% set vip_binds = [haproxy_bind_external_lb_vip_address] -%} -{%- if haproxy_bind_internal_lb_vip_address not in vip_binds %} - {% set _ = vip_binds.append(haproxy_bind_internal_lb_vip_address) %} -{% endif -%} - -{% for vip_address in extra_lb_vip_addresses %} - {% set _ = vip_binds.append(vip_address) %} -{% endfor %} - -{% for vip_address in extra_lb_tls_vip_addresses %} - {% set _ = vip_binds.append(vip_address) %} -{% endfor %} - -{%- if item.service.haproxy_bind is defined %} - {% set vip_binds = item.service.haproxy_bind %} -{% endif -%} +{% if item.service.haproxy_bind is defined %} +{% set vip_binds = item.service.haproxy_bind %} +{% else %} +{% set vip_binds = _haproxy_tls_vip_binds + extra_lb_vip_addresses %} +{% endif %} {% if not item.service.haproxy_backend_only | default(false) %} {% for vip_bind in vip_binds %} @@ -48,7 +37,7 @@ bind {{ vip_bind }}:{{ item.service.haproxy_redirect_http_port }} {% endif %} frontend {{ item.service.haproxy_service_name }}-front-{{ loop.index }} - bind {{ vip_bind }}:{{ item.service.haproxy_port }} {% if (item.service.haproxy_ssl | default(false) | bool) and (loop.index == 1 or vip_bind in extra_lb_tls_vip_addresses or item.service.haproxy_ssl_all_vips | default(false) | bool) %}ssl crt {{ haproxy_ssl_pem }} ciphers {{ haproxy_ssl_cipher_suite }}{% endif %} + bind {{ vip_bind }}:{{ item.service.haproxy_port }} {% if (item.service.haproxy_ssl | default(false) | bool) and (loop.index == 1 or vip_bind in extra_lb_tls_vip_addresses or (item.service.haproxy_ssl_all_vips | default(false) | bool and vip_bind not in extra_lb_vip_addresses)) %}ssl crt {{ haproxy_ssl_cert_path }}/haproxy_{{ ansible_facts['hostname'] }}-{{ vip_bind }}.pem ciphers {{ haproxy_ssl_cipher_suite }}{% endif %} {% if request_option == "http" %} option httplog @@ -75,7 +64,7 @@ frontend {{ item.service.haproxy_service_name }}-front-{{ loop.index }} {% endif %} {% endfor %} {% endif %} -{% if (item.service.haproxy_ssl | default(false) | bool) and request_option == 'http' and (loop.index == 1 or vip_bind in extra_lb_tls_vip_addresses or item.service.haproxy_ssl_all_vips | default(false) | bool) %} +{% if (item.service.haproxy_ssl | default(false) | bool) and request_option == 'http' and (loop.index == 1 or vip_bind in extra_lb_tls_vip_addresses or (item.service.haproxy_ssl_all_vips | default(false) | bool and vip_bind not in extra_lb_vip_addresses)) %} http-request add-header X-Forwarded-Proto https {% endif %} mode {{ item.service.haproxy_balance_type }} diff --git a/vars/main.yml b/vars/main.yml new file mode 100644 index 0000000..c40253b --- /dev/null +++ b/vars/main.yml @@ -0,0 +1,78 @@ +--- +# Copyright 2021, City Network International AB +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +_haproxy_tls_vip_binds: | + {% set vip_binds = [haproxy_bind_external_lb_vip_address] %} + {% if haproxy_bind_internal_lb_vip_address != haproxy_bind_external_lb_vip_address %} + {% set _ = vip_binds.append(haproxy_bind_internal_lb_vip_address) %} + {% endif %} + {% for vip_address in extra_lb_tls_vip_addresses %} + {% set _ = vip_binds.append(vip_address) %} + {% endfor %} + {{ vip_binds }} + +_haproxy_pki_certificates: | + {% set _pki_certs = [] %} + {% for vip in _haproxy_tls_vip_binds %} + {% set _ = _pki_certs.append( + { + 'name': 'haproxy_' ~ ansible_facts['hostname'] ~ '-' ~ vip, + 'provider': 'ownca', + 'cn': ansible_facts['hostname'], + 'san': 'DNS:' ~ ansible_facts['hostname'] ~ ',DNS:' ~ ansible_facts['fqdn'] ~ ',' ~ (vip | ansible.netcommon.ipaddr) | ternary('IP:', 'DNS:') ~ vip, + 'signed_by': haproxy_pki_intermediate_cert_name, + } + ) %} + {% endfor %} + {{ _pki_certs }} + +_haproxy_pki_install_certificates: | + {% set _pki_install = [] %} + {% for vip in _haproxy_tls_vip_binds %} + {% set _cert_basename = '/haproxy_' ~ ansible_facts['hostname'] ~ '-' ~ vip %} + {% set _ = _pki_install.append( + { + 'src': haproxy_user_ssl_cert | default(haproxy_pki_certs_path ~ _cert_basename ~ '.crt'), + 'dest': haproxy_ssl_cert_path ~ _cert_basename ~ '.crt', + 'owner': 'root', + 'group': 'root', + 'mode': '0644' + } + ) + %} + {% set _ = _pki_install.append( + { + 'src': haproxy_user_ssl_key | default(haproxy_pki_keys_path ~ _cert_basename ~ '.key.pem'), + 'dest': haproxy_ssl_cert_path ~ _cert_basename ~ '.key', + 'owner': 'root', + 'group': 'root', + 'mode': '0644' + } + ) + %} + {# We need to put CA only when it's provided by user or internal CA is used and user certs are not provided #} + {% if (haproxy_user_ssl_cert is not defined and haproxy_user_ssl_key is not defined) or haproxy_user_ssl_ca_cert is defined %} + {% set _ = _pki_install.append( + { + 'src': haproxy_user_ssl_ca_cert | default(haproxy_pki_intermediate_cert_path), + 'dest': haproxy_ssl_cert_path ~ _cert_basename ~ '-ca.crt', + 'owner': 'root', + 'group': 'root', + 'mode': '0644' + }) + %} + {% endif %} + {% endfor %} + {{ _pki_install }}