Configure HAProxy SSL termination with Let's Encrypt and security headers

Intermediate 35 min Apr 17, 2026 662 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

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
sudo dnf update -y
sudo dnf install -y epel-release

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.*
sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo dnf install -y haproxy

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
sudo dnf 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
Note: Replace example.com with your actual domain names. The standalone mode temporarily uses port 80/443, so HAProxy must be stopped during certificate generation.

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
Important: Replace example.com with your domain, update the stats password, and configure your actual backend server IPs and health check endpoints.

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
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="8404" accept'
sudo firewall-cmd --reload

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

SymptomCauseFix
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

Running this in production?

Want this handled for you? Setting this up once is straightforward. Keeping it patched, monitored, backed up and performant across environments is the harder part. See how we run infrastructure like this for European teams.

Automated install script

Run this to automate the entire setup

Prefere não gerir isto sozinho?

Gerimos a infraestrutura de empresas que dependem do tempo de atividade. Totalmente gerida, com um contacto fixo que conhece o seu ambiente.

Tem um contacto fixo que conhece o seu ambiente

Roterdão 22:55 · acessível por mensagem, sem formulário de tickets