diff --git a/infra/ansible/roles/haproxy/tasks/main.yml b/infra/ansible/roles/haproxy/tasks/main.yml index d5c3ba52e..f081d37d6 100644 --- a/infra/ansible/roles/haproxy/tasks/main.yml +++ b/infra/ansible/roles/haproxy/tasks/main.yml @@ -26,6 +26,46 @@ mode: "0750" tags: [haproxy, config] +# Chicken-and-egg : haproxy.cfg.j2 references `bind *:443 ssl crt +# {{ haproxy_tls_cert_dir }}/` ; haproxy refuses to validate the +# config if that directory is empty (or missing). dehydrated creates +# real LE certs there LATER (in letsencrypt.yml). To break the cycle, +# pre-create the dir with a 30-day self-signed placeholder cert. +# The placeholder is overwritten / shadowed once dehydrated lands ; +# SNI picks the matching real cert. +- name: Ensure TLS cert dir + placeholder cert exist (gates the haproxy.cfg validate) + when: haproxy_letsencrypt | default(false) + block: + - name: Ensure {{ haproxy_tls_cert_dir }} exists + ansible.builtin.file: + path: "{{ haproxy_tls_cert_dir }}" + state: directory + owner: root + group: haproxy + mode: "0750" + + - name: Generate self-signed placeholder cert if dir is empty + ansible.builtin.shell: | + set -e + if ls "{{ haproxy_tls_cert_dir }}"/*.pem >/dev/null 2>&1; then + echo "cert already present" + exit 0 + fi + openssl req -x509 -nodes -newkey rsa:2048 \ + -keyout /tmp/_placeholder.key \ + -out /tmp/_placeholder.crt \ + -days 30 \ + -subj '/CN=placeholder.veza.local' >/dev/null 2>&1 + cat /tmp/_placeholder.crt /tmp/_placeholder.key \ + > "{{ haproxy_tls_cert_dir }}/_placeholder.pem" + chmod 0640 "{{ haproxy_tls_cert_dir }}/_placeholder.pem" + chown root:haproxy "{{ haproxy_tls_cert_dir }}/_placeholder.pem" + rm -f /tmp/_placeholder.key /tmp/_placeholder.crt + echo "placeholder cert generated" + register: placeholder_cert + changed_when: "'placeholder cert generated' in placeholder_cert.stdout" + tags: [haproxy, config, letsencrypt] + - name: Render haproxy.cfg ansible.builtin.template: src: haproxy.cfg.j2 diff --git a/infra/ansible/roles/haproxy/templates/haproxy.cfg.j2 b/infra/ansible/roles/haproxy/templates/haproxy.cfg.j2 index 92c4f7474..1ef37a7be 100644 --- a/infra/ansible/roles/haproxy/templates/haproxy.cfg.j2 +++ b/infra/ansible/roles/haproxy/templates/haproxy.cfg.j2 @@ -63,9 +63,12 @@ frontend veza_http_in bind *:{{ haproxy_listen_https }} ssl crt {{ haproxy_tls_cert_dir }}/ alpn h2,http/1.1 http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains" # Let dehydrated's HTTP-01 challenges through unencrypted before any redirect. + # Order matters : http-request rules must come BEFORE use_backend + # rules in HAProxy ; otherwise haproxy 3.x warns and processes them + # in the unintended order. acl acme_challenge path_beg /.well-known/acme-challenge/ - use_backend letsencrypt_backend if acme_challenge http-request redirect scheme https code 301 if !{ ssl_fc } !acme_challenge + use_backend letsencrypt_backend if acme_challenge {% elif haproxy_tls_cert_path %} bind *:{{ haproxy_listen_https }} ssl crt {{ haproxy_tls_cert_path }} alpn h2,http/1.1 http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"