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

Intermediate 35 min Apr 17, 2026 167 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

Need help?

Don't want to manage this yourself?

We handle private cloud infrastructure for businesses that depend on uptime. From initial setup to ongoing operations.