veza/ansible/playbooks/30-haproxy-acme.yml
2025-12-03 22:56:50 +01:00

286 lines
10 KiB
YAML

---
# Configure HAProxy + Let's Encrypt ACME in container
# Sets up SSL termination and request routing
- name: Configure HAProxy + Let's Encrypt ACME for Veza V5 Ultra
hosts: edge
become: true
gather_facts: true
vars:
domain: "{{ domain | default('veza.talas.fr') }}"
acme_email: "{{ acme_email | default('ops@talas.fr') }}"
haproxy_container: "veza-haproxy"
webroot_port: 8888
tasks:
- name: Install HAProxy and ACME tools in container
command: |
incus exec {{ haproxy_container }} -- apt update
incus exec {{ haproxy_container }} -- apt install -y haproxy dehydrated nginx-light
register: install_result
failed_when: false
- name: Display installation result
debug:
var: install_result.stdout_lines
- name: Create ACME webroot directory
command: |
incus exec {{ haproxy_container }} -- mkdir -p /var/www/acme-challenge
incus exec {{ haproxy_container }} -- chown -R www-data:www-data /var/www/acme-challenge
register: webroot_result
failed_when: false
- name: Configure nginx for ACME challenges
command: |
incus exec {{ haproxy_container }} -- tee /etc/nginx/sites-available/acme << 'EOF'
server {
listen 127.0.0.1:{{ webroot_port }};
server_name _;
root /var/www/acme-challenge;
location /.well-known/acme-challenge/ {
try_files $uri =404;
}
}
EOF
register: nginx_config_result
failed_when: false
- name: Enable nginx ACME site
command: |
incus exec {{ haproxy_container }} -- ln -sf /etc/nginx/sites-available/acme /etc/nginx/sites-enabled/
incus exec {{ haproxy_container }} -- rm -f /etc/nginx/sites-enabled/default
incus exec {{ haproxy_container }} -- systemctl restart nginx
register: nginx_enable_result
failed_when: false
- name: Configure dehydrated for Let's Encrypt
command: incus exec {{ haproxy_container }} -- bash -c 'echo "CA=\"https://acme-v02.api.letsencrypt.org/directory\"" > /etc/dehydrated/config'
register: dehydrated_config_result
failed_when: false
- name: Add CHALLENGETYPE to dehydrated config
command: incus exec {{ haproxy_container }} -- bash -c 'echo "CHALLENGETYPE=\"http-01\"" >> /etc/dehydrated/config'
register: dehydrated_config_result2
failed_when: false
- name: Add WELLKNOWN to dehydrated config
command: incus exec {{ haproxy_container }} -- bash -c 'echo "WELLKNOWN=\"/var/www/acme-challenge\"" >> /etc/dehydrated/config'
register: dehydrated_config_result3
failed_when: false
- name: Add CONTACT_EMAIL to dehydrated config
command: incus exec {{ haproxy_container }} -- bash -c 'echo "CONTACT_EMAIL=\"{{ acme_email }}\"" >> /etc/dehydrated/config'
register: dehydrated_config_result4
failed_when: false
- name: Add HOOK to dehydrated config
command: incus exec {{ haproxy_container }} -- bash -c 'echo "HOOK=\"/etc/dehydrated/hook.sh\"" >> /etc/dehydrated/config'
register: dehydrated_config_result
failed_when: false
- name: Create dehydrated hook script
command: |
incus exec {{ haproxy_container }} -- bash -c 'cat > /etc/dehydrated/hook.sh << "EOF"
#!/bin/bash
# Dehydrated hook for HAProxy certificate management
case "$1" in
"deploy_cert")
# Deploy certificate to HAProxy
cat "$3" "$5" > /etc/haproxy/certs/${2}.pem
systemctl reload haproxy
;;
"clean_challenge")
# Clean up challenge files
rm -f /var/www/acme-challenge/*
;;
"deploy_challenge")
# Deploy challenge file
cp "$2" "/var/www/acme-challenge/$3"
;;
"unchanged_cert")
# Certificate unchanged
;;
esac
EOF'
register: hook_script_result
failed_when: false
- name: Make hook script executable
command: |
incus exec {{ haproxy_container }} -- chmod +x /etc/dehydrated/hook.sh
register: hook_executable_result
failed_when: false
- name: Create HAProxy configuration
command: |
incus exec {{ haproxy_container }} -- tee /etc/haproxy/haproxy.cfg << 'EOF'
global
daemon
user haproxy
group haproxy
log stdout local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
tune.ssl.default-dh-param 2048
defaults
mode http
log global
option httplog
option dontlognull
option log-health-checks
option forwardfor
option httpchk GET /health
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
# ACME challenge backend
backend acme
server acme 127.0.0.1:{{ webroot_port }} check
# API backend
backend be_api
balance roundrobin
option httpchk GET /api/health
http-check expect status 200
server api1 10.10.0.101:8080 check inter 2000 rise 2 fall 3
# WebSocket backend
backend be_ws
mode tcp
balance roundrobin
server ws1 10.10.0.102:8081 check inter 2000 rise 2 fall 3
# Stream backend
backend be_stream
balance roundrobin
option httpchk GET /stream/health
http-check expect status 200
server stream1 10.10.0.103:8082 check inter 2000 rise 2 fall 3
# Web frontend backend
backend be_web
balance roundrobin
option httpchk GET /
http-check expect status 200
server web1 10.10.0.104:3000 check inter 2000 rise 2 fall 3
# HTTP frontend (redirect to HTTPS)
frontend http_frontend
bind *:80
acl acme_challenge path_beg /.well-known/acme-challenge/
use_backend acme if acme_challenge
redirect scheme https code 301 if !acme_challenge
# HTTPS frontend
frontend https_frontend
bind *:443 ssl crt /etc/haproxy/certs/{{ domain }}.pem alpn h2,http/1.1
# Security headers
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
http-response set-header X-Content-Type-Options "nosniff"
http-response set-header X-Frame-Options "DENY"
http-response set-header X-XSS-Protection "1; mode=block"
http-response set-header Referrer-Policy "no-referrer"
http-response set-header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' wss:;"
# Routing rules
acl is_api path_beg /api
acl is_ws path_beg /ws
acl is_stream path_beg /stream
use_backend be_api if is_api
use_backend be_ws if is_ws
use_backend be_stream if is_stream
default_backend be_web
# Statistics
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
EOF
register: haproxy_config_result
failed_when: false
- name: Create HAProxy certificates directory
command: |
incus exec {{ haproxy_container }} -- mkdir -p /etc/haproxy/certs
register: certs_dir_result
failed_when: false
- name: Generate self-signed certificate (temporary)
command: |
incus exec {{ haproxy_container }} -- openssl req -x509 -newkey rsa:4096 -keyout /etc/haproxy/certs/{{ domain }}.pem -out /etc/haproxy/certs/{{ domain }}.pem -days 365 -nodes -subj "/C=FR/ST=France/L=Paris/O=Veza/OU=IT/CN={{ domain }}"
register: self_signed_result
failed_when: false
- name: Start HAProxy service
command: |
incus exec {{ haproxy_container }} -- systemctl enable haproxy
incus exec {{ haproxy_container }} -- systemctl start haproxy
register: haproxy_start_result
failed_when: false
- name: Check HAProxy status
command: |
incus exec {{ haproxy_container }} -- systemctl status haproxy
register: haproxy_status
failed_when: false
- name: Display HAProxy status
debug:
var: haproxy_status.stdout_lines
- name: Request Let's Encrypt certificate
command: |
incus exec {{ haproxy_container }} -- dehydrated -c -d {{ domain }}
register: acme_cert_result
failed_when: false
- name: Display ACME certificate result
debug:
var: acme_cert_result.stdout_lines
- name: Setup certificate renewal cron
command: |
incus exec {{ haproxy_container }} -- tee /etc/cron.d/dehydrated << 'EOF'
0 12 * * * root /usr/bin/dehydrated -c
EOF
register: cron_result
failed_when: false
- name: Test HAProxy configuration
command: |
incus exec {{ haproxy_container }} -- haproxy -c -f /etc/haproxy/haproxy.cfg
register: haproxy_test_result
failed_when: false
- name: Display HAProxy test result
debug:
var: haproxy_test_result.stdout_lines
post_tasks:
- name: Show HAProxy statistics
command: |
incus exec {{ haproxy_container }} -- curl -s http://localhost:8404/stats
register: haproxy_stats
failed_when: false
- name: Display HAProxy statistics
debug:
var: haproxy_stats.stdout_lines