# Install a TLS certificate on HAProxy

HAProxy expects a single PEM file containing the server certificate, its intermediates and the private key, concatenated in that order, referenced with `bind :443 ssl crt /path/site.pem`. It is the only common TLS server that wants the private key in the same file as the certificate, which is the detail people carry over incorrectly from nginx. Point `crt` at a directory instead of a file and HAProxy loads every certificate in it and selects by SNI.

Source: https://certguru.io/platforms/haproxy/
Last updated: 2026-08-19
Commands verified against OpenSSL 3.5.7.

---

## Build the combined file

*Certificate, chain, then key*

```bash
cat certificate.crt chain.crt private.key > /etc/haproxy/certs/site.pem
```

```bash
chmod 600 /etc/haproxy/certs/site.pem
```

> **This file contains a private key**
>
> Unlike an nginx certificate file, this one is a secret. `chmod 600`, owned by root, never in a
>   configuration repository, and excluded from any backup that goes somewhere less protected.

## Configure

*/etc/haproxy/haproxy.cfg*

```haproxy
global
    ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
    ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384

frontend https
    bind :80
    bind :443 ssl crt /etc/haproxy/certs/site.pem alpn h2,http/1.1

    http-request redirect scheme https unless { ssl_fc }
    http-response set-header Strict-Transport-Security "max-age=63072000"

    default_backend web

backend web
    server app1 10.0.0.10:8080 check
```

| Placeholder | What to put there |
| --- | --- |
| `crt /etc/haproxy/certs/site.pem` | One file holding certificate, chain and key. |
| `alpn h2,http/1.1` | Advertises HTTP/2. Without it clients fall back to HTTP/1.1. |
| `ssl_fc` | True when the connection arrived over TLS — used here for the redirect. |

## Several certificates

```haproxy
bind :443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 strict-sni
```

Every file in the directory is loaded and selected by SNI. `strict-sni` rejects a connection whose
SNI matches nothing, rather than quietly serving whichever certificate sorted first — which is
otherwise a confusing way to serve the wrong site.

## Validate before reloading

```bash
haproxy -c -f /etc/haproxy/haproxy.cfg
```

Expected output:

```
Configuration file is valid
```

```bash
systemctl reload haproxy
```

## Verify from outside

*Is the chain complete?*

```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts /dev/null \
  | grep -c "BEGIN CERTIFICATE"
```

Expected output:

```
2
```

A count of 1 means `chain.crt` was left out of the concatenation.

## Starting from a PFX

```bash
openssl pkcs12 -in certificate.pfx -nocerts -nodes -out /tmp/private.key
openssl pkcs12 -in certificate.pfx -clcerts -nokeys -out /tmp/certificate.crt
openssl pkcs12 -in certificate.pfx -cacerts -nokeys -out /tmp/chain.crt
cat /tmp/certificate.crt /tmp/chain.crt /tmp/private.key > /etc/haproxy/certs/site.pem
chmod 600 /etc/haproxy/certs/site.pem
```

Delete the temporary files afterwards — two of them contain the private key.

## Mutual TLS

```haproxy
bind :443 ssl crt /etc/haproxy/certs/site.pem \
     ca-file /etc/haproxy/certs/client-ca.crt verify required

http-request set-header X-Client-DN %{+Q}[ssl_c_s_dn]
```

Use `verify optional` while rolling out, so clients without a certificate still connect. See
[mutual TLS](/concepts/mtls/).

## Reloading a renewed certificate without dropping connections

```bash
echo -e "set ssl cert /etc/haproxy/certs/site.pem <<\n$(cat /etc/haproxy/certs/site.pem)\n" \
  | socat stdio /var/run/haproxy/admin.sock
echo "commit ssl cert /etc/haproxy/certs/site.pem" | socat stdio /var/run/haproxy/admin.sock
```

## Common HAProxy failures

| Symptom | Cause |
| --- | --- |
| `unable to load SSL private key` | The key was not concatenated into the file |
| Clients report an untrusted issuer | `chain.crt` missing from the concatenation |
| Wrong certificate served | No SNI match; add `strict-sni` to make it explicit |
| `bind` fails at startup | Wrong path, or the file is not readable by HAProxy |


## Frequently asked questions

### Why does HAProxy want the key in the same file?

It is simply the interface it was designed with — one file per TLS identity, loaded whole. It makes a certificate directory easy to manage, at the cost of a file that must be protected like a private key, because it contains one.

### Does the order inside the file matter?

The certificate must come before its intermediates. The key is conventionally last but HAProxy accepts it anywhere in the file. Keep to certificate, chain, key so the file is predictable to anyone reading it.

### How do I serve several certificates?

Point `crt` at a directory. HAProxy loads every file in it and selects by SNI. Add `strict-sni` to reject connections whose SNI matches nothing rather than falling back to the first certificate.

### Can HAProxy reload certificates without a restart?

Yes, through the runtime API — `set ssl cert` followed by `commit ssl cert` on the admin socket. That makes renewal possible with no dropped connections.


## References

- [HAProxy configuration manual](https://docs.haproxy.org/3.0/configuration.html) — the bind ssl crt syntax and what the file must contain

## Related pages

- https://certguru.io/platforms/nginx/
- https://certguru.io/concepts/certificate-chain/
- https://certguru.io/errors/unable-to-get-local-issuer-certificate/
- https://certguru.io/convert/pfx-to-pem/
- https://certguru.io/concepts/mtls/

--
CertGuru — https://certguru.io
