Set up HAProxy 2.8 with SSL termination, automatic Let's Encrypt certificates, and security headers for high-performance load balancing with HTTPS offloading and automated certificate renewal.
Prerequisites
- Root or sudo access
- Domain name with DNS configured
- Backend web servers running
- Basic understanding of SSL/TLS
What this solves
HAProxy SSL termination offloads encryption from your backend servers while providing centralized certificate management. This tutorial configures HAProxy 2.8 with automatic Let's Encrypt certificates, modern cipher suites, security headers including HSTS, and health monitoring for production-ready HTTPS load balancing.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of HAProxy and dependencies.
sudo apt update && sudo apt upgrade -y
sudo apt install -y software-properties-common
Install HAProxy 2.8
Install HAProxy 2.8 from the official repository to get the latest features and security updates.
sudo add-apt-repository ppa:vbernat/haproxy-2.8 -y
sudo apt update
sudo apt install -y haproxy=2.8.*
Install Certbot for Let's Encrypt
Install Certbot to automatically obtain and renew SSL certificates from Let's Encrypt.
sudo apt install -y certbot python3-certbot-dns-cloudflare
Create directory structure
Set up directories for certificates and HAProxy configuration files with proper permissions.
sudo mkdir -p /etc/haproxy/certs
sudo mkdir -p /etc/haproxy/conf.d
sudo mkdir -p /var/lib/haproxy
sudo chown -R haproxy:haproxy /var/lib/haproxy
sudo chmod 755 /etc/haproxy/certs
Obtain Let's Encrypt certificates
Use Certbot to obtain SSL certificates for your domains. This example uses standalone mode for initial setup.
sudo systemctl stop haproxy
sudo certbot certonly --standalone -d example.com -d www.example.com --email admin@example.com --agree-tos --non-interactive
sudo systemctl start haproxy
Create certificate bundle script
HAProxy requires certificates in PEM format combining the certificate and private key. Create a script to generate the bundle.
#!/bin/bash
HAProxy Certificate Bundle Script
DOMAIN="example.com"
CERT_DIR="/etc/letsencrypt/live/${DOMAIN}"
HAPROXY_CERT_DIR="/etc/haproxy/certs"
Combine certificate and private key for HAProxy
cat ${CERT_DIR}/fullchain.pem ${CERT_DIR}/privkey.pem > ${HAPROXY_CERT_DIR}/${DOMAIN}.pem
Set proper permissions
chmod 600 ${HAPROXY_CERT_DIR}/${DOMAIN}.pem
chown haproxy:haproxy ${HAPROXY_CERT_DIR}/${DOMAIN}.pem
Test HAProxy configuration
haproxy -c -f /etc/haproxy/haproxy.cfg
if [ $? -eq 0 ]; then
# Reload HAProxy if configuration is valid
systemctl reload haproxy
echo "Certificate bundle updated and HAProxy reloaded successfully"
else
echo "HAProxy configuration test failed. Not reloading."
exit 1
fi
Make script executable and run it
Set execute permissions and create the initial certificate bundle for HAProxy.
sudo chmod +x /usr/local/bin/haproxy-cert-bundle.sh
sudo /usr/local/bin/haproxy-cert-bundle.sh
Configure HAProxy with SSL termination
Create the main HAProxy configuration with SSL termination, modern cipher suites, and security headers.
global
log stdout local0 info
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-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
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
ssl-default-server-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
ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
mode http
log global
option httplog
option dontlognull
option log-health-checks
option forwardfor
option http-server-close
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
Statistics interface
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
stats auth admin:secure-password-here
HTTP to HTTPS redirect
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/haproxy/certs/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"
http-response set-header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';"
# 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 web_servers
Backend servers
backend web_servers
balance roundrobin
option httpchk GET /health
http-check expect status 200
server web1 192.168.1.10:80 check inter 2000 rise 2 fall 3
server web2 192.168.1.11:80 check inter 2000 rise 2 fall 3
server web3 192.168.1.12:80 check backup
Create certificate renewal hook
Configure Certbot to automatically update HAProxy certificates when they're renewed.
#!/bin/bash
Certbot renewal hook for HAProxy
/usr/local/bin/haproxy-cert-bundle.sh
if [ $? -eq 0 ]; then
logger "HAProxy certificates updated successfully"
else
logger "HAProxy certificate update failed"
exit 1
fi
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
Enable and start HAProxy
Enable HAProxy to start automatically on boot and start the service.
sudo systemctl enable haproxy
sudo systemctl start haproxy
sudo systemctl status haproxy
Configure automatic certificate renewal
Set up a systemd timer for automatic certificate renewal instead of relying on cron.
[Unit]
Description=Certbot Renewal Service
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --no-self-upgrade
PrivateTmp=true
[Unit]
Description=Certbot Renewal Timer
Requires=certbot-renewal.service
[Timer]
OnCalendar=--* 03:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable certbot-renewal.timer
sudo systemctl start certbot-renewal.timer
Configure firewall rules
Open the necessary ports for HTTP, HTTPS, and HAProxy stats interface.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow from 192.168.1.0/24 to any port 8404
sudo ufw --force enable
Configure advanced security features
Set up rate limiting and DDoS protection
Configure advanced rate limiting to protect against abuse and DDoS attacks.
# Rate limiting configuration
backend rate_limit_abuse
stick-table type ip size 1m expire 10m store gpc0,http_req_rate(10s)
http-request deny if { src_get_gpc0 gt 0 }
http-request track-sc1 src
http-request sc-inc-gpc0(1) if { sc1_http_req_rate gt 100 }
Geographic blocking (example)
acl blocked_countries src_is_loc CN RU
http-request deny if blocked_countries
User-Agent filtering
acl bad_bots hdr_sub(User-Agent) -i bot crawler spider
http-request deny if bad_bots { nbsrv(web_servers) lt 2 }
Include security configuration
Add the security configuration to the main HAProxy config file.
sudo echo 'include /etc/haproxy/conf.d/*.cfg' >> /etc/haproxy/haproxy.cfg
Configure health checks and monitoring
Set up comprehensive health monitoring for backend servers and SSL certificates.
#!/bin/bash
HAProxy health check script
LOG_FILE="/var/log/haproxy-health.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
Check HAProxy status
if ! systemctl is-active --quiet haproxy; then
echo "[$DATE] CRITICAL: HAProxy is not running" >> $LOG_FILE
systemctl restart haproxy
exit 1
fi
Check certificate expiration (30 days warning)
CERT_FILE="/etc/haproxy/certs/example.com.pem"
if [ -f "$CERT_FILE" ]; then
EXPIRY_DATE=$(openssl x509 -in $CERT_FILE -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
CURRENT_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - CURRENT_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt 30 ]; then
echo "[$DATE] WARNING: SSL certificate expires in $DAYS_LEFT days" >> $LOG_FILE
fi
fi
Check backend server health
echo "show stat" | socat stdio /run/haproxy/admin.sock | grep -v "^#" | while IFS=',' read -r line; do
SERVER=$(echo $line | cut -d',' -f1-2)
STATUS=$(echo $line | cut -d',' -f18)
if [[ "$STATUS" == "DOWN" ]]; then
echo "[$DATE] WARNING: Backend server $SERVER is DOWN" >> $LOG_FILE
fi
done
echo "[$DATE] Health check completed" >> $LOG_FILE
sudo chmod +x /usr/local/bin/haproxy-health-check.sh
Set up monitoring cron job
Schedule regular health checks to monitor HAProxy and certificate status.
echo '/5 * root /usr/local/bin/haproxy-health-check.sh' | sudo tee -a /etc/crontab
Verify your setup
Test your HAProxy SSL termination configuration and verify all components are working correctly.
# Check HAProxy status and configuration
sudo systemctl status haproxy
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
Test SSL certificate
openssl s509 -in /etc/haproxy/certs/example.com.pem -text -noout
Check certificate expiration
openssl x509 -in /etc/haproxy/certs/example.com.pem -noout -dates
Test HTTPS connection
curl -I https://example.com
Check security headers
curl -I https://example.com | grep -E "Strict-Transport-Security|X-Frame-Options|X-Content-Type-Options"
Test HTTP to HTTPS redirect
curl -I http://example.com
Check HAProxy stats
curl -u admin:secure-password-here http://your-server-ip:8404/stats
Verify certificate renewal timer
sudo systemctl status certbot-renewal.timer
sudo systemctl list-timers certbot-renewal.timer
Your HAProxy SSL termination should now be working with automatic certificate renewal. You can access the stats interface at http://your-server-ip:8404/stats to monitor backend server health and connection statistics.
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| SSL handshake errors | Certificate bundle format incorrect | Verify certificate bundle contains both cert and private key: openssl x509 -in /etc/haproxy/certs/example.com.pem -text -noout |
| Certificate renewal fails | Port 80/443 blocked during renewal | Use webroot or DNS challenge: certbot renew --webroot -w /var/www/html |
| Backend servers marked down | Health check endpoint not responding | Verify health check URL returns 200: curl -I http://192.168.1.10/health |
| HAProxy won't start | Configuration syntax error | Test configuration: sudo haproxy -c -f /etc/haproxy/haproxy.cfg |
| Stats page not accessible | Firewall blocking port 8404 | Allow stats port: sudo ufw allow from trusted_network to any port 8404 |
| Rate limiting too aggressive | Thresholds set too low | Adjust rate limits in frontend: http-request deny if { sc_http_req_rate(0) gt 50 } |
| Certificate permissions denied | Wrong ownership on certificate files | Fix ownership: sudo chown haproxy:haproxy /etc/haproxy/certs/*.pem |
| SSL ciphers too restrictive | Old browsers can't connect | Add intermediate ciphers to ssl-default-bind-ciphers in global section |
Next steps
- Implement HAProxy WAF integration with ModSecurity 3 for advanced threat protection - Add web application firewall capabilities
- Implement HAProxy rate limiting and DDoS protection with advanced security rules - Enhanced protection against attacks
- Monitor HAProxy and Consul with Prometheus and Grafana dashboards - Set up comprehensive monitoring
- Configure HAProxy multi-site SSL termination with wildcard certificates - Handle multiple domains efficiently
- Set up HAProxy blue-green deployments with zero downtime switching - Implement advanced deployment strategies
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
DOMAIN=""
EMAIL=""
BACKEND_IP="127.0.0.1:8080"
usage() {
echo "Usage: $0 -d DOMAIN -e EMAIL [-b BACKEND_IP]"
echo " -d DOMAIN Domain name for SSL certificate"
echo " -e EMAIL Email for Let's Encrypt registration"
echo " -b BACKEND_IP Backend server IP:PORT (default: 127.0.0.1:8080)"
exit 1
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
cleanup() {
if [[ $? -ne 0 ]]; then
error "Installation failed. Cleaning up..."
systemctl stop haproxy 2>/dev/null || true
rm -f /etc/haproxy/haproxy.cfg.backup
if [[ -f /etc/haproxy/haproxy.cfg.original ]]; then
mv /etc/haproxy/haproxy.cfg.original /etc/haproxy/haproxy.cfg
fi
fi
}
trap cleanup ERR
check_prerequisites() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
exit 1
fi
if ! command -v curl &> /dev/null; then
error "curl is required but not installed"
exit 1
fi
}
detect_distro() {
if [[ ! -f /etc/os-release ]]; then
error "Cannot detect distribution"
exit 1
fi
. /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 makecache"
PKG_INSTALL="dnf install -y"
PKG_UPGRADE="dnf update -y"
FIREWALL_CMD="firewall-cmd"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum makecache"
PKG_INSTALL="yum install -y"
PKG_UPGRADE="yum update -y"
FIREWALL_CMD="firewall-cmd"
;;
*)
error "Unsupported distribution: $ID"
exit 1
;;
esac
}
while getopts "d:e:b:h" opt; do
case $opt in
d) DOMAIN="$OPTARG" ;;
e) EMAIL="$OPTARG" ;;
b) BACKEND_IP="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
if [[ -z "$DOMAIN" || -z "$EMAIL" ]]; then
usage
fi
check_prerequisites
detect_distro
echo "[1/10] Updating system packages..."
$PKG_UPDATE
$PKG_UPGRADE
echo "[2/10] Installing dependencies..."
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL software-properties-common curl gnupg
else
$PKG_INSTALL curl gnupg2 epel-release
fi
echo "[3/10] Installing HAProxy 2.8..."
if [[ "$PKG_MGR" == "apt" ]]; then
add-apt-repository ppa:vbernat/haproxy-2.8 -y
apt update
$PKG_INSTALL haproxy=2.8.*
else
$PKG_INSTALL haproxy
fi
echo "[4/10] Installing Certbot..."
$PKG_INSTALL certbot
echo "[5/10] Creating directory structure..."
mkdir -p /etc/haproxy/certs
mkdir -p /etc/haproxy/conf.d
mkdir -p /var/lib/haproxy
mkdir -p /run/haproxy
chown -R haproxy:haproxy /var/lib/haproxy
chown haproxy:haproxy /etc/haproxy/certs
chmod 750 /etc/haproxy/certs
chmod 755 /var/lib/haproxy
echo "[6/10] Configuring firewall..."
if command -v ufw &> /dev/null; then
ufw allow 80/tcp
ufw allow 443/tcp
ufw --force enable
elif command -v firewall-cmd &> /dev/null; then
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
fi
echo "[7/10] Obtaining Let's Encrypt certificate..."
systemctl stop haproxy 2>/dev/null || true
certbot certonly --standalone -d "$DOMAIN" --email "$EMAIL" --agree-tos --non-interactive --force-renewal
echo "[8/10] Creating certificate bundle script..."
cat > /usr/local/bin/haproxy-cert-bundle.sh << 'EOF'
#!/bin/bash
set -euo pipefail
DOMAIN="$1"
CERT_DIR="/etc/letsencrypt/live/${DOMAIN}"
HAPROXY_CERT_DIR="/etc/haproxy/certs"
if [[ ! -d "$CERT_DIR" ]]; then
echo "Certificate directory not found: $CERT_DIR"
exit 1
fi
cat "${CERT_DIR}/fullchain.pem" "${CERT_DIR}/privkey.pem" > "${HAPROXY_CERT_DIR}/${DOMAIN}.pem"
chmod 640 "${HAPROXY_CERT_DIR}/${DOMAIN}.pem"
chown haproxy:haproxy "${HAPROXY_CERT_DIR}/${DOMAIN}.pem"
if haproxy -c -f /etc/haproxy/haproxy.cfg; then
systemctl reload haproxy
echo "Certificate bundle updated and HAProxy reloaded successfully"
else
echo "HAProxy configuration test failed. Not reloading."
exit 1
fi
EOF
chmod 750 /usr/local/bin/haproxy-cert-bundle.sh
/usr/local/bin/haproxy-cert-bundle.sh "$DOMAIN"
echo "[9/10] Configuring HAProxy..."
if [[ -f /etc/haproxy/haproxy.cfg ]]; then
cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.original
fi
cat > /etc/haproxy/haproxy.cfg << EOF
global
log stdout local0 info
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
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
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
mode http
log global
option httplog
option dontlognull
option log-health-checks
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 ssl_frontend
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/${DOMAIN}.pem
redirect scheme https code 301 if !{ ssl_fc }
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"
http-response set-header Referrer-Policy "strict-origin-when-cross-origin"
default_backend web_servers
backend web_servers
balance roundrobin
option httpchk GET /health
http-check expect status 200
server web1 ${BACKEND_IP} check
listen stats
bind *:8404
stats enable
stats uri /stats
stats refresh 30s
stats admin if TRUE
EOF
haproxy -c -f /etc/haproxy/haproxy.cfg
echo "[10/10] Starting and enabling services..."
systemctl enable haproxy
systemctl start haproxy
cat > /etc/cron.d/haproxy-certbot << EOF
0 2 * * * root certbot renew --quiet && /usr/local/bin/haproxy-cert-bundle.sh ${DOMAIN}
EOF
log "Verifying installation..."
sleep 2
if systemctl is-active --quiet haproxy; then
log "HAProxy is running successfully"
else
error "HAProxy failed to start"
exit 1
fi
if curl -s -o /dev/null -w "%{http_code}" "https://${DOMAIN}" | grep -q "200\|301\|302"; then
log "SSL termination is working correctly"
else
warn "SSL endpoint may not be responding correctly"
fi
log "HAProxy SSL termination installation completed!"
log "Domain: $DOMAIN"
log "Backend: $BACKEND_IP"
log "Stats available at: https://$DOMAIN:8404/stats"
log "Certificate auto-renewal configured via cron"
Review the script before running. Execute with: bash install.sh