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

269 lines
9.2 KiB
YAML

---
# Configure HAProxy inside container with Let's Encrypt ACME HTTP-01
# Handles SSL termination and routing for Veza V5 Ultra
- name: Configure HAProxy in container with ACME
hosts: edge
become: true
gather_facts: true
vars:
haproxy_container: veza-haproxy
acme_webroot_port: 8888
haproxy_certs_dir: /etc/haproxy/certs
acme_webroot_dir: /var/www/acme-challenge
tasks:
- name: Install HAProxy and ACME tools in container
command: |
incus exec {{ haproxy_container }} -- bash -c "
apt update && apt install -y \
haproxy \
curl \
wget \
socat \
cron \
nginx-light \
openssl
"
register: haproxy_install_result
failed_when: false
- name: Display HAProxy installation result
debug:
var: haproxy_install_result.stdout_lines
when: haproxy_install_result.stdout_lines is defined
- name: Create ACME webroot directory
command: |
incus exec {{ haproxy_container }} -- mkdir -p {{ acme_webroot_dir }}
register: webroot_create_result
failed_when: false
- name: Create HAProxy certificates directory
command: |
incus exec {{ haproxy_container }} -- mkdir -p {{ haproxy_certs_dir }}
register: certs_dir_result
failed_when: false
- name: Install dehydrated for ACME
command: |
incus exec {{ haproxy_container }} -- bash -c "
cd /opt && \
git clone https://github.com/dehydrated-io/dehydrated.git && \
chmod +x dehydrated/dehydrated
"
register: dehydrated_install_result
failed_when: false
- name: Create dehydrated configuration
command: |
incus exec {{ haproxy_container }} -- bash -c "
cat > /opt/dehydrated/config << 'EOF'
WELLKNOWN={{ acme_webroot_dir }}
DOMAINS_TXT=/opt/dehydrated/domains.txt
HOOK=/opt/dehydrated/hook.sh
CHALLENGETYPE=http-01
EOF
"
register: dehydrated_config_result
failed_when: false
- name: Create domains file for ACME
command: |
incus exec {{ haproxy_container }} -- bash -c "
echo '{{ domain }}' > /opt/dehydrated/domains.txt
"
register: domains_file_result
failed_when: false
- name: Create ACME hook script
command: |
incus exec {{ haproxy_container }} -- bash -c "
cat > /opt/dehydrated/hook.sh << 'EOF'
#!/bin/bash
case \"\$1\" in
deploy_challenge)
# Start nginx for ACME challenge
nginx -c /etc/nginx/nginx.conf -g 'daemon on;'
;;
clean_challenge)
# Stop nginx after challenge
nginx -s quit
;;
deploy_cert)
# Combine cert and key for HAProxy
cat \$3 \$5 > {{ haproxy_certs_dir }}/{{ domain }}.pem
# Reload HAProxy
systemctl reload haproxy
;;
esac
EOF
chmod +x /opt/dehydrated/hook.sh
"
register: hook_script_result
failed_when: false
- name: Create nginx config for ACME webroot
command: |
incus exec {{ haproxy_container }} -- bash -c "
cat > /etc/nginx/nginx.conf << 'EOF'
events { worker_connections 1024; }
http {
server {
listen {{ acme_webroot_port }};
location /.well-known/acme-challenge/ {
root {{ acme_webroot_dir }};
}
}
}
EOF
"
register: nginx_config_result
failed_when: false
- name: Create HAProxy configuration
command: |
incus exec {{ haproxy_container }} -- bash -c "
cat > /etc/haproxy/haproxy.cfg << 'EOF'
global
log stdout local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 20000
ssl-default-bind-options no-sslv3 no-tls-tickets
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
timeout connect 5s
timeout client 50s
timeout server 50s
timeout http-request 5s
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
frontend http_frontend
bind *:80
acl acme_challenge path_beg /.well-known/acme-challenge/
use_backend acme_backend if acme_challenge
redirect scheme https code 301 if !acme_challenge
frontend https_frontend
bind *:443 ssl crt {{ haproxy_certs_dir }}/{{ domain }}.pem alpn h2,http/1.1
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 Referrer-Policy no-referrer
http-response set-header Content-Security-Policy \"default-src 'self'; connect-src 'self' wss://{{ domain }} https://{{ domain }}; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'\"
acl is_api path_beg /api
acl is_ws path_beg /ws
acl is_stream path_beg /stream
use_backend api_backend if is_api
use_backend ws_backend if is_ws
use_backend stream_backend if is_stream
default_backend web_backend
backend acme_backend
server acme_server 127.0.0.1:{{ acme_webroot_port }}
backend api_backend
server api_server veza-backend:{{ veza_backend_port }} check
backend ws_backend
mode tcp
server ws_server veza-chat:{{ veza_chat_port }} check
backend stream_backend
server stream_server veza-stream:{{ veza_stream_port }} check
backend web_backend
server web_server veza-web:{{ veza_web_port }} check
EOF
"
register: haproxy_config_result
failed_when: false
- name: Create self-signed certificate for initial setup
command: |
incus exec {{ haproxy_container }} -- bash -c "
openssl req -x509 -newkey rsa:2048 -keyout {{ haproxy_certs_dir }}/{{ domain }}.key -out {{ haproxy_certs_dir }}/{{ domain }}.crt -days 365 -nodes -subj '/CN={{ domain }}' && \
cat {{ haproxy_certs_dir }}/{{ domain }}.crt {{ haproxy_certs_dir }}/{{ domain }}.key > {{ haproxy_certs_dir }}/{{ domain }}.pem
"
register: selfsigned_cert_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: Display HAProxy start result
debug:
var: haproxy_start_result.stdout_lines
when: haproxy_start_result.stdout_lines is defined
- name: Check HAProxy status
command: |
incus exec {{ haproxy_container }} -- systemctl status haproxy
register: haproxy_status_result
failed_when: false
- name: Display HAProxy status
debug:
var: haproxy_status_result.stdout_lines
when: haproxy_status_result.stdout_lines is defined
- name: Create ACME renewal cron job
command: |
incus exec {{ haproxy_container }} -- bash -c "
echo '0 2 * * * /opt/dehydrated/dehydrated -c' | crontab -
"
register: cron_setup_result
failed_when: false
- name: Display cron setup result
debug:
var: cron_setup_result.stdout_lines
when: cron_setup_result.stdout_lines is defined
post_tasks:
- 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
when: haproxy_test_result.stdout_lines is defined
- name: Show final HAProxy status
command: |
incus exec {{ haproxy_container }} -- netstat -tlnp | grep haproxy
register: final_haproxy_status
failed_when: false
- name: Display final HAProxy status
debug:
var: final_haproxy_status.stdout_lines
when: final_haproxy_status.stdout_lines is defined