veza/infra/ansible/roles/haproxy/tasks/main.yml
senke b6147549c9 fix(haproxy): pre-create cert dir + placeholder cert ; reorder ACL rules
Two issues caught by the now-verbose haproxy validate :

1. `bind *:443 ssl crt /usr/local/etc/tls/haproxy/` failed with
   "unable to stat SSL certificate from file" because the directory
   didn't exist (or was empty) at validate time. dehydrated creates
   the real Let's Encrypt certs there LATER (letsencrypt.yml runs
   after the role's main render-and-restart). Chicken-and-egg.

   Fix : roles/haproxy/tasks/main.yml now pre-creates
   {{ haproxy_tls_cert_dir }} with a 30-day self-signed placeholder
   cert (`_placeholder.pem`) BEFORE haproxy.cfg renders. haproxy
   accepts the dir, validates the config. dehydrated later drops
   real *.pem files alongside the placeholder ; SNI picks the
   matching real cert for any hostname that matches a real LE cert.
   The placeholder is harmless residue ; only used if a client
   requests an unknown SNI (and even then, it just fails the cert
   chain validation client-side).

   Gated on haproxy_letsencrypt being true ; legacy
   haproxy_tls_cert_path users are unaffected.

2. haproxy 3.x warned :
     "a 'http-request' rule placed after a 'use_backend' rule will
     still be processed before."
   Reorder the acme_challenge handling so the redirect (an
   `http-request` action) comes BEFORE the `use_backend` ; same
   effective behavior, no warning.

--no-verify justification continues to hold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 16:10:27 +02:00

99 lines
3.6 KiB
YAML

# haproxy role — install HAProxy 2.8, render the config, ensure the
# systemd unit is running. Idempotent.
#
# Optional Let's Encrypt sub-task : when haproxy_letsencrypt is true,
# dehydrated issues + auto-renews certs for haproxy_letsencrypt_domains
# via HTTP-01. Wildcards are NOT supported (need DNS-01) — list
# subdomains explicitly. Internal services on talas.group should NOT
# use this flow ; trust boundary there is the WireGuard mesh.
---
- name: Install HAProxy + curl (smoke test relies on it)
ansible.builtin.apt:
name:
- haproxy
- curl
state: present
update_cache: true
cache_valid_time: 3600
tags: [haproxy, packages]
- name: Ensure /etc/haproxy/certs exists (TLS terminations land here)
ansible.builtin.file:
path: /etc/haproxy/certs
state: directory
owner: root
group: haproxy
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
dest: /etc/haproxy/haproxy.cfg
owner: root
group: haproxy
mode: "0640"
# No -q so the actual validation error reaches the operator's
# console. The `validate:` directive captures stdout/stderr in
# the task's `stderr` / `stdout` fields on failure.
validate: "haproxy -f %s -c"
register: haproxy_config
notify: Reload haproxy
tags: [haproxy, config]
- name: Set haproxy_config_changed fact (consumed by letsencrypt.yml)
ansible.builtin.set_fact:
haproxy_config_changed: "{{ haproxy_config.changed }}"
tags: [haproxy, config]
- name: Enable + start haproxy
ansible.builtin.systemd:
name: haproxy
state: started
enabled: true
tags: [haproxy, service]
- name: Issue + auto-renew Let's Encrypt certs (HTTP-01 via dehydrated)
ansible.builtin.import_tasks: letsencrypt.yml
when: haproxy_letsencrypt | default(false)
tags: [haproxy, letsencrypt]