InfraRunBook
    Back to articles

    HAProxy SSL Termination Issues

    HAProxy
    Published: Apr 6, 2026
    Updated: Apr 6, 2026

    Diagnose and fix the most common HAProxy SSL termination failures including wrong certificates, incomplete chains, cipher mismatches, TLS version restrictions, and broken SNI routing.

    HAProxy SSL Termination Issues

    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.pem
    file 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_sni
    is 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_backend
    and 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.com
    and 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 code
    at 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.pem
    should return 2 or more;
    grep -c "BEGIN RSA PRIVATE KEY\|BEGIN PRIVATE KEY" file.pem
    should return 1; and the MD5 hash of the certificate modulus and key modulus should match. Use
    openssl x509 -noout -modulus -in file.pem | openssl md5
    and
    openssl rsa -noout -modulus -in file.pem | openssl md5
    to 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,

    curl
    uses 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_client
    and look for
    verify error:num=20
    which 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-tlsv11
    and 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-ciphersuites
    directive (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.com
    and 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.50
    for 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.cfg
    first 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_client
    calls 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 failure
    with 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-format
    in your frontend and increase the log level. You can also run
    openssl s_client
    from 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 haproxy
    sends 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 -dates
    against 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
    .

    Frequently Asked Questions

    HAProxy starts successfully but clients still see a certificate error — where do I start?

    Start with openssl s_client -connect 192.168.10.50:443 -servername solvethenetwork.com and 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 code at 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.

    How do I check if my HAProxy PEM file is correctly formatted?

    Run three checks: grep -c 'BEGIN CERTIFICATE' file.pem should return 2 or more; grep -c 'BEGIN PRIVATE KEY' file.pem should return 1; and the MD5 hash of the certificate modulus and key modulus should match. Use openssl x509 -noout -modulus -in file.pem | openssl md5 and openssl rsa -noout -modulus -in file.pem | openssl md5 to compare them.

    Can HAProxy serve multiple certificates on the same IP and port?

    Yes. Use the directory-based crt /etc/haproxy/certs/ syntax in the bind directive. HAProxy automatically selects 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.

    What is the correct order of sections inside a HAProxy PEM file?

    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 the conventional approach.

    My curl works but browsers display a certificate warning — why?

    By default curl uses 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 require the server to send the full chain. Always test with openssl s_client and look for verify error:num=20 which reveals chain issues regardless of local CA caching.

    How do I enable OCSP stapling in HAProxy?

    Add ocsp-update on to the bind directive (HAProxy 2.2+). You also need to pre-fetch the OCSP response or configure HAProxy to fetch and cache OCSP responses automatically. Ensure the OCSP responder URL in the certificate's AIA extension is reachable from the HAProxy host.

    What HAProxy version introduced TLS 1.3 support?

    TLS 1.3 support 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-ciphersuites directive was introduced in HAProxy 1.8. Verify your build with: haproxy -vv | grep OpenSSL.

    How do I see what ciphers HAProxy is currently advertising?

    Use openssl s_client -connect 192.168.10.50:443 -servername solvethenetwork.com and read the 'Cipher is' line. For a full list of what HAProxy would accept, run openssl ciphers -v '<your-cipher-string>' with the exact cipher string from haproxy.cfg. nmap --script ssl-enum-ciphers -p 443 192.168.10.50 gives a complete enumeration.

    How can I test SNI routing before pushing a configuration change to production?

    Run haproxy -c -f /etc/haproxy/haproxy.cfg first to catch syntax errors. After applying the change, immediately run a loop of openssl s_client calls 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.

    What does SSL handshake failure in HAProxy logs mean and how do I get more detail?

    The log entry SSL handshake failure with backend <NOSRV> means the TLS handshake failed before HAProxy could route the connection. Run openssl s_client from a failing client — the TLS alert code in the error message (e.g., alert handshake failure, alert protocol version) tells you exactly which phase failed and narrows down the root cause.

    Related Articles