# /etc/haproxy/haproxy.cfg
# (c) 2026, Bernard Condrau (this config file)
#
# 2026-09-14: initial
#
# Features:
# HTTP/1, HTTP/2, HTTP/3, acme integration, static blacklist, static and dynamic whitelist
# handles transition from one backend server without reverse proxy to HAProxy with several backend servers
# Notes:
# # = comment, ## = acme integration, remove ## once you have set up acme (https://www.haproxy.com/blog/haproxy-and-let-s-encrypt)
# Remove the 'default_backend' once transition is done from Certbot / Apache to acme / HAProxy
# Remove 'public_inbound_https' once all hosts are SSL terminated here
#
# 0. Settings
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
user haproxy
group haproxy
daemon
# Modern SSL/TLS security settings for Debian 13
# Minimum TLS version requirement
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
# TLS 1.3 Cipher Suites (handled by ssl-default-bind-ciphersuites)
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
# TLS 1.2 Ciphers (handled by ssl-default-bind-ciphers)
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
# Administrative socket for stats/management
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
## setenv ACCOUNT_THUMBPRINT 'lCufto4sDRTHdmWL0EugFywGV54hBCuTTXvwifi65R4'
defaults
log global
mode tcp
log-format "%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts %ac/%fc/%bc/%sc/%rc %sq/%bq"
timeout connect 5s
timeout client 30s
timeout server 5m # set this high enough to allow complex websites to reply, for example Joomla during install or php calculations
# 1. Public Interface (SNI Router)
# Receives traffic through TCP (HTTP)
frontend public_inbound_http
bind *:80
mode http
# Handle Letsencrypt requests
##http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/' }
# Deny traffic based on origin
http-request deny if { src -f /etc/haproxy/blacklist.lst }
# Redirect all incoming traffic to HTTPS permanently
http-request redirect scheme https code 301 unless { ssl_fc }
# Receives traffic through TCP (HTTP/1, HTTP/2)
frontend public_inbound_https
bind :443
mode tcp
# Deny traffic based on origin
tcp-request connection reject if { src -f /etc/haproxy/blacklist.lst }
# Wait for the TLS client hello to parse the SNI extension
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
# Define the domains (one line for each) you want to terminate SSL for
# Remove once all hosts are terminated here
acl terminate_ssl req.ssl_sni -i bak.condrau.com
# Route conditionally based on SNI
use_backend http_dispatcher if terminate_ssl
default_backend passthrough_server
# Receives traffic through UDP (HTTP/3), decrypts SSL, and processes HTTP rules
# HTTP/3 cannot be routed to apache web server without decrypting SSL first
frontend public_inbound_https3
# pass certs directory path so all pem files will be loaded
# uncomment the following line once all hosts are SSL terminated here
# bind :443
bind quic4@:443 ssl crt /etc/haproxy/certs/ accept-proxy strict-sni
mode http
# Handle Letsencrypt requests
##http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/' }
# Deny traffic based on origin (host aware)
acl trusted_ip src -f /etc/haproxy/whitelist.lst # static whitelist
acl whitelist_ip src,in_table(stick_table_whitelist) # dynamic whitelist
acl backuppc hdr(host) -i bak.condrau.com
http-request deny if backuppc !trusted_ip !whitelist_ip
# Let clients know we support HTTP/3
http-response set-header Alt-Svc 'h3=":443"; ma=86400'
# Inject headers so backend servers know the original request was HTTPS
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
http-request set-header X-Forwarded-For %[src]
# Route traffic based on HTTP host headers
use_backend backuppc_server if backuppc
default_backend http_server
# 2. Transparent passthrough for encrypted TLS domains
backend passthrough_server
mode tcp
# downstream server handling its own TLS certificates (Calypso)
server calypso 192.168.3.90:443 check
# 3. Intermediary loopback backend
# This forwards traffic to the Layer 7 (HTTP) processing frontend
backend http_dispatcher
mode tcp
server loopback 127.0.0.1:8443 send-proxy-v2
# 4. Layer 7 http frontend (HTTP/1, HTTP/2)
# Receives traffic from the loopback, decrypts SSL, and processes HTTP rules
frontend internal_inbound_http
# pass certs directory path so all pem files will be loaded
bind 127.0.0.1:8443 ssl crt /etc/haproxy/certs/ accept-proxy strict-sni
mode http
# Handle Letsencrypt requests
##http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].${ACCOUNT_THUMBPRINT}\n" if { path_beg '/.well-known/acme-challenge/' }
# Deny traffic based on origin (host aware)
acl trusted_ip src -f /etc/haproxy/whitelist.lst # static whitelist
acl whitelist_ip src,in_table(stick_table_whitelist) # dynamic whitelist
acl backuppc hdr(host) -i bak.condrau.com
http-request deny if backuppc !trusted_ip !whitelist_ip
# Let clients know we support HTTP/3
http-response set-header Alt-Svc 'h3=":443"; ma=86400'
# Inject headers so backend servers know the original request was HTTPS
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Port 443
http-request set-header X-Forwarded-For %[src]
# Route traffic based on HTTP host headers
use_backend backuppc_server if backuppc
default_backend http_server
# 5. HTTP backends
backend backuppc_server
mode http
http-reuse safe
cookie SERVERID insert indirect nocache
server epione 192.168.1.50:8080 check
backend http_server
mode http
http-reuse aggressive
cookie SERVERID insert indirect nocache
server serenity 192.168.3.100:80 check
# 6. stick table that stores IPv4 addresses
backend stick_table_whitelist
mode http
stick-table type ip size 10 expire 1h
# /etc/haproxy/whitelist.lst # Allow static addresses 183.88.218.149 109.205.169.18
# /etc/haproxy/blacklist.lst # Deny malicious IP addresses 148.66.10.94 5.188.62.0/24 54.39.29.64 185.185.134.115 185.46.46.0/24
echo "set table stick_table_whitelist key 192.168.1.99" | socat /run/haproxy/admin.sock stdio
echo "clear table stick_table_whitelist key 192.168.1.99" | socat /run/haproxy/admin.sock stdio
echo "show table stick_table_whitelist" | socat /run/haproxy/admin.sock stdio
HAProxy.cfg because you cannot rely on the IP address being the same when you want to remove the IP address. You can add the following command to update and remove all entries in the stick table:IP=$(curl -s https://api.ipify.org) && echo "set table stick_table_whitelist key $IP" | sudo /usr/bin/socat /run/haproxy/admin.sock stdio echo "clear table stick_table_whitelist" | sudo /usr/bin/socat /run/haproxy/admin.sock stdio
update-ip in /etc/sudoers.d so <user> can execute the command as root without password:vim /etc/sudoers.d/update-ip <user> ALL=(root) NOPASSWD: /usr/bin/socat
#!/bin/bash
# bco, 2026-09-20
# update my current IP address in HAProxy stick table
#
IP=$(curl -s https://api.ipify.org)
# Regex pattern matching 0-255 for individual octets
octet='(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'
pattern="^${octet}\.${octet}\.${octet}\.${octet}$"
if [[ $IP =~ $pattern ]]; then
ssh -p 50322 huahin.condrau.com "echo \"set table stick_table_whitelist key $IP\" | sudo /usr/bin/socat /run/haproxy/admin.sock stdio"
else
echo "Invalid IP address"
sleep 5
exit 1
fi
exit 0
C:\Windows\System32\wsl.exe bash ~/batch/updateIP