Configure HAProxy to handle SSL termination with automated Let's Encrypt certificates, enabling secure HTTPS load balancing across multiple backend servers. This setup reduces CPU load on backend servers while providing centralized SSL certificate management.
Prerequisites
- Root or sudo access
- Domain name pointing to server
- Backend web servers configured
- Basic knowledge of SSL/TLS concepts
What this solves
SSL termination at the load balancer level allows HAProxy to handle HTTPS connections and forward unencrypted traffic to backend servers. This reduces computational overhead on your application servers while centralizing certificate management. Let's Encrypt provides free SSL certificates with automated renewal, making this solution both secure and cost-effective for production environments.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of all components.
sudo apt update && sudo apt upgrade -y
Install HAProxy and Certbot
Install HAProxy for load balancing and Certbot for Let's Encrypt certificate management.
sudo apt install -y haproxy certbot socat
Configure firewall rules
Open the necessary ports for HTTP, HTTPS, and HAProxy statistics interface.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 8404/tcp
sudo ufw reload
Create SSL certificate directory
Create a dedicated directory for SSL certificates with proper permissions for HAProxy access.
sudo mkdir -p /etc/ssl/certs/haproxy
sudo chown root:haproxy /etc/ssl/certs/haproxy
sudo chmod 750 /etc/ssl/certs/haproxy
Obtain Let's Encrypt certificates
Use Certbot standalone mode to obtain SSL certificates for your domain. Replace example.com with your actual domain.
sudo certbot certonly --standalone --preferred-challenges http --http-01-port 80 -d example.com -d www.example.com
Create combined certificate file
HAProxy requires the certificate and private key in a single file. Create this combined file from the Let's Encrypt certificates.
sudo cat /etc/letsencrypt/live/example.com/fullchain.pem /etc/letsencrypt/live/example.com/privkey.pem | sudo tee /etc/ssl/certs/haproxy/example.com.pem
sudo chmod 640 /etc/ssl/certs/haproxy/example.com.pem
sudo chown root:haproxy /etc/ssl/certs/haproxy/example.com.pem
Configure HAProxy SSL termination
Create the main HAProxy configuration with SSL termination, backend health checks, and security headers.
global
log 127.0.0.1:514 local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
# SSL Configuration
ssl-default-bind-ciphers ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!SHA1:!AESCCM
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-server-ciphers ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!SHA1:!AESCCM
ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets
# Performance tuning
tune.ssl.default-dh-param 2048
tune.bufsize 32768
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
Statistics interface
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
HTTP frontend - redirect to HTTPS
frontend http_frontend
bind *:80
redirect scheme https code 301 if !{ ssl_fc }
HTTPS frontend with SSL termination
frontend https_frontend
bind *:443 ssl crt /etc/ssl/certs/haproxy/example.com.pem
# Security headers
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
http-response set-header X-Frame-Options "DENY"
http-response set-header X-Content-Type-Options "nosniff"
http-response set-header X-XSS-Protection "1; mode=block"
http-response set-header Referrer-Policy "strict-origin-when-cross-origin"
# Rate limiting
stick-table type ip size 100k expire 30s store http_req_rate(10s)
http-request track-sc0 src
http-request deny if { sc_http_req_rate(0) gt 20 }
# Default backend
default_backend web_servers
Backend server configuration
backend web_servers
balance roundrobin
option httpchk GET /health
http-check expect status 200
# Backend servers
server web1 192.168.1.10:80 check inter 3000 rise 2 fall 3
server web2 192.168.1.11:80 check inter 3000 rise 2 fall 3
server web3 192.168.1.12:80 backup check inter 3000 rise 2 fall 3
Create certificate renewal script
Create an automated script to renew certificates and reload HAProxy configuration.
#!/bin/bash
DOMAIN="example.com"
CERT_PATH="/etc/letsencrypt/live/$DOMAIN"
HAPROXY_CERT_PATH="/etc/ssl/certs/haproxy/$DOMAIN.pem"
LOG_FILE="/var/log/haproxy-cert-renewal.log"
Function to log messages
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
Renew certificates
log_message "Starting certificate renewal for $DOMAIN"
Stop HAProxy temporarily for standalone renewal
sudo systemctl stop haproxy
Renew certificate
if sudo certbot renew --standalone --preferred-challenges http --http-01-port 80; then
log_message "Certificate renewal successful"
# Create combined certificate file
if sudo cat "$CERT_PATH/fullchain.pem" "$CERT_PATH/privkey.pem" | sudo tee "$HAPROXY_CERT_PATH" > /dev/null; then
sudo chmod 640 "$HAPROXY_CERT_PATH"
sudo chown root:haproxy "$HAPROXY_CERT_PATH"
log_message "Combined certificate file created successfully"
else
log_message "ERROR: Failed to create combined certificate file"
exit 1
fi
# Start HAProxy
if sudo systemctl start haproxy; then
log_message "HAProxy restarted successfully"
else
log_message "ERROR: Failed to restart HAProxy"
exit 1
fi
# Verify HAProxy is running
if sudo systemctl is-active --quiet haproxy; then
log_message "Certificate renewal process completed successfully"
else
log_message "ERROR: HAProxy is not running after restart"
exit 1
fi
else
log_message "ERROR: Certificate renewal failed"
sudo systemctl start haproxy
exit 1
fi
Make renewal script executable
Set proper permissions for the certificate renewal script.
sudo chmod 750 /usr/local/bin/renew-haproxy-certs.sh
sudo chown root:haproxy /usr/local/bin/renew-haproxy-certs.sh
Configure automatic certificate renewal
Create a systemd timer for automated certificate renewal every 30 days.
[Unit]
Description=Renew HAProxy SSL certificates
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/renew-haproxy-certs.sh
User=root
Group=haproxy
Create systemd timer
Configure the timer to run certificate renewal monthly.
[Unit]
Description=Run HAProxy certificate renewal monthly
Requires=haproxy-cert-renewal.service
[Timer]
OnCalendar=monthly
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
Enable and test the renewal system
Enable the systemd timer and test the renewal script.
sudo systemctl daemon-reload
sudo systemctl enable haproxy-cert-renewal.timer
sudo systemctl start haproxy-cert-renewal.timer
sudo systemctl status haproxy-cert-renewal.timer
Validate and start HAProxy
Check the configuration syntax and start HAProxy with SSL termination.
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl enable --now haproxy
sudo systemctl status haproxy
Configure SSL security hardening
Add additional security configurations for enhanced SSL protection.
# SSL Security Parameters
Include this in your main haproxy.cfg global section
DH Parameters
ssl-dh-param-file /etc/ssl/certs/dhparam.pem
SSL Session Cache
tune.ssl.cachesize 100000
tune.ssl.lifetime 300
tune.ssl.maxrecord 1460
Security Headers Template
http-response set-header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; media-src 'self'; frame-src 'none';"
http-response set-header Permissions-Policy "geolocation=(), microphone=(), camera=()"
http-response set-header X-Permitted-Cross-Domain-Policies "none"
Generate DH parameters
Create strong Diffie-Hellman parameters for enhanced SSL security.
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
sudo chmod 644 /etc/ssl/certs/dhparam.pem
Verify your setup
Test SSL termination, certificate validity, and load balancer functionality.
# Check HAProxy status and logs
sudo systemctl status haproxy
sudo journalctl -u haproxy -f --no-pager
Test SSL certificate
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
Verify SSL configuration
curl -I https://example.com
Check HAProxy statistics
curl http://localhost:8404/stats
Test backend connectivity
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Monitor certificate expiration
sudo certbot certificates
Performance optimization
Configure connection tuning
Optimize HAProxy for high-traffic environments with advanced connection settings.
# Add to global section for performance optimization
nbproc 1
nbthread 4
cpu-map auto:1/1-4 0-3
Advanced connection settings
tune.maxaccept 100
tune.http.maxhdr 100
tune.ssl.default-dh-param 2048
Memory optimization
tune.bufsize 32768
tune.maxrewrite 8192
global.maxconn 10000
Implement health check monitoring
Configure comprehensive health checks for backend servers with custom endpoints.
# Enhanced backend configuration
backend web_servers
balance roundrobin
option httpchk GET /health HTTP/1.1\r\nHost:\ example.com
http-check expect string "OK"
# Advanced health check settings
option tcp-check
option log-health-checks
# Server configurations with detailed health checks
server web1 192.168.1.10:80 check inter 5000 rise 3 fall 2 maxconn 500 weight 100
server web2 192.168.1.11:80 check inter 5000 rise 3 fall 2 maxconn 500 weight 100
server web3 192.168.1.12:80 check inter 5000 rise 3 fall 2 maxconn 250 weight 50 backup
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| SSL handshake failures | Combined certificate file missing or incorrect permissions | Recreate combined cert: sudo cat /etc/letsencrypt/live/example.com/{fullchain,privkey}.pem > /etc/ssl/certs/haproxy/example.com.pem |
| 503 Service Unavailable | Backend servers unreachable or health checks failing | Check backend server status and health check endpoint: curl http://backend-ip/health |
| Certificate renewal fails | Port 80 blocked or HAProxy not stopped during renewal | Ensure firewall allows port 80 and script stops HAProxy: sudo systemctl stop haproxy && sudo certbot renew |
| HAProxy won't start | Configuration syntax errors | Validate configuration: sudo haproxy -c -f /etc/haproxy/haproxy.cfg |
| Stats page not accessible | Firewall blocking port 8404 | Open stats port: sudo ufw allow 8404/tcp or sudo firewall-cmd --add-port=8404/tcp |
| Mixed content warnings | Backend serving HTTP resources over HTTPS | Configure backend to serve HTTPS or update application URLs to relative paths |
Next steps
- Implement HAProxy rate limiting and DDoS protection with advanced security rules for enhanced security
- Monitor HAProxy and Consul with Prometheus and Grafana dashboards for comprehensive monitoring
- Configure HAProxy with Consul for dynamic service discovery for automated backend management
- Implement HAProxy WAF integration with ModSecurity for application-level protection
- Configure HAProxy multi-site SSL termination with SNI for hosting multiple domains
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
# Variables
DOMAIN=""
BACKEND_IPS=""
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Usage function
usage() {
echo "Usage: $0 -d DOMAIN [-b BACKEND_IPS]"
echo " -d DOMAIN Domain name for SSL certificate (e.g., example.com)"
echo " -b BACKEND_IPS Comma-separated backend server IPs (default: 127.0.0.1:8080)"
echo "Example: $0 -d example.com -b 192.168.1.10:8080,192.168.1.11:8080"
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
cleanup() {
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log_error "Installation failed. Cleaning up..."
systemctl stop haproxy 2>/dev/null || true
systemctl disable haproxy 2>/dev/null || true
fi
exit $exit_code
}
trap cleanup ERR
# Check if running as root
check_root() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
}
# Detect distribution
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
PKG_UPGRADE="apt upgrade -y"
FIREWALL_CMD="ufw"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update || true"
PKG_INSTALL="dnf install -y"
PKG_UPGRADE="dnf upgrade -y"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update || true"
PKG_INSTALL="yum install -y"
PKG_UPGRADE="yum upgrade -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
}
# Configure firewall
configure_firewall() {
if command -v ufw >/dev/null 2>&1 && [[ "$FIREWALL_CMD" == "ufw" ]]; then
ufw allow 80/tcp
ufw allow 443/tcp
ufw allow 8404/tcp
ufw --force enable
elif command -v firewall-cmd >/dev/null 2>&1 && [[ "$FIREWALL_CMD" == "firewall-cmd" ]]; 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
else
log_warning "No supported firewall found. Please manually open ports 80, 443, and 8404"
fi
}
# Generate HAProxy configuration
generate_haproxy_config() {
local backends=""
IFS=',' read -ra BACKEND_ARRAY <<< "$BACKEND_IPS"
local i=1
for backend in "${BACKEND_ARRAY[@]}"; do
backends+=" server web$i $backend check\n"
((i++))
done
cat > /etc/haproxy/haproxy.cfg << EOF
global
log 127.0.0.1:514 local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
# SSL Configuration
ssl-default-bind-ciphers ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!SHA1:!AESCCM
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-server-ciphers ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!SHA1:!AESCCM
ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets
# Performance tuning
tune.ssl.default-dh-param 2048
tune.bufsize 32768
defaults
mode http
log global
option httplog
option dontlognull
option http-server-close
option forwardfor except 127.0.0.0/8
option redispatch
retries 3
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
frontend http_frontend
bind *:80
redirect scheme https code 301 if !{ ssl_fc }
frontend https_frontend
bind *:443 ssl crt /etc/ssl/certs/haproxy/$DOMAIN.pem
# Security headers
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
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"
default_backend web_servers
backend web_servers
balance roundrobin
option httpchk GET /health
$(echo -e "$backends")
EOF
}
# Create certificate renewal script
create_renewal_script() {
cat > /usr/local/bin/haproxy-ssl-renew << 'EOF'
#!/bin/bash
DOMAIN=$1
certbot renew --pre-hook "systemctl stop haproxy" --post-hook "systemctl start haproxy"
if [ $? -eq 0 ]; then
cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem > /etc/ssl/certs/haproxy/$DOMAIN.pem
chown root:haproxy /etc/ssl/certs/haproxy/$DOMAIN.pem
chmod 640 /etc/ssl/certs/haproxy/$DOMAIN.pem
systemctl reload haproxy
fi
EOF
chmod 755 /usr/local/bin/haproxy-ssl-renew
}
# Parse command line arguments
while getopts "d:b:h" opt; do
case $opt in
d) DOMAIN="$OPTARG" ;;
b) BACKEND_IPS="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
# Validate arguments
if [[ -z "$DOMAIN" ]]; then
log_error "Domain is required"
usage
fi
if [[ -z "$BACKEND_IPS" ]]; then
BACKEND_IPS="127.0.0.1:8080"
log_warning "No backend IPs specified, using default: $BACKEND_IPS"
fi
# Main installation
main() {
log_info "[1/10] Checking prerequisites..."
check_root
detect_distro
log_info "[2/10] Updating system packages..."
$PKG_UPDATE
$PKG_UPGRADE
log_info "[3/10] Installing HAProxy and Certbot..."
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL haproxy certbot socat
else
$PKG_INSTALL epel-release
$PKG_INSTALL haproxy certbot socat
fi
log_info "[4/10] Configuring firewall rules..."
configure_firewall
log_info "[5/10] Creating SSL certificate directory..."
mkdir -p /etc/ssl/certs/haproxy
chown root:haproxy /etc/ssl/certs/haproxy
chmod 750 /etc/ssl/certs/haproxy
log_info "[6/10] Stopping HAProxy for certificate generation..."
systemctl stop haproxy 2>/dev/null || true
log_info "[7/10] Obtaining Let's Encrypt certificates..."
certbot certonly --standalone --preferred-challenges http --http-01-port 80 \
--non-interactive --agree-tos --register-unsafely-without-email \
-d "$DOMAIN" -d "www.$DOMAIN"
log_info "[8/10] Creating combined certificate file..."
cat "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" \
"/etc/letsencrypt/live/$DOMAIN/privkey.pem" > "/etc/ssl/certs/haproxy/$DOMAIN.pem"
chmod 640 "/etc/ssl/certs/haproxy/$DOMAIN.pem"
chown root:haproxy "/etc/ssl/certs/haproxy/$DOMAIN.pem"
log_info "[9/10] Generating HAProxy configuration..."
generate_haproxy_config
log_info "Creating certificate renewal script..."
create_renewal_script
# Add cron job for certificate renewal
(crontab -l 2>/dev/null; echo "0 2 * * 0 /usr/local/bin/haproxy-ssl-renew $DOMAIN") | crontab -
log_info "[10/10] Starting and enabling HAProxy..."
systemctl enable haproxy
systemctl start haproxy
# Verification
log_info "Verifying installation..."
sleep 3
if systemctl is-active --quiet haproxy; then
log_success "HAProxy is running successfully"
else
log_error "HAProxy failed to start"
exit 1
fi
if ss -tlnp | grep -q ":443.*haproxy" && ss -tlnp | grep -q ":80.*haproxy"; then
log_success "HAProxy is listening on ports 80 and 443"
else
log_error "HAProxy is not listening on expected ports"
exit 1
fi
log_success "Installation completed successfully!"
log_info "HAProxy SSL termination is now configured for domain: $DOMAIN"
log_info "Backend servers: $BACKEND_IPS"
log_info "Statistics interface available at: http://$(hostname -I | awk '{print $1}'):8404/stats"
log_info "Certificate will auto-renew via cron job every Sunday at 2 AM"
}
main "$@"
Review the script before running. Execute with: bash install.sh