HAProxy is a TCP and HTTP load balancer that terminates TLS, so it decrypts traffic at the edge and forwards plain HTTP to your backend servers. That makes it the only machine in the path that needs the certificate, and it wants that certificate in a specific shape: one file, holding the certificate, any intermediates, and the private key.
This guide covers the whole path: generating a CSR, assembling the PEM file, writing the frontend and backend, checking the configuration, and reloading without dropping live connections. Commands were run against HAProxy 3.4, the current long-term supported branch, released June 2026 and maintained until Q2 2031.
What HAProxy expects: a single PEM file
Most servers take the certificate, the chain and the key as three separate settings. HAProxy takes one. Its configuration manual describes the crt keyword as designating “a PEM file containing both the required certificates and any associated private keys”, built by concatenating PEM files, and adds that “if your CA requires an intermediate certificate, this can also be concatenated into this file”.
Two behaviours are worth knowing before you start, because they save work later:
- The key can live beside the certificate instead of inside it. If the file has no private key in it, HAProxy looks for the same path with .key appended. So mydomain.pem plus mydomain.pem.key works as well as one combined file.
- You can point crt at a directory. HAProxy loads every file it finds there and selects the right certificate per request using SNI. That is how you serve several sites from one frontend without one bind line each.
Generate the CSR and private key
A CSR is the encoded request you hand to the Certificate Authority. Generate it on the HAProxy machine, or anywhere you can keep the private key safe, since the key never leaves your side.
openssl req -new -newkey rsa:2048 -nodes \
-keyout mydomain.key -out mydomain.csr \
-subj "/C=US/ST=California/L=San Jose/O=Your Company/CN=mydomain.com" \
-addext "subjectAltName=DNS:mydomain.com,DNS:www.mydomain.com"
The -addext line is not optional in practice. Browsers stopped matching host names against the Common Name years ago and read only the Subject Alternative Name, so a CSR carrying just a CN produces a certificate that fails in every current browser. List every name the certificate must cover, including the bare domain and the www form if you serve both.
Drop the -subj and -addext options if you prefer to be prompted for each field. Either way you end up with two files: mydomain.csr to submit, and mydomain.key to keep. Confirm the CSR contains what you expect before submitting it, either with our CSR decoder or locally:
openssl req -noout -text -verify -in mydomain.csr
Check that the SAN entries are listed and the signature verifies. If you would rather not use the command line at all, our CSR generator produces the same pair in the browser. More background on the underlying commands is in our guide to OpenSSL commands.
Get the certificate issued
Submit the CSR to the CA, pick the certificate type that matches what you are protecting, and complete validation. A single-domain certificate covers one host name, a wildcard covers every first-level subdomain, and a multi-domain (SAN) certificate covers a list of unrelated names. Behind a load balancer, multi-domain is the common choice, since one HAProxy instance usually fronts several sites.
The CA returns an archive containing your certificate and the intermediate chain, usually as a CA bundle file. Both are already in PEM format, which is what you need. Plan for the renewal now rather than later: since 15 March 2026 a publicly trusted TLS certificate can be valid for no more than 200 days, dropping to 100 days in March 2027 and 47 days in March 2029, so manual replacement stops being practical fairly soon.
Build the PEM file HAProxy will read
Create a directory for certificates and assemble the file there. Keep everything in one place from the start; splitting this across a home directory, /etc/haproxy and /etc/ssl is how people end up editing one file while HAProxy reads another.
sudo mkdir -p /etc/haproxy/certs
sudo chmod 700 /etc/haproxy/certs
Concatenate the certificate, then the intermediates, then the private key. The redirect has to run as root, so pipe into tee rather than writing sudo cat ... > /etc/haproxy/certs/..., which fails with a permission error because the shell opens the output file as your own user before sudo ever runs:
cat mydomain.crt intermediate.crt mydomain.key \
| sudo tee /etc/haproxy/certs/mydomain.pem > /dev/null
That file now contains your private key in plain text, so lock it down before going any further:
sudo chown root:root /etc/haproxy/certs/mydomain.pem
sudo chmod 600 /etc/haproxy/certs/mydomain.pem
Root-only permissions are correct here, not an obstacle. HAProxy is started with superuser privileges, which its manual notes is required for it to switch to its own unprivileged user afterwards, and it reads the certificate during that startup. There is no need to widen the permissions so the haproxy user can read the key, and you should not.
If you built the CSR on a different machine, copy the files across first and delete the copies from the transit directory afterwards:
scp mydomain.crt intermediate.crt mydomain.key sysadmin@haproxy-server:/home/sysadmin/
Configure HAProxy
Open /etc/haproxy/haproxy.cfg in a terminal editor on the server itself, such as nano or vim. Edit it in place rather than on a workstation, so you are never reloading a file that differs from the one you tested.
The frontend
One frontend can accept both plain HTTP and HTTPS. Bind port 80 for the redirect, bind port 443 with the certificate, and send everything else to the backend:
frontend web_frontend
mode http
bind *:80
bind *:443 ssl crt /etc/haproxy/certs/mydomain.pem alpn h2,http/1.1
http-request redirect scheme https code 301 unless { ssl_fc }
default_backend web_servers
alpn h2,http/1.1 offers HTTP/2 and falls back to HTTP/1.1. The redirect fires only when the request did not arrive over TLS, which is what ssl_fc tests, so requests on port 443 pass straight through.
Put every TLS option on that one bind line
This is where HAProxy configurations most often go wrong, and it fails quietly. Guides frequently present TLS hardening as a second step, showing a fresh bind line for port 443 with extra options on it. If you add that line rather than editing the existing one, you end up with two bind lines for the same port, and HAProxy does not complain. It starts, and it opens two separate listening sockets on port 443 with different TLS settings. Which socket a given connection lands on is not something you control, so your hardening covers roughly half your traffic.
The configuration check does not catch this either, and its own documentation explains why: -c “only performs a check of the configuration files and exits before trying to bind“. A duplicate listener is a binding-time condition, so a syntax check will never see it.
Set global defaults instead, so every bind line in the file inherits them and there is nothing to duplicate:
global
ssl-default-bind-options ssl-min-ver TLSv1.2 prefer-client-ciphers
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
Worth knowing before you copy that block anywhere: ssl-min-ver already defaults to TLSv1.2, in HAProxy’s own words. Setting it to TLSv1.2 changes nothing and only documents the intent, which is fine, but it is not the security improvement it is often presented as. The two cipher settings do real work, and they are separate on purpose: ssl-default-bind-ciphers applies to TLS 1.2 and below, ssl-default-bind-ciphersuites applies to TLS 1.3. Set only the first and your TLS 1.3 suites stay at their defaults.
The backend
Give every server a real address. This matters more than it looks:
backend web_servers
mode http
balance roundrobin
option httpchk GET /
server web1 10.0.0.11:80 check
server web2 10.0.0.12:80 check
A server line written as server web1 :80 check, with the address left out, does not raise an error. HAProxy accepts it and treats the empty address as the local machine, so the backend quietly points at HAProxy’s own port 80, which is the frontend you just configured. Traffic loops back into the redirect instead of reaching any application, and health checks look healthy because something really is listening. If a load balancer answers every HTTPS request with a redirect to itself, check the server lines first.
Since HAProxy terminates TLS, the backends receive plain HTTP on port 80 and need no certificate of their own. If policy requires encryption on that leg too, add ssl verify required and a CA file to the server lines and point them at port 443 instead.
Check the configuration before you apply it
Never restart a load balancer on an unverified file. Validate first:
sudo haproxy -c -V -f /etc/haproxy/haproxy.cfg
With -V it prints Configuration file is valid on success and returns exit status zero. Without it, success is silent. Any warnings are reported whether or not the file is valid, so read the output rather than trusting the absence of red text.
Reload rather than restart
sudo systemctl reload haproxy
The distinction is real on a load balancer. A reload starts a new process and signals the old one to “finish what they are doing and to leave”, so requests already in flight complete normally. A restart signals the old process to “terminate immediately without finishing what they were doing”, which cuts live connections, including uploads and long-running API calls. Use restart only when a reload cannot pick up the change, such as after altering the global section’s process settings.
Confirm the service came back and is listening on both ports:
sudo systemctl status haproxy
sudo ss -tlnp | grep haproxy
Exactly one listening socket per port is what you want here. Two on port 443 means you have the duplicate bind line described above.
Verify the certificate is being served
Check what HAProxy actually presents, including the chain, from the server itself:
openssl s_client -connect mydomain.com:443 -servername mydomain.com < /dev/null
Read the Certificate chain section at the top of the output. Your certificate should appear at depth 0 and the intermediate at depth 1. If depth 1 is missing, the intermediate never made it into the PEM file, and the site will work in some browsers while failing in others. Ignore the Verify return code line when judging this: it reports the chain verdict only, and it can read as successful in situations that have nothing to do with what you are testing.
Then confirm from the outside, where the result reflects what real visitors get. Our SSL Checker reports the certificate, the chain and the expiry date.
Renewals and automation
Replacing the PEM file is the whole renewal procedure: rebuild it from the new certificate and the same or a new key, then reload. Nothing in the HAProxy configuration references the expiry date, so no config edit is needed as long as the file path stays the same.
With certificate lifetimes shortening, automation is worth setting up now. HAProxy gained a built-in ACME client in version 3.2, configured through an acme section. Treat it as a preview rather than production infrastructure for the moment: it is still marked experimental in 3.4 and requires expose-experimental-directives in the global section, it supports the http-01 and dns-01 challenge types only, and certificates it generates have to be dumped from the stats socket to reach the disk. The established alternative is to run an external ACME client and have its deploy step rebuild the PEM file and reload HAProxy, which is the same pattern described in our ACME guide for Apache and NGINX.
Frequently Asked Questions
Your certificate first, then any intermediates, then the private key. HAProxy’s manual describes the file as one built by concatenating PEM files and says the intermediate can be concatenated into it. If you prefer to keep the key separate, leave it out entirely and save it as the same path with .key appended, which HAProxy loads automatically.
Usually a backend server line with no address, such as server web1 :80 check. HAProxy treats the empty address as the local machine, so the backend points at HAProxy’s own port 80, which is the frontend that issues the HTTP to HTTPS redirect. Give every server line a real IP address or host name.
You can, and that is the problem. HAProxy neither merges them nor warns, and haproxy -c passes because it exits before trying to bind. You get two listening sockets on one port with whatever different TLS options each line carries, so a setting present on only one of them applies to only part of your traffic. Keep one bind line per port and put shared TLS settings in ssl-default-bind-options.
Not by itself. HAProxy documents the default value of ssl-min-ver as TLSv1.2 already, so setting it to the same value records your intent without changing behaviour. Real hardening comes from the cipher settings, and remember that TLS 1.3 needs ssl-default-bind-ciphersuites while TLS 1.2 and below use ssl-default-bind-ciphers.
Not in the standard setup. HAProxy terminates TLS at the edge and forwards plain HTTP, which is why only the load balancer holds the certificate. Add ssl verify required with a CA file on the server lines only if your policy requires the internal leg encrypted as well.
Point crt at a directory instead of a file. HAProxy loads every certificate in it and picks the right one per request using SNI, so a single bind line covers all of them. A multi-domain (SAN) certificate is the other route, and it suits a set of names that renew together.
Branch 3.4, released June 2026, is the current long-term supported release and is maintained until Q2 2031. HAProxy’s even-numbered branches are the LTS ones, with roughly five years of maintenance, while odd-numbered branches such as 3.3 get 12 to 18 months. Everything in this guide applies to 3.0 and later; only the built-in ACME client requires 3.2 or newer.
If the certificate is installed but browsers still complain, our guides to common SSL errors cover the usual causes, most often an incomplete chain or a name the certificate does not list.
Save 10% on SSL Certificates when ordering from SSL Dragon today!
Fast issuance, strong encryption, 99.99% browser trust, dedicated support, and 25-day money-back guarantee. Coupon code: SAVE10


