149 lines
5.4 KiB
Django/Jinja
149 lines
5.4 KiB
Django/Jinja
# 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 /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
|
|
crt-base /etc/ssl/private
|
|
|
|
# SSL/TLS configuration
|
|
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
|
|
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
|
|
|
|
defaults
|
|
log global
|
|
mode http
|
|
option httplog
|
|
option dontlognull
|
|
# Log format with timing information for latency analysis
|
|
log-format "%ci:%cp [%tr] %ft %b/%s %TR/%Tw/%Tc/%Tr/%Ta %ST %B %CC %CS %tsc %ac/%fc/%bc/%sc/%rc %sq/%bq %hr %hs %{+Q}r"
|
|
timeout connect 5s
|
|
timeout client 50s
|
|
timeout server 50s
|
|
|
|
# Stats page with Prometheus metrics
|
|
listen stats
|
|
bind *:{{ haproxy_stats_port }}
|
|
mode http
|
|
stats enable
|
|
stats uri /metrics
|
|
stats refresh 15s
|
|
stats show-legends
|
|
stats show-node
|
|
|
|
# Prometheus metrics endpoint
|
|
http-request use-service prometheus-exporter if { path /metrics }
|
|
|
|
# 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 }} alpn h2,http/1.1
|
|
mode http
|
|
option httplog
|
|
option forwardfor
|
|
# Tell backends the original connection was HTTPS (TLS terminates here)
|
|
http-request set-header X-Forwarded-Proto https
|
|
|
|
# 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 %}
|
|
# ACL for {{ backend.subdomain }}.{{ haproxy_domain }} (matches with or without port)
|
|
acl host_{{ backend.subdomain }} hdr_beg(host) -i {{ backend.subdomain }}.{{ haproxy_domain }}
|
|
{% if backend.redirect_root is defined %}
|
|
# Redirect root path to {{ backend.redirect_root }} (avoids redirect loop by matching exact path)
|
|
http-request redirect location {{ backend.redirect_root }} code 302 if host_{{ backend.subdomain }} { path / }
|
|
{% endif %}
|
|
use_backend backend_{{ backend.subdomain }} if host_{{ backend.subdomain }}
|
|
{% else %}
|
|
# Default backend for root domain
|
|
default_backend backend_root
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
# Backend definitions
|
|
{% for backend in haproxy_backends %}
|
|
{% if backend.subdomain %}
|
|
backend backend_{{ backend.subdomain }}
|
|
{% else %}
|
|
backend backend_root
|
|
{% endif %}
|
|
mode http
|
|
balance roundrobin
|
|
option httpchk
|
|
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
|
|
|
|
{% 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 %}
|