Illustrative teaching lab with a seated operator, rack servers and a HAProxy HTTPS DEMO board.

WIUNIX ACADEMY

HAProxy with Let’s Encrypt and validated certificate renewal

Balance two HTTP backends behind HAProxy TLS, preserve HTTP-01, sanitize proxy headers and deploy renewed certificates with validation and rollback.

Intermediate65 min readUpdated
Make this guide yours

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Ubuntu 26.04 LTSReviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment7 editable values

THE OUTCOME

What will you have at the end?

Serve app.example.com through two healthy backends with validated recoverable TLS deployment.

01HTTP-01Internet TCP80 → 192.0.2.10:80 → 127.0.0.1:8888 → webroot
02HTTPSapp.example.com TCP443 → HAProxy → 10.77.0.21 / 10.77.0.22 : 8080
Supported operating systems and versions

OS paths are documented; isolated execution scope and exclusions appear at the end.

Before you begin

  • A fresh sudo-capable edge VM, independent console, owned hostname and DNS/firewall administration.
  • Two ready private backends with /health/ready; unused /srv/academy-haproxy-tls and listener ports.
  • Open settings, edit, Apply, then copy. Values are public and retained only in this tab/language/environment; never enter a key, password or token.

A TLS edge with a recoverable certificate path

Build one HAProxy edge for app.example.com, forwarding to 10.77.0.21 and 10.77.0.22 on 8080. Keep HTTP-01 available while all other HTTP requests redirect to HTTPS. A certificate renewal must pass key, chain, name and proxy validation before a graceful reload.

This is an intermediate fresh-VM exercise, about 65 minutes plus DNS/ACME time. One edge is a single point of failure. No session persistence, application data replication or high availability is implied. Private HTTP backends require an isolated trusted network; use authenticated backend TLS across untrusted networks.

Separate the public edge from the challenge responder

Clients reach local address 192.0.2.10 on TCP80/443, directly or through deliberate NAT. Public DNS must point to the real public ingress, not blindly to that local address. The example domain and documentation IPv4 cannot issue a certificate: replace them first. The public challenge port is always80; 8888 is loopback-only behind HAProxy.

Only the challenge prefix reaches the static webroot. TLS traffic is round-robin balanced with a readiness check. HAProxy replaces incoming proxy headers; applications trust these headers only from this edge and reject direct internet access. A CDN or upstream proxy changes the trust boundary and is outside this direct-edge recipe.

01

LET’S BUILD

Check ownership, addresses and existing listeners

Run on a new disposable edge VM, not a production HAProxy host. The selected local IPv4 must already belong to this VM; TCP80/443 and the loopback challenge port must be free. Stop if HAProxy, Certbot state, one of the named units or /srv/academy-haproxy-tls already exists. Preserve the SSH console and firewall owner. Apply only reviewed TCP80/443 ingress and edge-to-backend rules through that owner; this guide does not overwrite a firewall.

bash

Bash on dedicated edge VM; external checks noted in text

