# 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` |