# HAProxy TLS-Termination Gateway — Headless VM Setup
A step-by-step guide to building a minimal, GUI-free Linux VM in VMware Workstation that runs HAProxy as a reverse proxy: it receives **external HTTPS** requests, forwards them as **plain HTTP** to internal backends, and returns responses to clients over **HTTPS**.
**Request flow:**
```
Client ──HTTPS/443──> HAProxy (TLS terminates) ──HTTP──> Internal backend
Client <─HTTPS/443── HAProxy (re-encrypts) <─HTTP── Internal backend
```
The backend servers never handle TLS.
---
## 1. Create the VM (VMware Workstation)
| Setting | Value |
|---|---|
| Guest OS | Linux → Ubuntu 64-bit |
| vCPU | 1 (2 if scaling later) |
| RAM | 1 GB (2 GB if scaling later) |
| Disk | 10 GB |
| Network | **Bridged** — VM gets its own LAN IP so it can receive external requests |
| ISO | Ubuntu Server 24.04 LTS (or Debian 12) |
Use the **Server** ISO — it ships headless by default. Bridged networking is important: it gives the VM a real address on your LAN, which a reverse proxy needs.
---
## 2. Install the OS (minimal, no GUI)
During the Ubuntu Server installer:
- Select **"Ubuntu Server (minimized)"** when offered — strips extra tooling.
- Skip all Featured Server Snaps (no Docker, etc. here).
- **Check "Install OpenSSH server"** so you can manage it remotely.
- Set a **static IP** (Network step → edit interface → Manual), or reserve one via DHCP on your router. A reverse proxy needs a stable address.
Example static config in the installer (Manual IPv4):
```
Subnet: 192.168.1.0/24
Address: 192.168.1.10
Gateway: 192.168.1.1
Name servers: 1.1.1.1,8.8.8.8
```
---
## 3. First Boot — Update & Verify Headless
```bash
sudo apt update && sudo apt upgrade -y
sudo apt autoremove --purge -y
# Confirm no GUI — should return: multi-user.target
systemctl get-default
```
If it ever shows `graphical.target`:
```bash
sudo systemctl set-default multi-user.target
```
Manage the VM remotely from your host:
```bash
ssh youruser@192.168.1.10
```
### (Optional) Set static IP post-install via Netplan
Edit `/etc/netplan/*.yaml`:
```yaml
network:
version: 2
ethernets:
ens33: # your interface name (check with: ip link)
dhcp4: no
addresses: [192.168.1.10/24]
routes:
- to: default
via: 192.168.1.1
nameservers:
addresses: [1.1.1.1, 8.8.8.8]
```
```bash
sudo netplan apply
ip -4 addr show # confirm address
ip route | grep default # confirm gateway
```
---
## 4. Install HAProxy
```bash
sudo apt update
sudo apt install -y haproxy
haproxy -v
```
---
## 5. Prepare the TLS Certificate
HAProxy expects the certificate and private key **concatenated into one PEM file**.
### Real certificate (Let's Encrypt)
```bash
sudo mkdir -p /etc/haproxy/certs
sudo bash -c 'cat /etc/letsencrypt/live/yourdomain.com/fullchain.pem \
/etc/letsencrypt/live/yourdomain.com/privkey.pem \
> /etc/haproxy/certs/yourdomain.com.pem'
sudo chmod 600 /etc/haproxy/certs/yourdomain.com.pem
```
### Self-signed certificate (testing)
```bash
sudo mkdir -p /etc/haproxy/certs
sudo openssl req -x509 -newkey rsa:2048 -nodes \
-keyout /tmp/key.pem -out /tmp/cert.pem -days 365 \
-subj "/CN=test.local"
sudo bash -c 'cat /tmp/cert.pem /tmp/key.pem > /etc/haproxy/certs/test.local.pem'
sudo chmod 600 /etc/haproxy/certs/test.local.pem
```
---
## 6. Configure HAProxy
Edit `/etc/haproxy/haproxy.cfg`:
```
global
log /dev/log local0
maxconn 4096
tune.ssl.default-dh-param 2048
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
# External HTTPS listener — terminates TLS here
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/
default_backend internal_http
# Redirect plain HTTP (port 80) -> HTTPS
frontend http_redirect
bind *:80
http-request redirect scheme https code 301
# Internal backend — plain HTTP
backend internal_http
balance roundrobin
# tell the backend the original request was HTTPS
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-For %[src]
server app1 192.168.1.50:8080 check
server app2 192.168.1.51:8080 check
```
Replace the `server` lines with your actual internal backend addresses. Drop the second one if you only have a single backend.
---
## 7. Validate, Enable & Start
```bash
sudo haproxy -c -f /etc/haproxy/haproxy.cfg # syntax check
sudo systemctl enable --now haproxy
sudo systemctl restart haproxy
sudo systemctl status haproxy
```
---
## 8. Open the Firewall
```bash
sudo ufw allow 22/tcp # keep SSH open
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
---
## 9. Test
```bash
# self-signed cert: -k skips certificate validation
curl -k https:///
# check the redirect from HTTP -> HTTPS
curl -I http:///
```
---
## Optional Add-ons
### Stats / monitoring dashboard
Add to `haproxy.cfg` (served over HTTP, viewed from a browser — no GUI needed on the VM):
```
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats admin if TRUE
```
Then open `http://:8404/stats` (allow port 8404 in ufw).
### Multiple domains (SNI)
Drop several `*.pem` files into `/etc/haproxy/certs/`. With `bind *:443 ssl crt /etc/haproxy/certs/`, HAProxy automatically selects the right certificate per domain using SNI.
### Auto-renewing real certs (Let's Encrypt)
```bash
sudo apt install -y certbot
sudo certbot certonly --standalone -d yourdomain.com
# then re-concatenate fullchain + privkey into the certs dir after renewal
# (automate with a certbot deploy-hook)
```
---
## VMware Networking Note
For the VM to receive external HTTPS requests:
- **Bridged networking** — VM gets its own LAN IP (simplest), **or**
- **NAT + host port forwarding** — forward host `443 → VM:443`.
Point your DNS record (or hosts file for testing) at the VM's reachable IP.
---
## Quick Reference — Common Commands
| Task | Command |
|---|---|
| Syntax check config | `sudo haproxy -c -f /etc/haproxy/haproxy.cfg` |
| Restart | `sudo systemctl restart haproxy` |
| Status | `sudo systemctl status haproxy` |
| Live logs | `sudo journalctl -u haproxy -f` |
| Reload without dropping connections | `sudo systemctl reload haproxy` |
HAProxy TLS-Termination Gateway — Headless VM Setup
08 Jul 2026
3 min read
2
