Set up HAProxy to handle SSL certificates for multiple domains using Server Name Indication (SNI), enabling secure HTTPS traffic termination and load balancing across different backend services.
Prerequisites
- Root or sudo access
- Multiple SSL certificates for different domains
- Backend web servers running on HTTP
- Basic understanding of SSL/TLS certificates
What this solves
This tutorial shows you how to configure HAProxy to terminate SSL/HTTPS connections for multiple websites using Server Name Indication (SNI). You'll set up certificate management, secure cipher suites, and backend health checks for production-grade load balancing. This is essential when you need to serve HTTPS traffic for multiple domains through a single load balancer.
Step-by-step configuration
Install HAProxy
Install HAProxy with SSL support from your distribution's package manager.
sudo apt update
sudo apt install -y haproxy ssl-cert
Create SSL certificate directory
Create a secure directory structure for storing SSL certificates that HAProxy can access.
sudo mkdir -p /etc/ssl/haproxy
sudo chmod 750 /etc/ssl/haproxy
sudo chown root:haproxy /etc/ssl/haproxy
Prepare SSL certificates
HAProxy requires certificates in PEM format with the private key and certificate chain concatenated. Create certificate files for each domain.
# For each domain, combine cert and key into single PEM file
sudo cat /path/to/example.com.crt /path/to/example.com.key > /etc/ssl/haproxy/example.com.pem
sudo cat /path/to/api.example.com.crt /path/to/api.example.com.key > /etc/ssl/haproxy/api.example.com.pem
sudo cat /path/to/app.example.com.crt /path/to/app.example.com.key > /etc/ssl/haproxy/app.example.com.pem
/etc/letsencrypt/live/domain/. The fullchain.pem contains the certificate chain, and privkey.pem contains the private key.Set certificate permissions
Secure the certificate files with appropriate permissions for HAProxy access.
sudo chmod 640 /etc/ssl/haproxy/*.pem
sudo chown root:haproxy /etc/ssl/haproxy/*.pem
Backup original configuration
Create a backup of the default HAProxy configuration before making changes.
sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.backup
Configure HAProxy global settings
Configure the global section with SSL security settings and performance tuning.
global
daemon
user haproxy
group haproxy
pidfile /var/run/haproxy.pid
# SSL Configuration
ssl-default-bind-ciphers ECDHE+aRSA+AES256+GCM+SHA384:ECDHE+aRSA+AES128+GCM+SHA256:ECDHE+aRSA+AES256+SHA384:ECDHE+aRSA+AES128+SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-server-ciphers ECDHE+aRSA+AES256+GCM+SHA384:ECDHE+aRSA+AES128+GCM+SHA256:ECDHE+aRSA+AES256+SHA384:ECDHE+aRSA+AES128+SHA256
ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets
# Performance tuning
tune.ssl.default-dh-param 2048
tune.bufsize 32768
# Logging
log 127.0.0.1:514 local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
option httplog
option dontlognull
option http-server-close
option forwardfor
option redispatch
retries 3
# Security headers
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"
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
Configure HTTPS frontend with SNI
Set up the frontend to handle HTTPS traffic and route requests based on the SNI hostname.
# HTTPS Frontend with SNI
frontend https_frontend
bind *:443 ssl crt /etc/ssl/haproxy/
mode http
# Redirect HTTP to HTTPS (optional)
redirect scheme https if !{ ssl_fc }
# SNI-based routing
use_backend example_backend if { ssl_fc_sni example.com }
use_backend api_backend if { ssl_fc_sni api.example.com }
use_backend app_backend if { ssl_fc_sni app.example.com }
# Default backend for unmatched domains
default_backend default_backend
HTTP Frontend (redirect to HTTPS)
frontend http_frontend
bind *:80
mode http
redirect scheme https code 301
Configure backend server pools
Define backend server pools with health checks and load balancing algorithms.
# Backend for example.com
backend example_backend
mode http
balance roundrobin
option httpchk GET /health
http-check expect status 200
server web1 192.168.1.10:80 check inter 5s fall 3 rise 2
server web2 192.168.1.11:80 check inter 5s fall 3 rise 2
server web3 192.168.1.12:80 check inter 5s fall 3 rise 2
Backend for api.example.com
backend api_backend
mode http
balance leastconn
option httpchk GET /api/health
http-check expect status 200
server api1 192.168.1.20:8080 check inter 10s fall 3 rise 2
server api2 192.168.1.21:8080 check inter 10s fall 3 rise 2
Backend for app.example.com
backend app_backend
mode http
balance source
option httpchk GET /status
http-check expect status 200
server app1 192.168.1.30:3000 check inter 5s fall 3 rise 2
server app2 192.168.1.31:3000 check inter 5s fall 3 rise 2
server app3 192.168.1.32:3000 check inter 5s fall 3 rise 2
Default backend for unmatched domains
backend default_backend
mode http
http-request deny
Configure HAProxy statistics
Enable the statistics interface for monitoring backend health and performance.
# Statistics interface
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
stats auth admin:your-secure-password-here
Configure rsyslog for HAProxy logging
Set up dedicated logging for HAProxy to track SSL termination and backend health.
# HAProxy logging configuration
$ModLoad imudp
$UDPServerRun 514
$UDPServerAddress 127.0.0.1
HAProxy log files
local0.* /var/log/haproxy.log
& stop
Restart rsyslog and test configuration
Apply the logging configuration and validate the HAProxy configuration syntax.
sudo systemctl restart rsyslog
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Enable and start HAProxy
Start HAProxy and enable it to start automatically on boot.
sudo systemctl enable haproxy
sudo systemctl start haproxy
sudo systemctl status haproxy
Configure firewall rules
Open the necessary ports for HTTPS, HTTP, and statistics interface.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8404/tcp
sudo ufw reload
Verify your setup
Test SSL termination, SNI routing, and backend health checks to ensure everything works correctly.
# Check HAProxy status and processes
sudo systemctl status haproxy
ps aux | grep haproxy
Test SSL certificate loading
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Test HTTPS connectivity for each domain
curl -I https://example.com
curl -I https://api.example.com
curl -I https://app.example.com
Check SSL certificate details
openssl s_client -connect example.com:443 -servername example.com < /dev/null
Verify backend server health
curl -s http://your-haproxy-server:8404/stats
Check HAProxy logs
sudo tail -f /var/log/haproxy.log
Configure advanced SSL security
Generate DH parameters
Generate stronger Diffie-Hellman parameters for enhanced SSL security.
sudo openssl dhparam -out /etc/ssl/haproxy/dhparams.pem 2048
sudo chmod 644 /etc/ssl/haproxy/dhparams.pem
Update HAProxy with DH parameters
Configure HAProxy to use the custom DH parameters for SSL connections.
# Add to global section
global
# existing configuration...
ssl-dh-param-file /etc/ssl/haproxy/dhparams.pem
# Enhanced cipher suites
ssl-default-bind-ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options prefer-client-ciphers ssl-min-ver TLSv1.2 no-tls-tickets
Implement OCSP stapling
Enable OCSP stapling for better SSL performance and security validation.
# Update certificate files with OCSP stapling
sudo mkdir -p /etc/ssl/haproxy/ocsp
For each certificate, fetch OCSP response (example for one domain)
sudo openssl x509 -in /etc/ssl/haproxy/example.com.pem -text -noout | grep -A4 "OCSP"
Update HAProxy configuration
sudo systemctl reload haproxy
Set up certificate automation
Create certificate renewal script
Automate certificate renewal and HAProxy reload for Let's Encrypt certificates.
#!/bin/bash
Certificate renewal script for HAProxy
set -e
CERT_DIR="/etc/ssl/haproxy"
LETSENCRYPT_DIR="/etc/letsencrypt/live"
Array of domains
DOMAINS=("example.com" "api.example.com" "app.example.com")
Function to combine cert and key
combine_cert() {
local domain=$1
if [ -f "$LETSENCRYPT_DIR/$domain/fullchain.pem" ] && [ -f "$LETSENCRYPT_DIR/$domain/privkey.pem" ]; then
cat "$LETSENCRYPT_DIR/$domain/fullchain.pem" "$LETSENCRYPT_DIR/$domain/privkey.pem" > "$CERT_DIR/$domain.pem"
chmod 640 "$CERT_DIR/$domain.pem"
chown root:haproxy "$CERT_DIR/$domain.pem"
echo "Combined certificate for $domain"
else
echo "Certificate files not found for $domain"
exit 1
fi
}
Renew certificates
certbot renew --quiet
Combine certificates
for domain in "${DOMAINS[@]}"; do
combine_cert "$domain"
done
Test configuration and reload HAProxy
if haproxy -c -f /etc/haproxy/haproxy.cfg; then
systemctl reload haproxy
echo "HAProxy reloaded successfully"
else
echo "HAProxy configuration test failed"
exit 1
fi
Make script executable and set up cron
Schedule automatic certificate renewal to run weekly.
sudo chmod +x /usr/local/bin/renew-haproxy-certs.sh
Add to root crontab
sudo crontab -e
Add this line to run weekly on Sundays at 2 AM
0 2 0 /usr/local/bin/renew-haproxy-certs.sh >> /var/log/cert-renewal.log 2>&1
Monitor and troubleshoot
Set up log rotation
Configure log rotation for HAProxy logs to prevent disk space issues.
/var/log/haproxy.log {
daily
rotate 30
missingok
notifempty
compress
delaycompress
postrotate
/bin/kill -HUP $(cat /var/run/rsyslogd.pid 2>/dev/null) 2>/dev/null || true
/bin/kill -USR1 $(cat /var/run/haproxy.pid 2>/dev/null) 2>/dev/null || true
endscript
}
Create monitoring script
Set up basic monitoring to check backend health and SSL certificate expiration.
#!/bin/bash
HAProxy monitoring script
set -e
Check HAProxy status
if ! systemctl is-active --quiet haproxy; then
echo "CRITICAL: HAProxy is not running"
exit 2
fi
Check SSL certificate expiration (30 days warning)
for cert in /etc/ssl/haproxy/*.pem; do
if [ -f "$cert" ]; then
domain=$(basename "$cert" .pem)
expiry_date=$(openssl x509 -in "$cert" -noout -enddate | cut -d= -f2)
expiry_seconds=$(date -d "$expiry_date" +%s)
current_seconds=$(date +%s)
days_until_expiry=$(( (expiry_seconds - current_seconds) / 86400 ))
if [ $days_until_expiry -lt 30 ]; then
echo "WARNING: Certificate for $domain expires in $days_until_expiry days"
else
echo "OK: Certificate for $domain expires in $days_until_expiry days"
fi
fi
done
Check backend server health
curl -s http://localhost:8404/stats | grep -E "(UP|DOWN)" | while read line; do
echo "Backend status: $line"
done
echo "HAProxy monitoring complete"
Make monitoring script executable
Set permissions and test the monitoring script.
sudo chmod +x /usr/local/bin/monitor-haproxy.sh
sudo /usr/local/bin/monitor-haproxy.sh
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| HAProxy fails to start with SSL error | Certificate file format or permissions issue | Check sudo haproxy -c -f /etc/haproxy/haproxy.cfg and verify certificate format |
| SNI routing not working | Certificate not matching domain or missing in directory | Verify certificate filename matches domain and check ssl_fc_sni conditions |
| Backend servers showing as DOWN | Health check endpoint not responding or network issue | Test health check URL manually and verify backend server connectivity |
| SSL handshake failures | Cipher suite mismatch or TLS version incompatibility | Review cipher configuration and check client TLS support |
| High CPU usage during SSL termination | Insufficient DH parameters or cipher suite inefficiency | Generate 2048-bit DH params and optimize cipher suites |
| Statistics page not accessible | Firewall blocking port 8404 or authentication failure | Check firewall rules and verify stats authentication credentials |
Next steps
- Configure HAProxy SSL termination with Let's Encrypt and security headers
- Configure Prometheus Alertmanager with email notifications for production monitoring
- Set up HAProxy monitoring with Prometheus and Grafana dashboards
- Implement HAProxy rate limiting and DDoS protection with advanced security rules
- Configure advanced HAProxy backend health checks and automatic failover
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
HAPROXY_SSL_DIR="/etc/ssl/haproxy"
HAPROXY_CONFIG="/etc/haproxy/haproxy.cfg"
DOMAINS=()
BACKEND_IPS=()
PKG_MGR=""
PKG_INSTALL=""
# Usage message
usage() {
echo "Usage: $0 -d domain1,domain2,domain3 -b backend_ip1,backend_ip2"
echo " -d: Comma-separated list of domains (required)"
echo " -b: Comma-separated list of backend server IPs (required)"
echo " -h: Show this help message"
echo ""
echo "Example: $0 -d example.com,api.example.com,app.example.com -b 10.0.1.10,10.0.1.11"
exit 1
}
# Logging functions
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Cleanup function for rollback
cleanup() {
if [[ $? -ne 0 ]]; then
log_error "Installation failed. Rolling back changes..."
if [[ -f "$HAPROXY_CONFIG.backup" ]]; then
mv "$HAPROXY_CONFIG.backup" "$HAPROXY_CONFIG"
fi
systemctl stop haproxy 2>/dev/null || true
fi
}
trap cleanup ERR
# Check if running as root or with sudo
check_privileges() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
}
# Detect distribution and package manager
detect_distro() {
if [[ ! -f /etc/os-release ]]; then
log_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
;;
fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
log_info "Detected distribution: $ID ($VERSION_ID)"
}
# Parse command line arguments
parse_arguments() {
while getopts "d:b:h" opt; do
case $opt in
d)
IFS=',' read -ra DOMAINS <<< "$OPTARG"
;;
b)
IFS=',' read -ra BACKEND_IPS <<< "$OPTARG"
;;
h)
usage
;;
\?)
log_error "Invalid option: -$OPTARG"
usage
;;
esac
done
if [[ ${#DOMAINS[@]} -eq 0 ]] || [[ ${#BACKEND_IPS[@]} -eq 0 ]]; then
log_error "Both domains (-d) and backend IPs (-b) are required"
usage
fi
}
# Install HAProxy with SSL support
install_haproxy() {
echo "[1/8] Installing HAProxy..."
if [[ "$PKG_MGR" == "apt" ]]; then
apt update
$PKG_INSTALL haproxy ssl-cert
else
$PKG_INSTALL haproxy openssl
fi
# Verify installation
if ! command -v haproxy &> /dev/null; then
log_error "HAProxy installation failed"
exit 1
fi
log_success "HAProxy installed successfully"
}
# Create SSL certificate directory
create_ssl_directory() {
echo "[2/8] Creating SSL certificate directory..."
mkdir -p "$HAPROXY_SSL_DIR"
chmod 750 "$HAPROXY_SSL_DIR"
if getent group haproxy > /dev/null 2>&1; then
chown root:haproxy "$HAPROXY_SSL_DIR"
else
log_warning "haproxy group not found, using root ownership"
chown root:root "$HAPROXY_SSL_DIR"
fi
log_success "SSL directory created: $HAPROXY_SSL_DIR"
}
# Generate self-signed certificates for demonstration
generate_demo_certificates() {
echo "[3/8] Generating demo SSL certificates..."
for domain in "${DOMAINS[@]}"; do
if [[ ! -f "$HAPROXY_SSL_DIR/$domain.pem" ]]; then
log_info "Generating self-signed certificate for $domain"
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout "/tmp/$domain.key" \
-out "/tmp/$domain.crt" \
-subj "/C=US/ST=State/L=City/O=Organization/CN=$domain" 2>/dev/null
# Combine certificate and key
cat "/tmp/$domain.crt" "/tmp/$domain.key" > "$HAPROXY_SSL_DIR/$domain.pem"
# Set secure permissions
chmod 640 "$HAPROXY_SSL_DIR/$domain.pem"
if getent group haproxy > /dev/null 2>&1; then
chown root:haproxy "$HAPROXY_SSL_DIR/$domain.pem"
else
chown root:root "$HAPROXY_SSL_DIR/$domain.pem"
fi
# Clean up temporary files
rm -f "/tmp/$domain.key" "/tmp/$domain.crt"
fi
done
log_success "Demo certificates generated"
log_warning "Replace demo certificates with real ones in production!"
}
# Backup original configuration
backup_configuration() {
echo "[4/8] Backing up original configuration..."
if [[ -f "$HAPROXY_CONFIG" ]]; then
cp "$HAPROXY_CONFIG" "$HAPROXY_CONFIG.backup"
log_success "Configuration backed up to $HAPROXY_CONFIG.backup"
fi
}
# Generate HAProxy configuration
generate_haproxy_config() {
echo "[5/8] Generating HAProxy configuration..."
cat > "$HAPROXY_CONFIG" << 'EOF'
global
daemon
user haproxy
group haproxy
pidfile /var/run/haproxy.pid
# SSL Configuration
ssl-default-bind-ciphers ECDHE+aRSA+AES256+GCM+SHA384:ECDHE+aRSA+AES128+GCM+SHA256:ECDHE+aRSA+AES256+SHA384:ECDHE+aRSA+AES128+SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-server-ciphers ECDHE+aRSA+AES256+GCM+SHA384:ECDHE+aRSA+AES128+GCM+SHA256:ECDHE+aRSA+AES256+SHA384:ECDHE+aRSA+AES128+SHA256
ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets
# Performance tuning
tune.ssl.default-dh-param 2048
tune.bufsize 32768
# Logging
log 127.0.0.1:514 local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
option httplog
option dontlognull
option http-server-close
option forwardfor
option redispatch
retries 3
# Security headers
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"
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
# HTTP Frontend - Redirect to HTTPS
frontend http_frontend
bind *:80
mode http
redirect scheme https
# HTTPS Frontend with SNI
frontend https_frontend
bind *:443 ssl crt /etc/ssl/haproxy/
mode http
EOF
# Add SNI routing rules for each domain
for i in "${!DOMAINS[@]}"; do
echo " use_backend backend_$i if { ssl_fc_sni ${DOMAINS[$i]} }" >> "$HAPROXY_CONFIG"
done
echo "" >> "$HAPROXY_CONFIG"
# Generate backend configurations
for i in "${!DOMAINS[@]}"; do
cat >> "$HAPROXY_CONFIG" << EOF
# Backend for ${DOMAINS[$i]}
backend backend_$i
mode http
balance roundrobin
option httpchk GET /
http-check expect status 200
EOF
for j in "${!BACKEND_IPS[@]}"; do
echo " server web$((j+1)) ${BACKEND_IPS[$j]}:80 check" >> "$HAPROXY_CONFIG"
done
echo "" >> "$HAPROXY_CONFIG"
done
# Add stats interface
cat >> "$HAPROXY_CONFIG" << 'EOF'
# Stats interface
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
EOF
log_success "HAProxy configuration generated"
}
# Configure firewall
configure_firewall() {
echo "[6/8] Configuring firewall..."
if command -v ufw &> /dev/null && ufw status | grep -q "Status: active"; then
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 8404/tcp
log_success "UFW firewall configured"
elif command -v firewall-cmd &> /dev/null; then
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --permanent --add-port=443/tcp
firewall-cmd --permanent --add-port=8404/tcp
firewall-cmd --reload
log_success "firewalld configured"
else
log_warning "No active firewall detected. Manually open ports 80, 443, and 8404"
fi
}
# Start and enable HAProxy service
start_haproxy_service() {
echo "[7/8] Starting HAProxy service..."
# Test configuration first
if ! haproxy -c -f "$HAPROXY_CONFIG"; then
log_error "HAProxy configuration test failed"
exit 1
fi
systemctl enable haproxy
systemctl restart haproxy
if systemctl is-active --quiet haproxy; then
log_success "HAProxy service started and enabled"
else
log_error "Failed to start HAProxy service"
exit 1
fi
}
# Verify installation
verify_installation() {
echo "[8/8] Verifying installation..."
# Check if HAProxy is running
if ! systemctl is-active --quiet haproxy; then
log_error "HAProxy is not running"
exit 1
fi
# Check if ports are listening
if ! ss -tlnp | grep -q ":443"; then
log_error "HAProxy is not listening on port 443"
exit 1
fi
if ! ss -tlnp | grep -q ":80"; then
log_error "HAProxy is not listening on port 80"
exit 1
fi
log_success "Installation verified successfully"
echo ""
log_info "HAProxy multi-site SSL termination configured for domains:"
for domain in "${DOMAINS[@]}"; do
echo " - https://$domain"
done
echo ""
log_info "Stats interface available at: http://your-server-ip:8404/stats"
echo ""
log_warning "Remember to:"
log_warning "1. Replace demo SSL certificates with real ones"
log_warning "2. Update backend server configurations as needed"
log_warning "3. Configure proper DNS records for your domains"
}
# Main execution
main() {
check_privileges
parse_arguments "$@"
detect_distro
install_haproxy
create_ssl_directory
generate_demo_certificates
backup_configuration
generate_haproxy_config
configure_firewall
start_haproxy_service
verify_installation
log_success "HAProxy multi-site SSL termination setup completed!"
}
main "$@"
Review the script before running. Execute with: bash install.sh