Homelab · Part 15

Homelab Build — Part 15 — Exposing Services: Setting Up a Reverse Proxy (Nginx Proxy Manager & HAProxy)

Design and deploy an enterprise-grade DMZ reverse proxy using Nginx Proxy Manager and HAProxy to publish homelab services with automated Let’s Encrypt DNS-01 SSL certificates and strict pfSense firewall isolation.

Quick idea: Never expose backend application ports directly to the internet; terminate all external and internal web traffic at an isolated reverse proxy inside your DMZ, where automated SSL certificates, access lists, and strict firewall pinholes protect internal servers from direct network exposure.
DMZ Isolation

Host the reverse proxy in a segregated DMZ VLAN (VLAN 30) so an application exploit cannot pivot freely into your management or Active Directory subnets.

DNS-01 ACME

Generate wildcard SSL certificates via Cloudflare API DNS challenges without opening inbound HTTP port 80 to the public internet.

Access Control

Enforce granular IP access lists on the proxy to ensure administrative interfaces remain reachable only from trusted internal and VPN subnets.

Why a DMZ Reverse Proxy Matters

In Part 14 of our Homelab Build series, we established secure, encrypted remote access into our private infrastructure using WireGuard and OpenVPN on pfSense. For administrative tasks—such as connecting via SSH, RDP, or domain-joined PowerShell sessions—a VPN is the undisputed gold standard.

However, an enterprise homelab often hosts services that need to be accessed without requiring a persistent VPN tunnel: a family media portal, public-facing webhooks, external uptime monitors, or internal services that simply require valid, browser-trusted TLS certificates without manual root CA installation on personal mobile devices.

Think of a reverse proxy like a reception desk in the lobby of a high-security corporate facility: visitors never roam freely into employee cubicles, executive boardrooms, or the data centre vault. Instead, all external visitors enter through a single monitored turnstile, state their destination at the desk, have their identification checked, and are escorted or proxied through to their destination. If a visitor turns hostile, they are contained within the reinforced lobby—never inside the primary office floors.

In networking terms, exposing an internal application—such as a Grafana dashboard on port 3000 or a Proxmox node on port 8006—directly to the internet via port forwarding is an architectural disaster. If the application contains an unauthenticated Remote Code Execution (RCE) vulnerability, the attacker lands directly on your core server subnet.

A reverse proxy situated in a Demilitarized Zone (DMZ) provides four essential operational layers:

  • Single Attack Surface: Only standard web ports (TCP 80 and 443) are forwarded from your public IP, directed exclusively to the proxy host.
  • Centralised Certificate Management: A single daemon manages Let’s Encrypt renewal for all domain names, presenting valid HTTPS to clients while connecting to backends over HTTP or HTTPS.
  • URL Routing and Port Masking: Instead of memorising non-standard ports like https://10.10.20.30:8443, clients access clean hostnames such as https://monitor.lab.karanth.ovh.
  • Blast Radius Containment: By isolating the reverse proxy in VLAN 30, even if the web server daemon is fully compromised, strict pfSense firewall rules prevent the attacker from initiating connections into your Active Directory domain controllers or management hypervisors.

Nginx Proxy Manager vs. HAProxy

When implementing a reverse proxy in an enterprise-style homelab, two solutions dominate the ecosystem: Nginx Proxy Manager (NPM) and HAProxy (typically deployed via the native pfSense package). Choosing between them requires evaluating your operational priorities:

Architectural Dimension Nginx Proxy Manager (NPM) HAProxy (on pfSense)
Deployment Location Dedicated Proxmox LXC / Docker in DMZ (VLAN 30) Directly on pfSense firewall kernel/OS
Security Blast Radius Isolated: Compromise of NPM leaves the firewall host untouched. High: An exploit in the reverse proxy runs directly on the edge firewall.
Certificate Management Built-in graphical ACME client with native DNS-01 API support. Requires configuring the separate pfSense ACME package and syncing certs.
User Interface Modern, dedicated responsive web UI designed for rapid proxy host addition. Complex pfSense package UI with abstract Frontends, Backends, and ACLs.
Advanced Traffic Shaping Basic rate limiting, caching, and custom Nginx directives. Superior Layer 4 / Layer 7 balancing, health checking, and DDoS mitigation.

While HAProxy on pfSense eliminates the need for an extra container, running an internet-facing reverse proxy directly on your primary firewall violates the fundamental principle of defense-in-depth. If a zero-day exploit targets the web proxy, an attacker achieves root execution on the gateway router connecting every subnet in your home.

