Refactor HAProxy configuration and certificate management
- Updated HAProxy configuration template to reflect changes for the Taurus Production Environment, including SSL settings and rate limiting for specific endpoints. - Introduced new playbooks for certificate distribution and validation with OCI Vault, ensuring certificates are correctly managed and renewed. - Added hooks for uploading renewed certificates to OCI Vault and validating their integrity. - Enhanced the HAProxy configuration playbook to ensure proper service management and verification of the HAProxy service. - Updated inventory variables for certificate management and ensured compatibility with the new structure.
This commit is contained in:
101
ansible/haproxy/configure.yml
Normal file
101
ansible/haproxy/configure.yml
Normal file
@@ -0,0 +1,101 @@
|
||||
---
|
||||
# -----------------------------------------------------------------------------
|
||||
# HAProxy Configuration Playbook
|
||||
# -----------------------------------------------------------------------------
|
||||
# Templates haproxy.cfg and starts the HAProxy service. Must run AFTER both
|
||||
# haproxy/deploy.yml and certbot/deploy.yml so that:
|
||||
# - The HAProxy package is installed
|
||||
# - The real Let's Encrypt certificate exists at haproxy_cert_path
|
||||
#
|
||||
# Dependency chain:
|
||||
# haproxy/deploy.yml ← package + dirs
|
||||
# certbot/deploy.yml ← writes cert to /etc/haproxy/certs/
|
||||
# haproxy/configure.yml ← this playbook (config + start)
|
||||
#
|
||||
# Hosts: horkos (public reverse proxy), bootes (internal HAProxy)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
- name: Configure and start HAProxy
|
||||
hosts: all
|
||||
become: true
|
||||
tags: [haproxy, service, configure]
|
||||
|
||||
handlers:
|
||||
- name: reload haproxy
|
||||
ansible.builtin.systemd:
|
||||
name: haproxy
|
||||
state: reloaded
|
||||
|
||||
- name: restart haproxy
|
||||
ansible.builtin.systemd:
|
||||
name: haproxy
|
||||
state: restarted
|
||||
|
||||
tasks:
|
||||
- name: Check if host has haproxy service
|
||||
ansible.builtin.set_fact:
|
||||
has_haproxy_service: "{{ 'haproxy' in services | default([]) }}"
|
||||
|
||||
- name: Skip hosts without haproxy service
|
||||
ansible.builtin.meta: end_host
|
||||
when: not has_haproxy_service
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Certificate Check
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
- name: Check if TLS certificate exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ haproxy_cert_path }}"
|
||||
register: cert_file
|
||||
|
||||
- name: Fail if certificate is missing
|
||||
ansible.builtin.fail:
|
||||
msg: >
|
||||
Certificate not found at {{ haproxy_cert_path }}.
|
||||
Run certbot/deploy.yml before haproxy/configure.yml.
|
||||
Command: ansible-playbook certbot/deploy.yml
|
||||
when: not cert_file.stat.exists
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
- name: Template HAProxy configuration
|
||||
ansible.builtin.template:
|
||||
src: haproxy.cfg.j2
|
||||
dest: /etc/haproxy/haproxy.cfg
|
||||
owner: root
|
||||
group: "{{ haproxy_group | default('haproxy') }}"
|
||||
mode: '0640'
|
||||
validate: "haproxy -c -f %s"
|
||||
notify: reload haproxy
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Service Management
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
- name: Enable and start HAProxy service
|
||||
ansible.builtin.systemd:
|
||||
name: haproxy
|
||||
enabled: true
|
||||
state: started
|
||||
daemon_reload: true
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Verification
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
- name: Wait for HAProxy stats port to be ready
|
||||
ansible.builtin.uri:
|
||||
url: "http://localhost:{{ haproxy_stats_port }}/metrics"
|
||||
method: GET
|
||||
status_code: 200
|
||||
register: haproxy_health
|
||||
retries: 5
|
||||
delay: 3
|
||||
until: haproxy_health.status == 200
|
||||
|
||||
- name: HAProxy configuration status
|
||||
ansible.builtin.debug:
|
||||
msg: "HAProxy is running and serving metrics on port {{ haproxy_stats_port }}"
|
||||
@@ -1,117 +1,83 @@
|
||||
---
|
||||
- name: Deploy HAProxy
|
||||
hosts: ubuntu
|
||||
# -----------------------------------------------------------------------------
|
||||
# HAProxy Deployment Playbook
|
||||
# -----------------------------------------------------------------------------
|
||||
# Installs HAProxy and creates the directory structure required by downstream
|
||||
# playbooks. This playbook must run BEFORE certbot/deploy.yml so that the
|
||||
# /etc/haproxy/certs directory exists with the correct haproxy group ownership
|
||||
# when certbot writes the combined PEM file.
|
||||
#
|
||||
# Dependency chain:
|
||||
# haproxy/deploy.yml ← this playbook (package + dirs)
|
||||
# certbot/deploy.yml ← writes cert to /etc/haproxy/certs/
|
||||
# haproxy/configure.yml ← templates haproxy.cfg and starts the service
|
||||
#
|
||||
# Hosts: horkos (public reverse proxy), bootes (internal HAProxy)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
- name: Deploy HAProxy (package and directory structure)
|
||||
hosts: all
|
||||
become: true
|
||||
tags: [haproxy, service, deploy]
|
||||
|
||||
tasks:
|
||||
- name: Check if host has haproxy service
|
||||
set_fact:
|
||||
has_haproxy_service: "{{'haproxy' in services}}"
|
||||
ansible.builtin.set_fact:
|
||||
has_haproxy_service: "{{ 'haproxy' in services | default([]) }}"
|
||||
|
||||
- name: Skip hosts without haproxy service
|
||||
meta: end_host
|
||||
ansible.builtin.meta: end_host
|
||||
when: not has_haproxy_service
|
||||
|
||||
- name: Create haproxy group
|
||||
become: true
|
||||
ansible.builtin.group:
|
||||
name: "{{haproxy_group}}"
|
||||
gid: "{{haproxy_gid}}"
|
||||
system: true
|
||||
# -------------------------------------------------------------------------
|
||||
# Install HAProxy
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
- name: Create haproxy user
|
||||
become: true
|
||||
ansible.builtin.user:
|
||||
name: "{{haproxy_user}}"
|
||||
comment: "{{haproxy_user}}"
|
||||
group: "{{haproxy_group}}"
|
||||
uid: "{{haproxy_uid}}"
|
||||
system: true
|
||||
|
||||
- name: Add group haproxy to keeper_user
|
||||
become: true
|
||||
ansible.builtin.user:
|
||||
name: "{{keeper_user}}"
|
||||
groups: "{{haproxy_group}}"
|
||||
append: true
|
||||
|
||||
- name: Create required directories
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
path: "{{haproxy_directory}}"
|
||||
owner: "{{haproxy_user}}"
|
||||
group: "{{haproxy_group}}"
|
||||
state: directory
|
||||
mode: '750'
|
||||
|
||||
- name: Create /etc/haproxy directory
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
path: /etc/haproxy
|
||||
owner: root
|
||||
group: root
|
||||
state: directory
|
||||
mode: '755'
|
||||
|
||||
- name: Create certs directory
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
path: /etc/haproxy/certs
|
||||
owner: "{{haproxy_user}}"
|
||||
group: "{{haproxy_group}}"
|
||||
state: directory
|
||||
mode: '750'
|
||||
|
||||
- name: Check if certificate already exists
|
||||
become: true
|
||||
stat:
|
||||
path: "{{ haproxy_cert_path }}"
|
||||
register: cert_file
|
||||
|
||||
- name: Generate self-signed wildcard certificate
|
||||
become: true
|
||||
command: >
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048
|
||||
-keyout {{ haproxy_cert_path }}
|
||||
-out {{ haproxy_cert_path }}
|
||||
-subj "/C=US/ST=State/L=City/O=Ouranos/CN=*.{{ haproxy_domain }}"
|
||||
-addext "subjectAltName=DNS:*.{{ haproxy_domain }},DNS:{{ haproxy_domain }}"
|
||||
when: not cert_file.stat.exists and 'certbot' not in services
|
||||
|
||||
- name: Set certificate permissions
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
path: "{{ haproxy_cert_path }}"
|
||||
owner: "{{haproxy_user}}"
|
||||
group: "{{haproxy_group}}"
|
||||
mode: '640'
|
||||
|
||||
- name: Install HAProxy
|
||||
become: true
|
||||
- name: Ensure HAProxy is installed
|
||||
ansible.builtin.apt:
|
||||
name: haproxy
|
||||
state: present
|
||||
update_cache: true
|
||||
|
||||
- name: Template HAProxy configuration
|
||||
become: true
|
||||
ansible.builtin.template:
|
||||
src: "haproxy.cfg.j2"
|
||||
dest: /etc/haproxy/haproxy.cfg
|
||||
owner: "{{haproxy_user}}"
|
||||
group: "{{haproxy_group}}"
|
||||
mode: "640"
|
||||
validate: haproxy -c -f %s
|
||||
register: haproxy_config
|
||||
# -------------------------------------------------------------------------
|
||||
# User / Group
|
||||
# HAProxy's apt package creates the haproxy user/group, but we also need
|
||||
# the certbot group to exist so that /etc/haproxy/certs can be group-owned
|
||||
# by haproxy and written by certbot.
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
- name: Enable and start HAProxy service
|
||||
become: true
|
||||
ansible.builtin.systemd:
|
||||
name: haproxy
|
||||
enabled: true
|
||||
state: started
|
||||
- name: Ensure haproxy group exists
|
||||
ansible.builtin.group:
|
||||
name: "{{ haproxy_group | default('haproxy') }}"
|
||||
system: true
|
||||
|
||||
- name: Reload HAProxy if configuration changed
|
||||
become: true
|
||||
ansible.builtin.systemd:
|
||||
name: haproxy
|
||||
state: reloaded
|
||||
when: haproxy_config.changed
|
||||
- name: Ensure haproxy user exists
|
||||
ansible.builtin.user:
|
||||
name: "{{ haproxy_user | default('haproxy') }}"
|
||||
group: "{{ haproxy_group | default('haproxy') }}"
|
||||
system: true
|
||||
shell: /usr/sbin/nologin
|
||||
home: /nonexistent
|
||||
create_home: false
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Directory Structure
|
||||
# /etc/haproxy/certs must exist with haproxy group ownership before certbot
|
||||
# runs so that the renewal hook can write the combined PEM file there.
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
- name: Ensure /etc/haproxy directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/haproxy
|
||||
owner: root
|
||||
group: "{{ haproxy_group | default('haproxy') }}"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Ensure /etc/haproxy/certs directory exists
|
||||
ansible.builtin.file:
|
||||
path: /etc/haproxy/certs
|
||||
owner: "{{ certbot_user | default('certbot') }}"
|
||||
group: "{{ haproxy_group | default('haproxy') }}"
|
||||
state: directory
|
||||
mode: '0750'
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
# HAProxy configuration for Ouranos Titania
|
||||
# HAProxy configuration for Taurus Production Environment
|
||||
# Managed by Ansible - Red Panda Approved
|
||||
#
|
||||
# SSL: Let's Encrypt certificate for helu.ca subdomains
|
||||
# HTTP backends: Casdoor (talos), Gitea (xenia), SearXNG (xenia)
|
||||
# TCP backend: Gitea SSH (xenia)
|
||||
|
||||
global
|
||||
log 127.0.0.1:{{ haproxy_syslog_port }} local0
|
||||
log /dev/log local0
|
||||
log /dev/log local1 notice
|
||||
stats timeout 30s
|
||||
# Ubuntu systemd service handles user/group and daemonization
|
||||
|
||||
# Default SSL material locations
|
||||
ca-base /etc/ssl/certs
|
||||
@@ -38,29 +44,47 @@ listen stats
|
||||
# Prometheus metrics endpoint
|
||||
http-request use-service prometheus-exporter if { path /metrics }
|
||||
|
||||
# HTTP frontend - redirect all traffic to HTTPS
|
||||
# HTTP to HTTPS redirect
|
||||
frontend http_frontend
|
||||
bind *:{{ haproxy_http_port }}
|
||||
mode http
|
||||
option httplog
|
||||
|
||||
# Redirect all HTTP to HTTPS
|
||||
http-request redirect scheme https code 301
|
||||
|
||||
# HTTPS frontend with dynamic routing
|
||||
frontend https_frontend
|
||||
bind *:{{ haproxy_https_port }} ssl crt {{ haproxy_cert_path }}
|
||||
bind *:{{ haproxy_https_port }} ssl crt {{ haproxy_cert_path }} alpn h2,http/1.1
|
||||
mode http
|
||||
option httplog
|
||||
option forwardfor
|
||||
|
||||
# Forward original protocol and host for reverse-proxied services
|
||||
http-request set-header X-Forwarded-Proto https
|
||||
http-request set-header X-Forwarded-Port %[dst_port]
|
||||
|
||||
# Security headers
|
||||
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||
http-response set-header X-Frame-Options "SAMEORIGIN"
|
||||
http-response set-header X-Content-Type-Options "nosniff"
|
||||
http-response set-header X-XSS-Protection "1; mode=block"
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Rate limiting via stick-tables
|
||||
# -------------------------------------------------------------------------
|
||||
# General rate limit: 1000 req/min per source IP
|
||||
stick-table type ip size 100k expire 1m store http_req_rate(1m)
|
||||
http-request track-sc0 src
|
||||
|
||||
# Auth endpoint rate limit: 20 req/min per source IP
|
||||
acl is_auth_endpoint path_beg /api/login /api/signup /api/get-captcha /login/oauth/authorize /api/login/oauth/access_token
|
||||
acl host_id hdr_beg(host) -i id.{{ haproxy_domain }}
|
||||
|
||||
# Use backend stick-table for auth endpoint tracking
|
||||
http-request track-sc1 src table st_casdoor_auth if host_id is_auth_endpoint
|
||||
|
||||
# Deny if general rate exceeded
|
||||
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 1000 }
|
||||
|
||||
# Deny if auth endpoint rate exceeded
|
||||
http-request deny deny_status 429 if host_id is_auth_endpoint { sc_http_req_rate(1,st_casdoor_auth) gt 20 }
|
||||
|
||||
{% for backend in haproxy_backends %}
|
||||
{% if backend.subdomain %}
|
||||
@@ -86,29 +110,37 @@ backend backend_root
|
||||
{% endif %}
|
||||
mode http
|
||||
balance roundrobin
|
||||
{% if backend.ssl_backend | default(false) %}
|
||||
option httpchk
|
||||
http-check send meth GET uri {{ backend.health_path }} hdr Host {{ backend.subdomain }}.{{ haproxy_domain }}
|
||||
{% else %}
|
||||
option httpchk GET {{ backend.health_path }}
|
||||
{% endif %}
|
||||
http-check send meth GET uri {{ backend.health_path }} ver HTTP/1.1 hdr Host {{ backend.health_host | default(backend.backend_host) }}
|
||||
http-check expect status 200
|
||||
{% if backend.timeout_server is defined %}
|
||||
timeout server {{ backend.timeout_server }}
|
||||
{% endif %}
|
||||
server {{ backend.subdomain or 'root' }}_1 {{ backend.backend_host }}:{{ backend.backend_port }} check{% if backend.ssl_backend | default(false) %} ssl verify none{% endif %}
|
||||
server {{ backend.subdomain or 'root' }}_1 {{ backend.backend_host }}:{{ backend.backend_port }} check
|
||||
|
||||
{% endfor %}
|
||||
|
||||
# Stick-table for auth endpoint rate limiting (referenced by frontend)
|
||||
backend st_casdoor_auth
|
||||
stick-table type ip size 100k expire 1m store http_req_rate(1m)
|
||||
|
||||
# =============================================================================
|
||||
# TCP Frontends/Backends (non-HTTP protocols)
|
||||
# =============================================================================
|
||||
|
||||
{% for tcp_backend in haproxy_tcp_backends | default([]) %}
|
||||
# TCP passthrough: {{ tcp_backend.name }}
|
||||
frontend {{ tcp_backend.name }}_frontend
|
||||
bind *:{{ tcp_backend.listen_port }}
|
||||
mode tcp
|
||||
option tcplog
|
||||
timeout client 1h
|
||||
default_backend {{ tcp_backend.name }}_backend
|
||||
|
||||
backend {{ tcp_backend.name }}_backend
|
||||
mode tcp
|
||||
option tcp-check
|
||||
timeout server 1h
|
||||
server {{ tcp_backend.name }}_1 {{ tcp_backend.backend_host }}:{{ tcp_backend.backend_port }} check
|
||||
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user