HAProxy SSL Termination: Offload TLS Without the Headache
SSL termination at the load balancer is the standard pattern for any non-trivial deployment. HAProxy handles the TLS handshake centrally — your backends speak plain HTTP internally, certificate renewal happens in one place, and you get visibility into the decrypted traffic for logging and routing decisions.
How SSL Termination Works
In a termination setup:
- Client connects to HAProxy over HTTPS (TLS)
- HAProxy decrypts the traffic
- HAProxy forwards plain HTTP to backends
- Response travels back the same path, re-encrypted for the client
The alternative — SSL passthrough — forwards encrypted traffic directly to backends without decryption. Useful when backends must see raw TLS, but you lose the ability to inspect headers or make routing decisions based on content.
Prerequisites
- HAProxy 2.4+ (2.6+ recommended for better TLS 1.3 support)
- OpenSSL 1.1.1+ on the server
- A certificate bundle (fullchain + private key combined into one file)
Certificate Preparation
HAProxy requires the certificate and private key in a single .pem file:
# Combine fullchain + key for HAProxy
cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem \
> /etc/haproxy/certs/example.com.pem
chmod 600 /etc/haproxy/certs/example.com.pem
chown haproxy:haproxy /etc/haproxy/certs/example.com.pem
For Let's Encrypt renewals, add a deploy hook:
# /etc/letsencrypt/renewal-hooks/deploy/haproxy-reload.sh
#!/bin/bash
cat /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/letsencrypt/live/example.com/privkey.pem \
> /etc/haproxy/certs/example.com.pem
chmod 600 /etc/haproxy/certs/example.com.pem
chown haproxy:haproxy /etc/haproxy/certs/example.com.pem
# Reload HAProxy gracefully (no connection drops)
systemctl reload haproxy
chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy-reload.sh
Basic SSL Termination Configuration
global
log /dev/log local0
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
# TLS tuning
tune.ssl.default-dh-param 2048
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
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
timeout http-request 10s
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem alpn h2,http/1.1
bind *:80
# Redirect HTTP to HTTPS
http-request redirect scheme https code 301 unless { ssl_fc }
# Add forwarded headers
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http unless { ssl_fc }
http-request set-header X-Real-IP %[src]
# HSTS header
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
default_backend app_servers
backend app_servers
balance roundrobin
option httpchk GET /healthz
server app1 10.0.1.10:8080 check
server app2 10.0.1.11:8080 check
server app3 10.0.1.12:8080 check
SNI-Based Routing (Multiple Domains)
HAProxy can route to different backends based on the SNI hostname in the TLS handshake — without terminating the connection first (for passthrough), or after termination (for content routing):
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/
# HAProxy loads all .pem files from the directory automatically
# and uses SNI to select the correct certificate
# Route based on hostname after SSL termination
use_backend api_servers if { hdr(host) -i api.example.com }
use_backend admin_servers if { hdr(host) -i admin.example.com }
default_backend app_servers
backend api_servers
balance leastconn
server api1 10.0.2.10:8080 check
server api2 10.0.2.11:8080 check
backend admin_servers
balance roundrobin
server admin1 10.0.3.10:9000 check
backend app_servers
balance roundrobin
server app1 10.0.1.10:8080 check
server app2 10.0.1.11:8080 check
The directory-based certificate loading (crt /etc/haproxy/certs/) is extremely convenient — add a new .pem file and reload HAProxy to serve a new domain without changing config.
SSL Passthrough (When Backends Handle TLS)
When backends must handle their own TLS (e.g., gRPC with mutual TLS, or certain compliance requirements):
frontend tls_passthrough
bind *:443
mode tcp
option tcplog
# Route based on SNI without decrypting
use_backend grpc_backend if { req.ssl_sni -i grpc.example.com }
default_backend https_backend
backend grpc_backend
mode tcp
balance roundrobin
server grpc1 10.0.4.10:443 check
backend https_backend
mode tcp
balance roundrobin
server app1 10.0.1.10:443 check
server app2 10.0.1.11:443 check
Note: in mode tcp, HAProxy can read the SNI from the TLS ClientHello without decrypting it. You lose access to HTTP headers for routing, but backends get unmodified TLS.
Re-Encryption (SSL to Backends)
Some compliance requirements demand encryption even on internal networks. HAProxy can terminate client TLS and establish a new TLS connection to backends:
backend secure_backend
balance roundrobin
option httpchk GET /healthz
# ssl: connect to backends over HTTPS
# verify none: skip backend cert verification (use verify required for strict)
server app1 10.0.1.10:8443 check ssl verify none
server app2 10.0.1.11:8443 check ssl verify none
For strict backend certificate verification:
# Export the CA cert your backends use
cp /path/to/internal-ca.crt /etc/haproxy/certs/internal-ca.crt
backend secure_backend
balance roundrobin
server app1 10.0.1.10:8443 check ssl verify required ca-file /etc/haproxy/certs/internal-ca.crt
Session Persistence with SSL
When using SSL termination with session stickiness, use cookie-based persistence (IP hash is unreliable behind NAT):
backend app_servers
balance roundrobin
cookie SERVERID insert indirect nocache
server app1 10.0.1.10:8080 check cookie app1
server app2 10.0.1.11:8080 check cookie app2
server app3 10.0.1.12:8080 check cookie app3
HAProxy inserts a SERVERID cookie on the first response and routes subsequent requests from that client to the same backend.
Checking SSL Configuration
# Test config syntax
haproxy -c -f /etc/haproxy/haproxy.cfg
# Verify which ciphers are negotiated
openssl s_client -connect example.com:443 -servername example.com 2>&1 | \
grep -E "Protocol|Cipher|Session-ID"
# Check TLS 1.1 is rejected
openssl s_client -connect example.com:443 -tls1_1 2>&1 | grep -E "error|alert"
# Full SSL test
docker run --rm drwetter/testssl.sh example.com
# HAProxy stats socket
echo "show info" | socat stdio /run/haproxy/admin.sock | grep -E "SSL|Uptime"
# Count current SSL sessions
echo "show info" | socat stdio /run/haproxy/admin.sock | grep "SslFrontend"
Performance Tuning for SSL
SSL handshakes are CPU-intensive. Tune for throughput:
global
# Enable TLS session cache to avoid full handshakes on reconnect
tune.ssl.cachesize 20000
tune.ssl.lifetime 300
# Number of SSL handshakes per second (tune based on CPU)
# Default is unlimited; set if you need to protect backend
# tune.ssl.maxrecord 16384
# Use multiple threads for SSL (HAProxy 1.8+)
nbthread 4 # Set to CPU core count
Tuning reference:
| Setting | Purpose | Suggested Value |
|---|---|---|
tune.ssl.cachesize | SSL session cache entries | 20000 (tune up under load) |
tune.ssl.lifetime | Session cache TTL (seconds) | 300 |
tune.ssl.default-dh-param | DH key size | 2048 (4096 for high security) |
nbthread | Worker threads | CPU core count |
After any config change:
# Graceful reload — existing connections finish, new ones use new config
systemctl reload haproxy
Was this article helpful?
Related Articles
HAProxy Load Balancing: From Installation to Production
Configure HAProxy for HTTP and TCP load balancing — installation, frontends, backends, health checks, ACLs, SSL termination, and the stats dashboard.
HAProxy Advanced: SSL Termination, SNI Routing, and mTLS
Advanced HAProxy SSL configuration — multi-domain SNI routing from a single frontend, mutual TLS (mTLS) for service-to-service authentication, certificate management, and OCSP stapling.
HAProxy Configuration Guide: TCP/HTTP Load Balancing, Health Checks, and SSL
A comprehensive HAProxy configuration guide covering frontends, backends, balance algorithms, active health checks, SSL termination, ACLs, rate limiting, and the runtime socket API.
HAProxy Stick Tables For Session Persistence And DDoS Rate Limiting
If you've been running HAProxy in production for any length of time, you've probably hit one of two problems: either your users keep losing their sessions...
HAProxy ACL-Based Routing For Blue-Green Deployments Without Downtime
Blue-green deployments are one of the cleanest ways to ship without waking anyone up at 3am. HAProxy makes this surprisingly straightforward with ACL-based...
Fix HAProxy '503 Service Unavailable' Backend Down
Diagnose and fix HAProxy 503 Service Unavailable errors caused by backend servers failing health checks, connection limits, or misconfigured routing.
More in HAProxy
View all →HAProxy Health Checks and Automatic Failover
Configure HAProxy health checks at TCP, HTTP, and application layers with automatic failover, backup servers, and alerting for production reliability.