For our homelab architecture, we adopt the industry-standard design: Nginx Proxy Manager running inside an unprivileged Proxmox LXC container anchored in VLAN 30 (DMZ). This provides an intuitive administrative experience while enforcing an impenetrable network boundary at the firewall.

Subnet & DMZ Network Layout

In Part 3 and Part 12, we carved our physical network into functional VLANs. The reverse proxy integrates directly into this topology:

Subnet / VLAN Subnet Range Role in Reverse Proxy Architecture
VLAN 10 (Management) 10.10.10.0/24 Hosts Proxmox VE (10.10.10.2) and DC01 (10.10.10.10). Inbound access from DMZ is strictly blocked.
VLAN 20 (Servers) 10.10.20.0/24 Hosts internal web applications, Grafana, Portainer, and DC02. DMZ can only reach specific application ports.
VLAN 30 (DMZ) 10.10.30.0/24 Hosts the Nginx Proxy Manager LXC container (10.10.30.10). Gateway: 10.10.30.1.
VLAN 50 (WireGuard) 10.10.50.0/24 Remote administration tunnel. Permitted full access to NPM administrative UI on port 81.
Traffic flow model: Inbound HTTPS traffic strikes the pfSense WAN IP on TCP port 443. pfSense NAT forwards this traffic across to 10.10.30.10:443. Nginx Proxy Manager decrypts TLS, inspects the Host header, and opens an internal TCP connection to the upstream target (e.g., 10.10.20.30:3000). Return traffic flows back through the proxy to the external client.

Deploying Nginx Proxy Manager in a Proxmox LXC

Running Nginx Proxy Manager inside a lightweight Debian 12 unprivileged LXC container on Proxmox VE provides near-bare-metal performance with zero hypervisor virtualization overhead.

Step 1: Create the Container on Proxmox

On your primary Proxmox node (pve01), create a new unprivileged LXC container assigned to VLAN 30:

  1. Template: debian-12-standard.
  2. CT ID: 105 | Hostname: dmz-proxy01.
  3. Resources: 1 vCPU, 1024 MB RAM, 8 GB SSD disk storage.
  4. Network: Bridge vmbr0, VLAN Tag 30, IPv4 Static: 10.10.30.10/24, Gateway: 10.10.30.1.
  5. DNS: Point to DC01 (10.10.10.10) or pfSense (10.10.30.1).

Because Docker runs inside an unprivileged container, you must enable container nesting and keyctl features. Execute this on the Proxmox host shell:

# Enable nesting and keyctl on the unprivileged LXC container before booting
pct set 105 -features nesting=1,keyctl=1

# Start the container
pct start 105

# Enter the container console
pct enter 105

Step 2: Install Docker and Docker Compose

Inside the dmz-proxy01 container console, install Docker Engine using the official upstream repository:

# Update package indexes and install prerequisite packages
apt update && apt install -y curl ca-certificates gnupg lsb-release

# Add Docker official GPG key
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg

# Set up the Docker stable repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine and the Docker Compose plugin
apt update && apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

# Verify Docker daemon is running
systemctl status docker --no-pager

Step 3: Deploy Nginx Proxy Manager Compose Stack

Create a dedicated directory to house the persistent configuration databases and SSL certificates:

# Create directory structure for NPM data and Let's Encrypt certificates
mkdir -p /opt/npm && cd /opt/npm

Create the docker-compose.yml file:

# /opt/npm/docker-compose.yml
services:
  app:
    image: 'jc21/nginx-proxy-manager:latest'
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      # Public web traffic
      - '80:80'
      - '443:443'
      # Administrative Web UI
      - '81:81'
    environment:
      # Optional: disable IPv6 if not configured on your host
      - DISABLE_IPV6=true
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt

Launch the stack as a background daemon:

# Start the Nginx Proxy Manager stack
docker compose up -d

# Verify container health and port bindings
docker ps
Default credentials warning: Once the container initializes, browse to http://10.10.30.10:81. Log in with the default credentials: Email: admin@example.com, Password: changeme. You are immediately prompted to update the email address and configure a strong administrative password. Do not skip this step!

Automated SSL via Cloudflare DNS-01

Traditional Let’s Encrypt certificate issuance uses the HTTP-01 challenge: Let’s Encrypt connects to your server over TCP port 80 and attempts to download a cryptographic challenge file from http://yourdomain.com/.well-known/acme-challenge/....

