HAProxy Advanced: SSL Termination, SNI Routing, and mTLS
SSL in HAProxy: More Than Just Termination
HAProxy handles SSL/TLS at multiple points:
- SSL termination: Client speaks HTTPS to HAProxy, HAProxy speaks HTTP to backends
- SSL passthrough: HAProxy forwards encrypted traffic without decrypting (Layer 4)
- SSL re-encryption: HAProxy terminates SSL from client, re-encrypts to backend
- mTLS: Both client and server present certificates for mutual authentication
The right choice depends on your security requirements and whether backends can accept plain HTTP.
Certificate Preparation
HAProxy expects certificates in PEM format with the certificate chain and private key in a single file:
# Combine cert + chain + key
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
# Multiple domains: place all .pem files in a directory
ls /etc/haproxy/certs/
# example.com.pem
# api.example.com.pem
# admin.example.com.pem
SSL Termination: Single Domain
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem \
alpn h2,http/1.1 \
ssl-min-ver TLSv1.2
# Security headers
http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
http-response set-header X-Content-Type-Options "nosniff"
http-response set-header X-Frame-Options "SAMEORIGIN"
http-response del-header Server
http-response del-header X-Powered-By
default_backend web_servers
SNI-Based Multi-Domain Routing
Route multiple domains from a single port 443 frontend using SNI (Server Name Indication):
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/
# Use SNI to route to different backends
use_backend api_servers if { ssl_fc_sni -i api.example.com }
use_backend admin_servers if { ssl_fc_sni -i admin.example.com }
use_backend static_servers if { ssl_fc_sni -i static.example.com }
default_backend web_servers
HAProxy automatically selects the right certificate from the certs/ directory based on the SNI hostname. Name the files to match the domain (e.g., api.example.com.pem).
Wildcard Certificates
# Wildcard cert covers *.example.com
cp /etc/letsencrypt/live/example.com/fullchain.pem \
/etc/haproxy/certs/example.com.pem
cat /etc/letsencrypt/live/example.com/privkey.pem >> /etc/haproxy/certs/example.com.pem
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
# Route subdomains to different backends
acl is_api ssl_fc_sni -i api.example.com
acl is_app ssl_fc_sni -m end .example.com
use_backend api_servers if is_api
default_backend web_servers
SSL Passthrough (Layer 4)
When backends handle their own TLS (e.g., HTTPS backends, databases with TLS):
frontend tcp_in
bind *:443
mode tcp
option tcplog
# Route by SNI without decrypting
use_backend api_tls if { req_ssl_sni -i api.example.com }
default_backend web_tls
backend api_tls
mode tcp
server api01 10.0.1.10:443 check
backend web_tls
mode tcp
balance roundrobin
server web01 10.0.1.20:443 check
server web02 10.0.1.21:443 check
Passthrough: HAProxy never sees the plaintext — useful for end-to-end encryption requirements.
SSL Re-encryption (End-to-End TLS)
Terminate SSL at HAProxy and re-encrypt to backends:
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
default_backend secure_backends
backend secure_backends
option httpchk
http-check connect ssl
http-check send meth GET uri /health
server web01 10.0.1.10:443 ssl verify required ca-file /etc/haproxy/certs/ca.pem check
server web02 10.0.1.11:443 ssl verify required ca-file /etc/haproxy/certs/ca.pem check
ssl verify required ensures HAProxy validates the backend certificate — prevents man-in-the-middle.
To skip backend certificate verification (development/internal):
server web01 10.0.1.10:443 ssl verify none check
Mutual TLS (mTLS)
mTLS requires clients to present a certificate, enabling service-to-service authentication:
Generate CA and Client Certificates
# Create CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=Internal CA"
# Generate client certificate
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=service-client"
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt
# Combine for HAProxy
cat ca.crt > /etc/haproxy/certs/ca-chain.pem
HAProxy mTLS Frontend
frontend mtls_api
bind *:8443 ssl crt /etc/haproxy/certs/api.example.com.pem \
ca-file /etc/haproxy/certs/ca-chain.pem \
verify required
# Reject connections without valid client cert
http-request deny unless { ssl_c_verify 0 }
# Pass client cert info to backend
http-request set-header X-Client-CN %{+Q}[ssl_c_s_dn(cn)]
http-request set-header X-Client-Cert %{+Q}[ssl_c_der,base64]
default_backend api_servers
Test with curl
# Client connects with certificate
curl --cert client.crt --key client.key --cacert ca.crt \
https://api.example.com:8443/api/resource
# Without cert — should be rejected
curl --cacert ca.crt https://api.example.com:8443/api/resource
# curl: (56) OpenSSL SSL_read: error:... — connection refused
OCSP Stapling
Reduce TLS handshake latency by including the OCSP response in the handshake (avoids a separate client request to the CA):
# Fetch OCSP response
openssl ocsp \
-issuer /etc/letsencrypt/live/example.com/chain.pem \
-cert /etc/letsencrypt/live/example.com/cert.pem \
-url "$(openssl x509 -noout -ocsp_uri -in /etc/letsencrypt/live/example.com/cert.pem)" \
-respout /etc/haproxy/certs/example.com.ocsp \
-noverify
# Refresh OCSP daily (Let's Encrypt responses are valid 7 days)
cat >> /etc/cron.daily/haproxy-ocsp << 'EOF'
#!/bin/bash
/usr/bin/openssl ocsp \
-issuer /etc/letsencrypt/live/example.com/chain.pem \
-cert /etc/letsencrypt/live/example.com/cert.pem \
-url "$(openssl x509 -noout -ocsp_uri -in /etc/letsencrypt/live/example.com/cert.pem)" \
-respout /etc/haproxy/certs/example.com.ocsp \
-noverify && \
echo "set ssl ocsp-response $(base64 -w 0 /etc/haproxy/certs/example.com.ocsp)" | socat stdio /run/haproxy/admin.sock
EOF
chmod +x /etc/cron.daily/haproxy-ocsp
HAProxy automatically staples the OCSP response when the .ocsp file matches the certificate filename.
Runtime Certificate Updates (No Downtime)
HAProxy 2.2+ supports updating certificates without restarting:
# Update a certificate at runtime
echo "set ssl cert /etc/haproxy/certs/example.com.pem" | \
socat stdio /run/haproxy/admin.sock
# Then push the new cert data
cat /path/to/new-example.com.pem | \
socat stdio /run/haproxy/admin.sock
# Commit the change
echo "commit ssl cert /etc/haproxy/certs/example.com.pem" | \
socat stdio /run/haproxy/admin.sock
# Verify
echo "show ssl cert /etc/haproxy/certs/example.com.pem" | \
socat stdio /run/haproxy/admin.sock
This is essential for cert rotation with Let's Encrypt — no HAProxy restart, no dropped connections.
Was this article helpful?
Network & Traffic Engineer
Packets don't lie. I design and troubleshoot the network layer that everything else depends on — Nginx, Envoy, HAProxy, DNS, CDNs, and everything in between. If it touches a socket, it's my problem.
Related Articles
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 + Keepalived: Production HA Load Balancer Setup
Step-by-step guide to building a highly available load balancer pair with HAProxy and Keepalived — covering the full stack from VIP configuration to health checks, stats, and SSL termination.
Apache httpd: Virtual Hosts, SSL/TLS, and URL Rewriting in Production
How to configure Apache httpd for production use — name-based virtual hosts, SSL/TLS with Let's Encrypt, HTTP to HTTPS redirects, mod_rewrite rules, and performance tuning with MPM.
Envoy Proxy: Architecture, xDS Configuration, and Getting Started
An introduction to Envoy Proxy's architecture — listeners, clusters, filters, and the xDS dynamic configuration API. Covers static configuration for standalone use and how Envoy fits into service meshes like Istio.
Envoy Traffic Management: Retries, Timeouts, Canary Deployments, and Rate Limiting
Advanced Envoy traffic management — configuring retries with exponential backoff, per-request timeouts, weighted canary routing, global rate limiting, and fault injection for resilience testing.
Keepalived and VRRP: Building High-Availability Failover for Linux Services
How to configure Keepalived for automatic failover using VRRP — setting up master/backup pairs, virtual IPs, health-check scripts, and combining it with HAProxy or Nginx for zero-downtime load balancer HA.
More in Nginx
View all →Apache mod_proxy: Reverse Proxy, Load Balancing, and WebSocket Support
How to use Apache httpd as a reverse proxy with mod_proxy — proxying to backend services, load balancing across multiple upstreams, WebSocket proxying, and health check configuration.
Fix Nginx 502 Bad Gateway Behind a Reverse Proxy
Diagnose and fix Nginx 502 Bad Gateway errors when proxying to upstream backends — check sockets, timeouts, and upstream health.
Fix Nginx 'Too Many Open Files' Error
Resolve Nginx 'Too many open files' errors by increasing worker_rlimit_nofile, system ulimits, and kernel file descriptor limits.
Nginx Load Balancing: Round Robin, Least Conn, and IP Hash
Configure Nginx upstream load balancing with round robin, least connections, and IP hash strategies including health checks and failover.