Symptoms
SSL termination is one of the most operationally critical roles HAProxy performs, and it is also one of the most common sources of production incidents. When SSL termination breaks, the failure mode depends on where in the TLS handshake the problem occurs, which means symptoms can vary widely across client types, operating systems, and browser versions.
- Browsers display ERR_SSL_PROTOCOL_ERROR, ERR_SSL_VERSION_OR_CIPHER_MISMATCH, or SSL_ERROR_RX_RECORD_TOO_LONG
- curl returns SSL certificate problem: unable to get local issuer certificate or SSL handshake failed
- HAProxy logs show entries ending with SSL handshake failure and a backend of <NOSRV>
- Clients receive browser certificate warnings about an untrusted issuer or a hostname mismatch
- Specific browsers, TLS libraries, or device types fail the handshake while others succeed
- The wrong certificate is served for a given virtual host or domain name
- HAProxy refuses to start, logging SSL_CTX_use_PrivateKey_file failed or unable to load SSL private key
- Intermittent handshake failures affecting only a subset of incoming connections
- All traffic on port 443 drops immediately after a certificate renewal or HAProxy config change
Root Cause 1: Wrong Certificate Loaded
Why It Happens
HAProxy loads SSL certificates from PEM files named in the bind directive. When multiple certificates exist under
/etc/haproxy/certs/, it is straightforward to reference the wrong file — for example after a renewal that created a file with a new timestamp in its name, or when an automation pipeline wrote a staging certificate to the production path. HAProxy will load and serve whatever file you point it to without validating that the certificate CN or SANs match the intended domain.
How to Identify It
Inspect what certificate HAProxy is currently presenting to clients:
openssl s_client -connect 192.168.10.50:443 -servername solvethenetwork.com < /dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Output when the wrong certificate is loaded:
subject=CN = staging.solvethenetwork.com
issuer=C = US, O = Let's Encrypt, CN = R3
notBefore=Jan 1 00:00:00 2026 GMT
notAfter=Apr 1 00:00:00 2026 GMT
The CN shows staging.solvethenetwork.com instead of solvethenetwork.com. Confirm which file HAProxy is referencing in the configuration:
grep -n "crt" /etc/haproxy/haproxy.cfg
12: bind 192.168.10.50:443 ssl crt /etc/haproxy/certs/staging-solvethenetwork.pem
How to Fix It
Verify the correct PEM file exists and contains the right certificate:
openssl x509 -in /etc/haproxy/certs/solvethenetwork.pem -noout -subject -dates
Update the bind directive to point to the correct file:
frontend https_front
bind 192.168.10.50:443 ssl crt /etc/haproxy/certs/solvethenetwork.pem
default_backend web_servers
Validate the configuration and reload without dropping connections:
haproxy -c -f /etc/haproxy/haproxy.cfg && systemctl reload haproxy
For environments with many virtual hosts, use HAProxy's directory-based certificate loading combined with SNI selection so each domain is automatically matched to its certificate by filename:
frontend https_front
bind 192.168.10.50:443 ssl crt /etc/haproxy/certs/
default_backend web_servers
Root Cause 2: Incomplete Certificate Chain
Why It Happens
TLS requires the server to transmit not only its own leaf certificate but also every intermediate CA certificate in the chain up to (but not including) a trusted root. When the PEM file contains only the leaf certificate without intermediates, clients that do not have those intermediate certificates already cached will fail chain verification. This is a common regression after certificate renewals where automation copies only the
cert.pemfile rather than
fullchain.pem, or when a certificate is manually exported from a management platform that separates the chain into a different file.
How to Identify It
Initiate an SSL handshake and look at the chain depth and verification errors:
openssl s_client -connect 192.168.10.50:443 -servername solvethenetwork.com
A broken chain produces output similar to:
CONNECTED(00000003)
depth=0 CN = solvethenetwork.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = solvethenetwork.com
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
0 s:CN = solvethenetwork.com
i:C = US, O = Let's Encrypt, CN = R3
---
Only depth=0 appears — the intermediate R3 is not being sent by the server. Count the certificates inside the PEM file to confirm:
grep -c "BEGIN CERTIFICATE" /etc/haproxy/certs/solvethenetwork.pem
1
A correctly chained PEM for a Let's Encrypt certificate should return 2. For a certificate with two intermediates, expect 3.
How to Fix It
Rebuild the PEM file by concatenating the leaf certificate with the intermediate bundle. HAProxy expects leaf first, then intermediates, then the private key:
cat /etc/letsencrypt/live/solvethenetwork.com/fullchain.pem \
/etc/letsencrypt/live/solvethenetwork.com/privkey.pem \
> /etc/haproxy/certs/solvethenetwork.pem
If you are not using Certbot, concatenate manually:
cat solvethenetwork.crt intermediate.crt > /tmp/chain.pem
cat /tmp/chain.pem solvethenetwork.key > /etc/haproxy/certs/solvethenetwork.pem
Verify the chain depth is now correct:
grep -c "BEGIN CERTIFICATE" /etc/haproxy/certs/solvethenetwork.pem
2
Reload HAProxy and re-run the s_client check — the output should now show the complete chain:
Certificate chain
0 s:CN = solvethenetwork.com
i:C = US, O = Let's Encrypt, CN = R3
1 s:C = US, O = Let's Encrypt, CN = R3
i:C = Digital Signature Trust Co., O = DST Root CA X3
---
Verify return code: 0 (ok)
Root Cause 3: Cipher Mismatch
Why It Happens
HAProxy allows operators to restrict the TLS cipher suite to a hardened subset using the ssl-default-bind-ciphers directive for TLS 1.2 and below, and ssl-default-bind-ciphersuites for TLS 1.3. When these are configured too aggressively — for example, restricting to only two ECDHE-GCM ciphers — older clients, embedded devices running legacy OpenSSL, or Java applications that have not been updated will fail the handshake because the ClientHello and ServerHello share no common cipher. The connection drops immediately after the ServerHello with a TLS handshake_failure alert.
How to Identify It
Attempt a connection forcing a cipher that is not in HAProxy's allowed list:
openssl s_client -connect 192.168.10.50:443 \
-cipher "AES256-SHA" \
-servername solvethenetwork.com
CONNECTED(00000003)
140362483714368:error:14077410:SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure
Check what ciphers are configured in haproxy.cfg:
grep -E "cipher|ciphersuites" /etc/haproxy/haproxy.cfg
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256
List the ciphers that are currently allowed to understand the scope of the restriction:
openssl ciphers -v 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256' | awk '{print $1}'
ECDHE-ECDSA-AES128-GCM-SHA256
ECDHE-RSA-AES128-GCM-SHA256
Only two ciphers are enabled, which will reject the majority of legacy clients.
How to Fix It
Adopt the Mozilla SSL Configuration Generator Intermediate profile, which balances strong security with broad client compatibility:
global
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 prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
After reloading, confirm the negotiated cipher for a normal client:
openssl s_client -connect 192.168.10.50:443 \
-servername solvethenetwork.com 2>/dev/null | grep "Cipher is"
Cipher is ECDHE-RSA-AES256-GCM-SHA384
Root Cause 4: TLS Version Restricted
Why It Happens
Security hardening guides routinely recommend disabling TLS 1.0 and 1.1 in HAProxy using no-tlsv10 and no-tlsv11 in
ssl-default-bind-options. This is correct practice. However, operators sometimes go further and set ssl-min-ver TLSv1.3, which excludes all TLS 1.2 clients — including modern browsers that fall back to TLS 1.2 and many HTTP client libraries. The result is a connection drop for any client that does not support TLS 1.3, which in 2026 still includes a non-trivial portion of enterprise software, embedded systems, and IoT endpoints.
How to Identify It
Test connectivity using specific TLS versions to identify which are accepted:
openssl s_client -connect 192.168.10.50:443 -tls1_2 \
-servername solvethenetwork.com < /dev/null 2>&1 | grep -E "Protocol|alert|error"
139921406453568:error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version
openssl s_client -connect 192.168.10.50:443 -tls1_3 \
-servername solvethenetwork.com < /dev/null 2>&1 | grep "Protocol"
Protocol : TLSv1.3
TLS 1.3 succeeds while TLS 1.2 is rejected. Confirm the configuration is the cause:
grep -E "ssl-min-ver|no-tlsv|no-sslv" /etc/haproxy/haproxy.cfg
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets ssl-min-ver TLSv1.3
Check HAProxy logs for the volume of affected clients:
journalctl -u haproxy --since "30 minutes ago" | grep -i "handshake" | wc -l
847
That volume of handshake failures in 30 minutes confirms a systemic version mismatch, not an isolated client issue.
How to Fix It
Set the minimum TLS version to TLSv1.2 in the global block, which is the current industry-accepted baseline for security and compatibility:
global
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
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
If you need TLS 1.3-only enforcement on a specific frontend while keeping TLS 1.2 available elsewhere, override at the bind level:
frontend https_internal
bind 192.168.10.51:443 ssl crt /etc/haproxy/certs/solvethenetwork.pem ssl-min-ver TLSv1.3
frontend https_public
bind 192.168.10.50:443 ssl crt /etc/haproxy/certs/solvethenetwork.pem ssl-min-ver TLSv1.2
Verify TLS 1.2 connections now succeed:
openssl s_client -connect 192.168.10.50:443 -tls1_2 \
-servername solvethenetwork.com < /dev/null 2>&1 | grep "Protocol"
Protocol : TLSv1.2
Root Cause 5: SNI Routing Wrong
Why It Happens
Server Name Indication allows a single HAProxy frontend to serve multiple SSL certificates on one IP address. HAProxy selects the certificate to present based on the SNI hostname value in the TLS ClientHello. When ACLs using req.ssl_sni contain typos in hostname strings, use incorrect matching flags, or are evaluated in the wrong order, the ACL never matches and HAProxy falls through to
default_backend, serving the default certificate. Clients connecting to a virtual host that needs its own certificate instead receive the default one, triggering a CN mismatch error. Additionally, when HAProxy is configured in TCP mode for pass-through routing,
req.ssl_sniis available during the TCP phase but some operators attempt to use HTTP-layer ACLs against it, which never fire.
How to Identify It
Test which certificate is served for each expected SNI value:
openssl s_client -connect 192.168.10.50:443 \
-servername api.solvethenetwork.com < /dev/null 2>/dev/null \
| openssl x509 -noout -subject
subject=CN = solvethenetwork.com
The apex domain certificate is being served for api.solvethenetwork.com — a clear mismatch. Inspect the ACL configuration:
grep -A 10 "frontend https_front" /etc/haproxy/haproxy.cfg
frontend https_front
bind 192.168.10.50:443 ssl crt /etc/haproxy/certs/
acl is_api req.ssl_sni -i api.solvethenework.com
use_backend api_servers if is_api
default_backend web_servers
The ACL contains a typo: api.solvethenework.com is missing the letter t. Because this ACL never matches, every request falls through to
default_backendand receives the default certificate. Also check that the PEM files exist for each expected domain when using directory-based loading:
ls -1 /etc/haproxy/certs/
solvethenetwork.com.pem
portal.solvethenetwork.com.pem
The file api.solvethenetwork.com.pem is missing entirely, so even fixing the typo would still result in the default certificate being served.
How to Fix It
Correct all ACL hostname values and verify the PEM files exist for every expected domain:
frontend https_front
bind 192.168.10.50:443 ssl crt /etc/haproxy/certs/
acl is_api req.ssl_sni -i api.solvethenetwork.com
acl is_portal req.ssl_sni -i portal.solvethenetwork.com
use_backend api_servers if is_api
use_backend portal_servers if is_portal
default_backend web_servers
Rebuild and place the missing PEM file:
cat /etc/letsencrypt/live/api.solvethenetwork.com/fullchain.pem \
/etc/letsencrypt/live/api.solvethenetwork.com/privkey.pem \
> /etc/haproxy/certs/api.solvethenetwork.com.pem
chmod 640 /etc/haproxy/certs/api.solvethenetwork.com.pem
chown infrarunbook-admin:infrarunbook-admin /etc/haproxy/certs/api.solvethenetwork.com.pem
After reloading, run a systematic verification across all expected SNI values:
for domain in solvethenetwork.com api.solvethenetwork.com portal.solvethenetwork.com; do
echo -n "$domain -> "
openssl s_client -connect 192.168.10.50:443 -servername $domain < /dev/null 2>/dev/null \
| openssl x509 -noout -subject
done
solvethenetwork.com -> subject=CN = solvethenetwork.com
api.solvethenetwork.com -> subject=CN = api.solvethenetwork.com
portal.solvethenetwork.com -> subject=CN = portal.solvethenetwork.com
Root Cause 6: Certificate and Private Key Mismatch
Why It Happens
HAProxy PEM files must contain both the certificate and its corresponding private key. A mismatch occurs when a certificate is renewed and the new certificate is inadvertently paired with the old private key, or when the PEM file is assembled by concatenating files in the wrong order or from different renewal cycles. HAProxy detects this at startup and refuses to load, but operators sometimes work around this by restoring a backup certificate to an older key, reintroducing the mismatch.
How to Identify It
HAProxy logs a clear error on startup:
systemctl status haproxy
haproxy[3421]: [ALERT] SSL_CTX_use_PrivateKey_file('/etc/haproxy/certs/solvethenetwork.pem') failed.
haproxy[3421]: [ALERT] unable to load SSL private key from PEM file '/etc/haproxy/certs/solvethenetwork.pem'
Verify the mismatch by comparing the modulus of the certificate and the key. For a matched pair, both MD5 hashes must be identical:
openssl x509 -noout -modulus -in /etc/haproxy/certs/solvethenetwork.pem | openssl md5
(stdin)= a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
openssl rsa -noout -modulus -in /etc/haproxy/certs/solvethenetwork.pem | openssl md5
(stdin)= 9z8y7x6w5v4u3t2s1r0q9p8o7n6m5l4k
The hashes differ — the certificate and key are from different pairs.
How to Fix It
Rebuild the PEM file using the correct matching certificate and private key from the same issuance:
cat /etc/letsencrypt/live/solvethenetwork.com/fullchain.pem \
/etc/letsencrypt/live/solvethenetwork.com/privkey.pem \
> /etc/haproxy/certs/solvethenetwork.pem
chmod 640 /etc/haproxy/certs/solvethenetwork.pem
chown infrarunbook-admin:infrarunbook-admin /etc/haproxy/certs/solvethenetwork.pem
Confirm the moduli now match before starting HAProxy:
diff <(openssl x509 -noout -modulus -in /etc/haproxy/certs/solvethenetwork.pem | openssl md5) \
<(openssl rsa -noout -modulus -in /etc/haproxy/certs/solvethenetwork.pem | openssl md5)
No output from diff means the pair is valid. Start HAProxy:
haproxy -c -f /etc/haproxy/haproxy.cfg && systemctl start haproxy
Root Cause 7: Expired Certificate
Why It Happens
HAProxy serves an expired certificate without issuing any startup warning or log alert. This is a silent failure mode that becomes visible only when clients reject the connection. It is especially common with Let's Encrypt certificates, which have a 90-day validity period and depend on fully automated renewal. When the renewal cron job fails silently — due to DNS propagation issues, port 80 being blocked, or disk space exhaustion preventing file writes — the certificate expires unnoticed until clients start failing.
How to Identify It
openssl s_client -connect 192.168.10.50:443 -servername solvethenetwork.com \
< /dev/null 2>/dev/null | openssl x509 -noout -dates
notBefore=Jan 1 00:00:00 2026 GMT
notAfter=Mar 31 00:00:00 2026 GMT
The notAfter date is in the past. Perform a programmatic expiry check on the on-disk PEM file:
openssl x509 -in /etc/haproxy/certs/solvethenetwork.pem -noout -checkend 0
Certificate will expire
How to Fix It
Force a certificate renewal and rebuild the HAProxy PEM file:
certbot renew --cert-name solvethenetwork.com --force-renewal
cat /etc/letsencrypt/live/solvethenetwork.com/fullchain.pem \
/etc/letsencrypt/live/solvethenetwork.com/privkey.pem \
> /etc/haproxy/certs/solvethenetwork.pem
systemctl reload haproxy
Verify the new expiry date:
openssl x509 -in /etc/haproxy/certs/solvethenetwork.pem -noout -dates
notBefore=Apr 6 00:00:00 2026 GMT
notAfter=Jul 5 00:00:00 2026 GMT
Root Cause 8: Missing or Weak DH Parameters
Why It Happens
DHE cipher suites require Diffie-Hellman parameters to be available. On some systems and older HAProxy builds, the default DH parameters are 1024 bits, which modern browsers and security scanners reject as insufficiently secure. Clients enforcing a minimum DH key size of 2048 bits will abort the handshake with a weak key error. The PEM file can include custom DH parameters or HAProxy can be directed to a separate DH params file to resolve this.
How to Identify It
Run an SSL Labs-style check locally using testssl.sh or verify via openssl:
openssl s_client -connect 192.168.10.50:443 \
-cipher DHE-RSA-AES256-GCM-SHA384 \
-servername solvethenetwork.com < /dev/null 2>&1 | grep -E "Server Temp Key|dh"
Server Temp Key: DH, 1024 bits
A 1024-bit DH parameter will cause failures with browsers enforcing the RFC 7919 minimum. Check whether custom DH parameters are configured:
grep -E "dhparam|tune.ssl" /etc/haproxy/haproxy.cfg
(no output)
How to Fix It
Generate a 2048-bit DH parameter file and append it to the PEM file or reference it globally:
openssl dhparam -out /etc/haproxy/dhparam.pem 2048
cat /etc/haproxy/dhparam.pem >> /etc/haproxy/certs/solvethenetwork.pem
Alternatively, reference the DH params at the global level so they apply to all frontends:
global
tune.ssl.default-dh-param 2048
After reloading, verify the DH key size has increased:
openssl s_client -connect 192.168.10.50:443 \
-cipher DHE-RSA-AES256-GCM-SHA384 \
-servername solvethenetwork.com < /dev/null 2>&1 | grep "Server Temp Key"
Server Temp Key: DH, 2048 bits
Prevention
The majority of HAProxy SSL termination incidents are avoidable with consistent operational practices and automated verification.
- Automate certificate renewal end-to-end. Use Certbot deploy hooks to automatically rebuild the HAProxy PEM file and trigger a graceful reload immediately after every successful renewal. Place the hook script in
/etc/letsencrypt/renewal-hooks/deploy/rebuild-haproxy-pem.sh
and make it executable. Include a modulus comparison check inside the hook so mismatches are caught before the reload. - Monitor expiry proactively at multiple thresholds. Run a daily cron job on sw-infrarunbook-01 that uses
openssl x509 -checkend
against every PEM file in/etc/haproxy/certs/
and sends alerts at 30, 14, and 7 days before expiry. Also monitor the live certificate served by HAProxy, not just the on-disk file, to catch cases where the file was renewed but HAProxy was not reloaded. - Validate the configuration before every reload. Gate all HAProxy reload operations behind
haproxy -c -f /etc/haproxy/haproxy.cfg
. If the check returns a non-zero exit code, abort the reload and page the on-call operator. This catches key mismatches, missing PEM files, and syntax errors before they affect traffic. - Use a post-deploy SNI validation script. After any certificate or configuration change, run an automated script that issues
openssl s_client
calls for every expected SNI hostname and asserts that the returned certificate CN or SAN matches the domain. Run this script in your CI/CD pipeline as a deployment gate. - Standardize cipher and TLS version configuration. Define ciphers and TLS version options once in the global block so they apply consistently across all frontends. Use the Intermediate profile from the Mozilla SSL Configuration Generator as the baseline and review it annually.
- Use consistent PEM file naming. Name each PEM file after the domain it serves:
/etc/haproxy/certs/api.solvethenetwork.com.pem
. This makes the certificate-to-domain mapping obvious, prevents wrong-file mistakes, and enables HAProxy's automatic SNI-based certificate selection when loading from a directory. - Enable the HAProxy stats page and log SSL errors. Route HAProxy logs to syslog, enable the stats endpoint on a loopback address, and configure alerting on elevated SSL handshake failure rates.
listen stats
bind 127.0.0.1:8404
stats enable
stats uri /haproxy-stats
stats refresh 10s
stats auth infrarunbook-admin:Ch@ng3me!
Operational note: Always test a HAProxy reload against all SNI hostnames immediately after any certificate change. A successful reload does not guarantee that SNI routing is working — it only confirms the process accepted the configuration. The only reliable test is an actual TLS handshake from an external client or monitoring probe.
Frequently Asked Questions
Q: HAProxy starts successfully but clients still see a certificate error — where do I start?
A: Start with
openssl s_client -connect 192.168.10.50:443 -servername solvethenetwork.comand read the full output. Check the certificate CN and SANs against the hostname the client is using, verify the chain depth shows more than just depth=0, and check that the
verify return codeat the end is
0 (ok). The most common culprits are a wrong certificate being served, an incomplete chain, or the client connecting without SNI so it receives the default certificate instead of the one it needs.
Q: How do I check if my HAProxy PEM file is correctly formatted?
A: Run three checks:
grep -c "BEGIN CERTIFICATE" file.pemshould return 2 or more;
grep -c "BEGIN RSA PRIVATE KEY\|BEGIN PRIVATE KEY" file.pemshould return 1; and the MD5 hash of the certificate modulus and key modulus should match. Use
openssl x509 -noout -modulus -in file.pem | openssl md5and
openssl rsa -noout -modulus -in file.pem | openssl md5to compare them.
Q: Can HAProxy serve multiple certificates on the same IP and port?
A: Yes. Use the directory-based
crt /etc/haproxy/certs/syntax in the bind directive. HAProxy will automatically select the certificate that matches the SNI hostname sent by the client. Each PEM file in the directory should be named after the domain it covers. For wildcard certificates, HAProxy will match based on the CN and SAN fields in the certificate itself.
Q: What is the correct order of sections inside a HAProxy PEM file?
A: HAProxy expects: (1) the leaf certificate, (2) any intermediate CA certificates in chain order from leaf to root, and (3) the RSA or EC private key. The private key may also appear before the certificates — HAProxy handles both orderings — but placing it last is a common convention that makes the chain structure easier to visually verify.
Q: My curl works but browsers display a certificate warning — why?
A: By default,
curluses the system CA bundle which may already include the intermediate certificates for your issuer, making the chain appear complete even when HAProxy is not sending it. Browsers manage their own certificate stores and are stricter about requiring the server to send the full chain. Always test with
openssl s_clientand look for
verify error:num=20which reveals chain issues regardless of local CA caching.
Q: How do I enable OCSP stapling in HAProxy to improve handshake performance?
A: Add
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11and enable OCSP on the bind line with
ocsp-update on(HAProxy 2.2+). You also need to pre-fetch the OCSP response and embed it in the PEM file using
openssl ocsp, or configure HAProxy to fetch and cache OCSP responses automatically using the OCSP auto-update feature. Ensure the OCSP responder URL is reachable from sw-infrarunbook-01.
Q: What HAProxy version introduced TLS 1.3 support?
A: TLS 1.3 support in HAProxy depends on both the HAProxy version and the underlying OpenSSL version. HAProxy 1.8+ with OpenSSL 1.1.1+ supports TLS 1.3. The
ssl-default-bind-ciphersuitesdirective (for TLS 1.3 cipher suites) was introduced in HAProxy 1.8. To verify your HAProxy's OpenSSL version:
haproxy -vv | grep OpenSSL. If OpenSSL is below 1.1.1, TLS 1.3 is unavailable regardless of HAProxy version.
Q: How do I see what ciphers HAProxy is currently advertising during the handshake?
A: Use
openssl s_client -connect 192.168.10.50:443 -servername solvethenetwork.comand read the Acceptable client certificate CA names and Cipher is lines. For a full list of what HAProxy would accept, run
openssl ciphers -v '<your-cipher-string>'with the exact cipher string from your haproxy.cfg. Use
nmap --script ssl-enum-ciphers -p 443 192.168.10.50for a more complete enumeration of all advertised ciphers.
Q: How can I test SNI routing before pushing a configuration change to production?
A: Run
haproxy -c -f /etc/haproxy/haproxy.cfgfirst to catch syntax errors. Then use a staging HAProxy instance with the same configuration. After applying the change to production, immediately run a loop of
openssl s_clientcalls for every expected SNI hostname and assert the returned certificate CN is correct. Automate this as a post-deploy smoke test in your pipeline so it runs automatically after every HAProxy reload.
Q: What does SSL handshake failure in HAProxy logs mean and how do I get more detail?
A: The log entry
SSL handshake failurewith backend
<NOSRV>means the TLS handshake failed before HAProxy could route the connection to a backend server. To get more detail, enable verbose SSL error logging with
log-formatin your frontend and increase the log level. You can also run
openssl s_clientfrom a client that is experiencing failures — the TLS alert code in the error message (e.g.,
alert handshake failure,
alert protocol version) tells you exactly which phase of the handshake failed.
Q: Can I reload HAProxy without dropping existing SSL connections?
A: Yes.
systemctl reload haproxysends a SIGUSR2 signal, which triggers a graceful reload. HAProxy spawns a new process that takes over new connections immediately while the old process continues to serve existing connections until they close naturally. This means existing SSL sessions are not interrupted. The new process loads the updated certificate and configuration from disk, so clients that establish new connections after the reload will see the updated certificate immediately.
Q: How do I verify that HAProxy loaded the certificate I expect without restarting the service?
A: Use
openssl s_client -connect 192.168.10.50:443 -servername solvethenetwork.com < /dev/null 2>/dev/null | openssl x509 -noout -subject -datesagainst the live service. This queries the running HAProxy process directly and shows you the currently loaded certificate without requiring a restart or access to the HAProxy process internals. You can also use the HAProxy stats socket to inspect SSL state:
echo "show ssl cert /etc/haproxy/certs/solvethenetwork.pem" | socat stdio /var/run/haproxy/admin.sock.