While functional, HTTP-01 has significant drawbacks: it requires port 80 to remain open to the entire public internet, fails if your ISP blocks port 80, and cannot issue wildcard certificates (e.g., *.lab.karanth.ovh).

The DNS-01 challenge solves all three limitations. Instead of opening a web port, Let’s Encrypt issues a challenge that the ACME client writes directly to your public DNS zone as a _acme-challenge TXT record via your DNS provider’s API. Let’s Encrypt verifies the TXT record and signs the certificate. Zero inbound ports are required.

Step 1: Create a Scoped Cloudflare API Token

Never use your Global API Key. Create a scoped API token with minimal privileges:

  1. Log into the Cloudflare Dashboard and navigate to My Profile > API Tokens.
  2. Click Create Token > select Create Custom Token.
  3. Token Name: NPM-Homelab-DNS01.
  4. Permissions: Zone > DNS > Edit.
  5. Zone Resources: Include > Specific Zone > karanth.ovh (or your domain).
  6. Click Continue to summary > Create Token. Copy the generated token string.

Step 2: Request the Wildcard Certificate in NPM

In the Nginx Proxy Manager web interface:

  1. Navigate to SSL Certificates > click Add SSL Certificate > Let’s Encrypt.
  2. Domain Names: Enter *.lab.karanth.ovh and lab.karanth.ovh.
  3. Email Address: Enter your operational email for expiry notifications.
  4. Toggle Use a DNS Challenge to ON.
  5. DNS Provider: Select Cloudflare.
  6. In the Credentials File Content box, enter your API token:
    # Cloudflare API token for DNS-01 validation
    dns_cloudflare_api_token = 8f9b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b
  7. Propagation Seconds: Set to 120 (allows Cloudflare edge nodes to synchronise before ACME validation).
  8. Check I Agree to the Let’s Encrypt Terms of Service and click Save.

NPM connects to Cloudflare, creates the TXT challenge, verifies ownership, and stores the signed wildcard certificate in /opt/npm/letsencrypt/live/. Renewal runs automatically via cron every 60 days.

Configuring Proxy Hosts & Access Lists

With our wildcard certificate ready, we can publish internal services cleanly. Let us examine two distinct publishing patterns: an internal administrative portal requiring strict access control, and a public web service.

Pattern 1: Publishing Proxmox Web UI (with WebSocket Support)

Proxmox VE runs on port 8006 and relies heavily on WebSockets for its interactive noVNC console. To proxy Proxmox via NPM:

  1. Navigate to Hosts > Proxy Hosts > Add Proxy Host.
  2. Details Tab:
    • Domain Names: pve.lab.karanth.ovh
    • Scheme: https (Proxmox uses a self-signed cert on 8006)
    • Forward Hostname / IP: 10.10.10.2
    • Forward Port: 8006
    • Check Cache Assets: Off
    • Check Block Common Exploits: On
    • Check Websockets Support: On (Crucial for noVNC console functionality!)
  3. SSL Tab:
    • SSL Certificate: Select our wildcard *.lab.karanth.ovh
    • Check Force SSL: On
    • Check HTTP/2 Support: On
    • Check HSTS Enabled: On

Pattern 2: Restricting Access Lists for Sensitive Portals

Even if your proxy host is reachable externally, administrative portals (like Proxmox, pfSense, or Portainer) must never be open to the general public. NPM provides built-in Access Lists to enforce IP allow-lists:

  1. Navigate to Access Lists > Add Access List.
  2. Name: Internal and VPN Only.
  3. Under the Access tab, add your trusted subnets:
    • 10.10.0.0/16 (All homelab local subnets)
    • 10.10.50.0/24 (WireGuard VPN clients)
  4. Set Default to Deny.
  5. Save the list.
  6. Edit your sensitive Proxy Host (e.g., pve.lab.karanth.ovh) and attach the Internal and VPN Only access list.

Any request arriving from an external public IP address is immediately rejected with an HTTP 403 Forbidden before the request ever touches the internal Proxmox hypervisor.

pfSense Port Forwards & Firewall Rules

The final operational step is configuring our perimeter router to deliver external traffic to the proxy while locking down the DMZ perimeter.

1. Configure WAN Port Forwarding

In the pfSense web interface, navigate to Firewall > NAT > Port Forward and create a rule for HTTPS traffic:

