HAProxy Security Hardening in Practice: Load Balancer Protection Guide
HAProxy is a high-performance load balancer often exposed to the internet, facing unauthorized access, config leakage, and weak TLS. This guide covers access control, TLS hardening, management-port protection, and log audit with a complete, reproducible HAProxy hardening config.
HAProxy hardening is load-balancer-layer security. HAProxy sits at the traffic edge. Stolen admin access or a config defect can mean traffic hijack, config tampering, or direct backend compromise. This guide is for operators and security staff running HAProxy in production. The config is meant to be applied as-is.
Scope
This approach fits:
- Internet-exposed HAProxy that needs management-port restriction
- HTTPS sites that must disable weak TLS protocols and ciphers
- Source-IP or User-Agent limits in front of backends
- stats page access control plus audit logging
Prerequisites
- HAProxy 2.x or later; confirm with `haproxy -v`
- root or sudo; can edit `/etc/haproxy/haproxy.cfg`
- Valid TLS certificate (e.g. Certbot)
- Syntax-check before changes: `haproxy -c -f /etc/haproxy/haproxy.cfg`
How It Works
Three main risk classes: exposed management plane (stats page unauthenticated and remotely reachable by default); weak transport (TLS 1.0/1.1 or RC4 and similar); missing access control (any source can hit backends). Hardening: HTTP Basic Auth on the management plane, bound to localhost or an internal address; TLS 1.2+ and a tight cipher list on the wire; ACLs on the business plane for source and behavior; logging to close the loop.
Procedure
Step 1: Generate a stats-page password hash
Hash with openssl (do not store plaintext in the config):
# Generate a SHA-256 password hash; replace admin123 with a strong password
printf "admin123" | openssl passwd -6 -salt $(openssl rand -hex 4)
# Output similar to: $6$abcd1234$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Step 2: Write the hardened main config
Edit `/etc/haproxy/haproxy.cfg`. Full example:
global
log /dev/log local0
maxconn 50000
# Drop privileges to reduce root privilege-escalation risk
user haproxy
group haproxy
# stats socket: local administration only
stats socket /var/run/haproxy.sock mode 600 level admin
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5s
timeout client 30s
timeout server 30s
# Hide backend server information
server-state-file /var/run/haproxy.state
# Management page: localhost-only access + Basic Auth
listen stats
bind 127.0.0.1:8404
mode http
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:xxxxxx
stats admin if LOCALHOST
# Frontend: TLS hardening + ACL restrictions
frontend web_front
bind *:443 ssl crt /etc/haproxy/certs/site.pem
# Enable TLS 1.2 and 1.3 only
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384
# Block scanner/crawler User-Agents
acl is_bad_ua hdr_sub(User-Agent) -i sqlmap nikto nmap
http-request deny if is_bad_ua
# Limit concurrent connections from a single source IP
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 100 }
default_backend web_back
backend web_back
balance roundrobin
server web1 192.168.1.10:8080 check inter 3s fall 3 rise 2
server web2 192.168.1.11:8080 check inter 3s fall 3 rise 2
Key knobs: stats auth uses the ciphertext from Step 1; no-tlsv10/no-tlsv11 disable old protocols; stick-table plus `http_req_rate` caps each IP at 100 requests per 10 seconds.
Step 3: Validate and reload
# Syntax check; if Errors found, fix them before proceeding
haproxy -c -f /etc/haproxy/haproxy.cfg
# Graceful reload without interrupting connections
systemctl reload haproxy
Verification
# Verify TLS version: should show TLSv1.2 or TLSv1.3 only
openssl s_client -connect 127.0.0.1:443 -tls1_1 </dev/null 2>&1 | grep -i "protocol"
# Verify the stats page requires authentication
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8404/stats
# Without credentials should return 401; with credentials return 200
curl -s -u admin:password -o /dev/null -w "%{http_code}" http://127.0.0.1:8404/stats
# Verify external access to the management port is denied (run from another host)
nc -vz your-serverIP 8404
FAQ
FAQ 1: stats page unreachable from the admin workstation?
Confirm the bind address is internal or localhost (e.g. `bind 10.0.0.5:8404`). Open the port on the host firewall: `firewall-cmd --add-port=8404/tcp --permanent && firewall-cmd --reload`. Do not bind stats to 0.0.0.0 in production.
FAQ 2: TLS 1.3 breaks older clients?
Keep TLS 1.2 as a compatibility layer and set `ssl-default-bind-ciphers` to ECDHE suites: `ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256`. If TLS 1.0 is still required, assess the business risk and at least enable HSTS against downgrade.
Summary
Three lines: shrink the management plane, harden transport, tighten the business plane. Bind the admin port internally and authenticate it. Force TLS 1.2+ and drop weak ciphers. ACL plus rate tables on the front. Use `haproxy -c` then a graceful reload so traffic stays up. Put this config under change control. Re-run the verification commands after every HAProxy upgrade.