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.
Make this guide yoursYOUR SYSTEM. YOUR STEPS.
Where are you working?
Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.
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.
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.
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 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
A fresh edge has no conflicting service/state; its local address and the reviewed network path are known.
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 on dedicated edge VM; external checks noted in text
set -eu
. /etc/os-release
test "$VERSION_CODENAME" = 'trixie'
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
The codename matches; signed package versions are recorded and generic empty services are stopped.
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 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
Only the static challenge directory is readable by www-data; certificate directories are0700.
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 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
Three empty new files are open; the next three panels supply their complete contents.
LET’S BUILD
HTTP-only bootstrap configuration
File contents. Save exactly at the displayed filename.
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
The saved file matches this panel; no key material is included.
LET’S BUILD
HAProxy master-worker service
File contents. Save exactly at the displayed filename.
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
The saved file matches this panel; no key material is included.
LET’S BUILD
Loopback static responder
File contents. Save exactly at the displayed filename.
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
The saved file matches this panel; no key material is included.