Field Value Operational Purpose
Interface WAN Listens on the public internet connection.
Address Family IPv4 Standard public IPv4 transport.
Protocol TCP HTTPS web traffic.
Destination WAN address Matches traffic addressed to your public IP.
Destination Port Range HTTPS (443) Standard encrypted web port.
Redirect Target IP 10.10.30.10 Internal IP address of our NPM container in the DMZ.
Redirect Target Port HTTPS (443) Translates directly to NPM’s HTTPS listener.
Filter Rule Association Pass (or Rule) Automatically creates the matching WAN firewall pass rule.
HTTP Port 80 handling: If you use DNS-01 ACME validation, you do not need to forward port 80 to the DMZ at all. However, if you wish to support automatic HTTP-to-HTTPS redirection for external clients typing bare domain names into browsers, you can create an identical NAT rule for TCP port 80 pointing to 10.10.30.10:80.

2. Enforce DMZ Zone Isolation Rules

Navigate to Firewall > Rules > DMZ. The DMZ must be strictly isolated from the rest of your network. Traffic initiated from the DMZ should only be permitted out to the internet (for package updates and upstream DNS) and to specific defined backend service ports on VLAN 20:

Action Protocol Source Destination Port Description
Pass IPv4 UDP DMZ net 10.10.10.10 (DC01) 53 Allow DNS queries to internal Active Directory DNS.
Pass IPv4 TCP 10.10.30.10 10.10.20.30 (Grafana) 3000 Pinhole rule: allow proxy to reach internal Grafana service.
Pass IPv4 TCP 10.10.30.10 10.10.10.2 (PVE01) 8006 Pinhole rule: allow proxy to reach Proxmox Web UI.
Block IPv4 * DMZ net 10.10.0.0/16 (RFC1918) * Block DMZ from initiating traffic to ANY internal homelab subnet.
Pass IPv4 * DMZ net * * Default outbound internet access for package updates and API calls.

This rule structure enforces true zero-trust isolation: even if an attacker achieves root execution inside the Nginx container, they cannot scan your LAN, cannot contact your domain controllers on port 445 or 389, and cannot access management interfaces.

Troubleshooting Reverse Proxy Failures Cheat Sheet

When troubleshooting reverse proxies, errors can originate at DNS resolution, the edge firewall, the proxy engine, or the backend target. Use this diagnostic matrix to pinpoint failures:

Symptom Likely Cause Fix
502 Bad Gateway error NPM cannot establish a TCP connection to the upstream server IP or port. Verify the backend service is running, check pfSense DMZ firewall rules for missing pinhole pass rules, and ensure you selected the correct scheme (HTTP vs HTTPS).
Proxmox noVNC console disconnects immediately WebSockets are disabled on the proxy host definition in NPM. Edit the proxy host in NPM, open the Details tab, and toggle Websockets Support to ON.
DNS-01 challenge fails with authentication error Cloudflare API token lacks necessary permissions or has expired. Regenerate the token in Cloudflare ensuring it has Zone.DNS:Edit permission scoped to the exact domain zone, and set propagation seconds to 120.
ERR_TOO_MANY_REDIRECTS loop Cloudflare SSL is set to “Flexible” while NPM enforces HTTPS redirect. In Cloudflare dashboard under SSL/TLS, change encryption mode from Flexible to Full (strict).
Client receives HTTP 403 Forbidden Client IP is not listed in the NPM Access List attached to the proxy host. Verify the client is connected to the WireGuard VPN tunnel (10.10.50.0/24) or update the Access List allow rules to include the client subnet.

Final Thoughts

A reverse proxy is the ultimate bridge between the private isolation of an enterprise homelab and the convenience of modern web access. By deploying Nginx Proxy Manager inside an isolated DMZ VLAN, leveraging Cloudflare DNS-01 for zero-inbound-port wildcard TLS certificates, and enforcing strict firewall pinholes on pfSense, you achieve production-tier functionality without compromising security.

Your internal services enjoy clean, memorable URLs and fully validated cryptographic identities, while your core Active Directory domain and hypervisor infrastructure remain completely shielded behind enterprise perimeter boundaries.

Key takeaway: Always terminate reverse proxy traffic in an isolated DMZ segment, leverage DNS-01 challenges to eliminate inbound port 80 dependencies, and pair every proxy host with granular pfSense pinhole rules and NPM access lists to enforce strict zero-trust isolation.
Next in this series

Next, in Part 16 of the Homelab Build series, we will cover Identity Federation & Single Sign-On: integrating Authentik or Keycloak with our Active Directory forest via LDAP to provide unified MFA and SAML/OIDC authentication across all homelab web applications.