set -eu
case '/srv/academy-haproxy-tls' in /*) ;; *) printf '%s\n' 'Use a new absolute path'; exit 1;; esac
cat /etc/os-release
ps -p 1 -o comm=
test "$(ps -p 1 -o comm=)" = systemd
ip -br address
sudo ss -lntup
test ! -e '/srv/academy-haproxy-tls'
test ! -L '/srv/academy-haproxy-tls'
test ! -e /etc/haproxy
test ! -e /etc/letsencrypt
for name in academy-haproxy academy-acme-webroot academy-certbot; do
  test ! -e "/etc/systemd/system/$name.service"
done
test ! -e /etc/systemd/system/academy-certbot.timer
How do I know it worked?

A fresh edge has no conflicting service/state; its local address and the reviewed network path are known.

02

LET’S BUILD

Use signed distribution packages

Certbot is maintained and its webroot/deploy-hook workflow is documented. Upstream recommends Snap for many systems; this lab deliberately uses the distribution-maintained apt package and its support/security channel, not a downloaded one-line installer. Record actual candidates and versions. HAProxy 2.6/2.8/3.x use the directives shown here; do not require the latest upstream binary. The OS guard prevents copying commands into the wrong release. Disable only the empty services just installed on this fresh VM; dedicated units below own the exercise.

bash

Bash on dedicated edge VM; external checks noted in text

set -eu
. /etc/os-release
test "$VERSION_CODENAME" = 'resolute'
sudo apt-get update
apt-cache policy haproxy certbot python3
sudo apt-get install --no-install-recommends haproxy certbot python3 openssl ca-certificates curl dnsutils nano
sudo systemctl disable --now haproxy.service certbot.timer
haproxy -vv
certbot --version
python3 --version
How do I know it worked?

The codename matches; signed package versions are recorded and generic empty services are stopped.

03

LET’S BUILD

Create distinct public and protected directories

The root must be a dedicated absolute path with no spaces or symlinks, not an existing application directory. Only webroot is world-readable/traversable; ACME keys, backups and PEMs are root-only. The non-chrooted HAProxy master reads PEMs as root before workers drop to haproxy. Never grant www-data access to certificate keys.

bash

Bash on dedicated edge VM; external checks noted in text

set -eu
case '/srv/academy-haproxy-tls' in /*) ;; *) printf '%s\n' 'Use a new absolute path'; exit 1;; esac
test ! -e '/srv/academy-haproxy-tls'
test ! -L '/srv/academy-haproxy-tls'
sudo install -d -m 0755 '/srv/academy-haproxy-tls'
sudo install -d -m 0755 '/srv/academy-haproxy-tls/webroot/.well-known/acme-challenge'
sudo install -d -m 0700 '/srv/academy-haproxy-tls/certs' '/srv/academy-haproxy-tls/acme'
for dir in config work logs staging-config staging-work staging-logs; do
  sudo install -d -m 0700 "/srv/academy-haproxy-tls/acme/$dir"
done
How do I know it worked?

Only the static challenge directory is readable by www-data; certificate directories are0700.

04

LET’S BUILD

Open the HTTP bootstrap and service files

Create only these new files. Paste each following panel into its matching filename; do not execute file contents as shell. The bootstrap intentionally serves no application on HTTPS until issuance succeeds. Keep the temporary redirect phase within a planned setup window.

bash

Bash on dedicated edge VM; external checks noted in text

set -eu
test ! -e '/srv/academy-haproxy-tls/haproxy.cfg'
test ! -L '/srv/academy-haproxy-tls/haproxy.cfg'
sudo install -m 0644 /dev/null '/srv/academy-haproxy-tls/haproxy.cfg'
test ! -e /etc/systemd/system/academy-haproxy.service
test ! -L /etc/systemd/system/academy-haproxy.service
sudo install -m 0644 /dev/null /etc/systemd/system/academy-haproxy.service
test ! -e /etc/systemd/system/academy-acme-webroot.service
test ! -L /etc/systemd/system/academy-acme-webroot.service
sudo install -m 0644 /dev/null /etc/systemd/system/academy-acme-webroot.service
sudo nano '/srv/academy-haproxy-tls/haproxy.cfg' /etc/systemd/system/academy-haproxy.service /etc/systemd/system/academy-acme-webroot.service
How do I know it worked?

Three empty new files are open; the next three panels supply their complete contents.

05

LET’S BUILD

HTTP-only bootstrap configuration

File contents. Save exactly at the displayed filename.

text · /srv/academy-haproxy-tls/haproxy.cfg

File on the dedicated edge VM

global
  log stdout format raw local0
  user haproxy
  group haproxy
  ssl-default-bind-options ssl-min-ver TLSv1.2

defaults
  log global
  mode http
  option httplog
  timeout connect 5s
  timeout client 30s
  timeout server 30s
  timeout http-request 10s

frontend public_http
  bind 192.0.2.10:80
  acl known_host hdr(host) -i app.example.com app.example.com:80
  http-request deny deny_status 421 unless known_host
  acl acme_path path_beg /.well-known/acme-challenge/
  http-request redirect prefix https://app.example.com code 308 unless acme_path
  use_backend acme_webroot if acme_path

backend acme_webroot
  server challenge 127.0.0.1:8888 check
How do I know it worked?

The saved file matches this panel; no key material is included.

06

LET’S BUILD

HAProxy master-worker service

File contents. Save exactly at the displayed filename.

ini · /etc/systemd/system/academy-haproxy.service

File on the dedicated edge VM

[Unit]
Description=Academy isolated HAProxy listener
After=network-online.target academy-acme-webroot.service
Wants=network-online.target

[Service]
Type=notify
ExecStart=/usr/sbin/haproxy -Ws -f /srv/academy-haproxy-tls/haproxy.cfg -p /srv/academy-haproxy-tls/haproxy.pid
ExecReload=/usr/sbin/haproxy -c -f /srv/academy-haproxy-tls/haproxy.cfg
ExecReload=/bin/kill -USR2 $MAINPID
KillMode=mixed
Restart=on-failure

[Install]
WantedBy=multi-user.target
How do I know it worked?

The saved file matches this panel; no key material is included.

07

LET’S BUILD

Loopback static responder

File contents. Save exactly at the displayed filename.

ini · /etc/systemd/system/academy-acme-webroot.service

File on the dedicated edge VM

[Unit]
Description=Academy HTTP-01 static webroot
After=network.target

[Service]
User=www-data
Group=www-data
ExecStart=/usr/bin/python3 -m http.server 8888 --bind 127.0.0.1 --directory /srv/academy-haproxy-tls/webroot
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true

[Install]
WantedBy=multi-user.target
How do I know it worked?

The saved file matches this panel; no key material is included.

Explore all tutorials ↗