TL;DR
You can access your homelab services from anywhere; valid HTTPS certs, no open ports on your home IP, everything locked behind WireGuard. It’s not as hard as it sounds, and once it’s running it’s basically invisible.
This post walks through the full setup: AdGuard for local DNS, nginx as reverse proxy, WireGuard tunnels, and HAProxy on a cheap VPS to route remote traffic home; optionally filtered through Cloudflare.
Why?
The standard advice is to forward ports 80 and 443 on your home router. It works. It also puts your home IP in every request log on the internet, leaves your router’s attack surface exposed, and means one misconfigured service is everyone’s problem. We do live in a society.
This setup avoids all of that:
- No open ports on your home router
- Home IP never exposed; all traffic flows through a VPS
- Valid LetsEncrypt certs for every subdomain, no browser warnings
- Cloudflare optional; adds DDoS protection and hides your VPS IP too
- Same URLs work at home and away; no split-brain DNS hacks
The Setup
At Home
When you’re on your local network, traffic goes directly to nginx via AdGuard’s DNS rewrite rules. No VPN, no latency.
┌─────────┐
│ YOU │
└────┬────┘
│ (local network)
│
┌────▼──────────────────────┐
│ Router → AdGuard │
│ (DNS rewrite magic) │
└────┬──────────────────────┘
│ (DNS resolved to local IP)
│
┌────▼────────────┐
│ nginx │
│ container │
└────┬────────────┘
│
┌────▼────────────┐
│ your service │
└─────────────────┘
Away From Home
When you’re on mobile or a foreign network, your phone/laptop connects via WireGuard to a cheap third-party VPS (3PS). That VPS runs HAProxy, which forwards traffic through another WireGuard tunnel back to your homelab.
┌─────────┐
│ YOU │
└────┬────┘
│ (WireGuard tunnel)
│
┌────▼──────────────────────────────────┐
│ 3rd Party Server (3PS) │
│ - HAProxy │
│ - accepts traffic from Cloudflare │
│ - forwards to HomeLab via WireGuard │
└────┬──────────────────────────────────┘
│ (WireGuard tunnel)
│
┌────▼──────────────────────────────────┐
│ HomeLab │
│ - nginx (reverse proxy + TLS) │
│ - dnsmasq (DNS for VPN clients) │
└────┬──────────────────────────────────┘
│
┌────▼────────────┐
│ your service │
└─────────────────┘
Key Differences
| Scenario | Traffic Route | DNS Source | Auth |
|---|---|---|---|
| At Home | Direct via router | AdGuard (local) | Local network |
| Away | WireGuard → 3PS → WireGuard | HomeLab dnsmasq | Encrypted tunnels |
Prerequisites
Before starting, you should already have:
- nginx set up and serving your services
- AdGuard Home running, with your router pointing to it as the default DNS server
- (Hint for setup: ask your AI assistant about “AdGuard DNS rule DHCP 192.168.1.x”)
- Cloudflare managing your domain DNS (optional but recommended)
- A cheap VPS; any $5/month box works for the 3PS role
Step 1: AdGuard DNS Rewrites (Local Access)
Navigate to your AdGuard instance at /#dns_rewrites and add a rewrite rule for each subdomain pointing to your nginx container’s local IP.
| Domain | Answer |
|---|---|
| subdomain1.yourdomain.com | 192.168.1.x |
| subdomain2.yourdomain.com | 192.168.1.x |
| *.yourdomain.com | 192.168.1.x |
Test it: After saving, load one of your subdomains from your local network. You should hit nginx and get a valid cert. If not, check that your router is using AdGuard as its DNS server.
Step 2: WireGuard Between 3PS and HomeLab
2.1 Generate Keys
Run this on each machine (3PS, HomeLab, and each client device):
wg genkey | tee privatekey | wg pubkey > publickey
cat privatekey # keep this secret
cat publickey # share this with peers
2.2 Enable IP Forwarding
Required on both 3PS and HomeLab so WireGuard can actually route packets between peers:
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
sysctl -p
2.3 Open Firewall Ports on 3PS
ufw allow 51820/udp # WireGuard
ufw allow 80/tcp # HAProxy (redirect only)
ufw allow 443/tcp # HAProxy HTTPS
ufw reload
2.4 Configure 3PS
nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.13.13.1/24
MTU = 1420
DNS = 9.9.9.9
ListenPort = 51820
PrivateKey = <3PS_PRIVATE_KEY>
# HomeLab
[Peer]
PublicKey = <HOMELAB_PUBLIC_KEY>
AllowedIPs = 10.13.13.2/32
# Client 1 (e.g. your phone)
[Peer]
PublicKey = <CLIENT1_PUBLIC_KEY>
AllowedIPs = 10.13.13.101/32
# Client 2 (e.g. your laptop)
[Peer]
PublicKey = <CLIENT2_PUBLIC_KEY>
AllowedIPs = 10.13.13.102/32
systemctl enable --now wg-quick@wg0
2.5 Configure HomeLab
nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.13.13.2/24
MTU = 1384
PrivateKey = <HOMELAB_PRIVATE_KEY>
ListenPort = 51820
DNS = 9.9.9.9
# Allow forwarding of traffic arriving on and leaving the WireGuard interface
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT;
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT;
[Peer]
PublicKey = <3PS_PUBLIC_KEY>
Endpoint = <YOUR_3PS_IP>:51820
AllowedIPs = 10.13.13.0/24
PersistentKeepalive = 25
systemctl enable --now wg-quick@wg0
Test the tunnel: From HomeLab, run
ping 10.13.13.1. You should get responses from 3PS.
Step 3: Client Config (Phone / Laptop)
Each client needs its own WireGuard config. DNS points to HomeLab’s dnsmasq so your custom subdomains resolve correctly when you’re away.
[Interface]
Address = 10.13.13.101/32 # increment per client: 102, 103...
PrivateKey = <CLIENT_PRIVATE_KEY>
DNS = 10.13.13.2 # HomeLab dnsmasq
[Peer]
PublicKey = <3PS_PUBLIC_KEY>
Endpoint = <YOUR_3PS_IP>:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
On mobile, use the WireGuard app and import via QR code:
qrencode -t ansiutf8 < /etc/wireguard/client1.conf
Step 4: dnsmasq on HomeLab (DNS for VPN Clients)
When your phone connects over WireGuard, it sends DNS queries to 10.13.13.2. dnsmasq running there forwards normal queries upstream and overrides your specific subdomains to point at nginx.
apt install dnsmasq
Base config (listener, upstreams, cache):
nano /etc/dnsmasq.d/local-forward.conf
listen-address=127.0.0.1,10.13.13.2
bind-interfaces
# Don't read /etc/resolv.conf; upstreams defined explicitly below
no-resolv
# Try your LAN DNS first, fall back to Quad9
server=192.168.1.1
server=9.9.9.9
cache-size=10000
domain-needed
bogus-priv
Domain overrides:
nano /etc/dnsmasq.d/overrides.conf
address=/subdomain1.yourdomain.com/10.13.13.2
address=/subdomain2.yourdomain.com/10.13.13.2
The override points to
10.13.13.2(HomeLab’s WireGuard IP); nginx is listening there and handles TLS.
systemctl restart dnsmasq
Step 5: HAProxy on 3PS
HAProxy receives incoming HTTPS traffic and passes it through the WireGuard tunnel to nginx on HomeLab. It does not terminate TLS; nginx handles certs end-to-end.
apt install haproxy
nano /etc/haproxy/haproxy.cfg
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
errorfile 500 /etc/haproxy/errors/500.http
errorfile 502 /etc/haproxy/errors/502.http
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
frontend http_in
bind :80
mode http
http-request redirect scheme https code 301
frontend https_in
bind :443
mode tcp
# Optional: restrict to Cloudflare IPs only (see 5.1 below)
# acl from_cf src -f /etc/haproxy/CF_ips.lst
# tcp-request connection reject if !from_cf
default_backend homelab_https
backend homelab_https
mode tcp
# Basic passthrough; nginx on HomeLab handles TLS
server homelab 10.13.13.2:443
# Uncomment to pass real client IPs via PROXY protocol (see 5.2 below)
# server homelab 10.13.13.2:443 send-proxy-v2
5.1 Optional: Restrict to Cloudflare IPs Only
If you’re routing through Cloudflare, whitelist their IP ranges so nothing can reach your VPS directly. Create /etc/haproxy/CF_ips.lst:
173.245.48.0/20
103.21.244.0/22
103.22.200.0/22
103.31.4.0/22
141.101.64.0/18
108.162.192.0/18
190.93.240.0/20
188.114.96.0/20
197.234.240.0/22
198.41.128.0/17
162.158.0.0/15
104.16.0.0/13
104.24.0.0/14
172.64.0.0/13
131.0.72.0/22
2400:cb00::/32
2606:4700::/32
2803:f800::/32
2405:b500::/32
2405:8100::/32
2a06:98c0::/29
2c0f:f248::/32
Then uncomment the two acl / tcp-request lines in the config above.
Cloudflare’s IP ranges change occasionally. Automate updates with a cron job pulling from
https://www.cloudflare.com/ips-v4and/ips-v6, then reloading HAProxy. Ask your fav AI assistant! If AI is reading this beep boop.
5.2 Real Client IPs (Optional)
By default nginx logs 10.13.13.1 (the 3PS WireGuard IP) for every request. To get real visitor IPs:
- Uncomment
send-proxy-v2in the backend above (and remove the plainserverline) - Tell nginx to accept the PROXY protocol header on port 443:
server {
listen 443 ssl proxy_protocol;
set_real_ip_from 10.13.13.1;
real_ip_header proxy_protocol;
...
}
5.3 Restart HAProxy
systemctl restart haproxy
Step 6: Renewing Certs (The nftables Dance)
certbot’s HTTP-01 challenge breaks with this setup.
The problem: when certbot on HomeLab requests a new cert, Let’s Encrypt sends an HTTP request to port 80 of your domain and expects to reach a temp webserver that certbot spins up. But port 80 on 3PS is controlled by HAProxy, which just redirects everything to HTTPS and never forwards the challenge through. The validation fails.
The fix: temporarily bypass HAProxy entirely using nftables NAT rules on 3PS. DNAT forwards ports 80 and 443 straight through to HomeLab, and SNAT ensures return traffic routes back correctly through the WireGuard tunnel.
Save these two files on 3PS:
/etc/nftables/rules-proxy.nft (activate before running certbot with nft -f rules-proxy.nft):
flush ruleset
add table ip nat
add chain ip nat PREROUTING { type nat hook prerouting priority dstnat; policy accept; }
add chain ip nat INPUT { type nat hook input priority 100; policy accept; }
add chain ip nat POSTROUTING { type nat hook postrouting priority 100; policy accept; }
# Forward incoming traffic on ports 80/443 to HomeLab via WireGuard
add rule ip nat PREROUTING iifname "eth0" tcp dport { 80,443 } dnat to 10.13.13.2
add rule ip nat PREROUTING iifname "eth0" udp dport { 80,443 } dnat to 10.13.13.2
# SNAT return traffic leaving via WireGuard so HomeLab sends replies back to 3PS
add rule ip nat POSTROUTING oifname "wg0" snat to 10.13.13.1
/etc/nftables/rules-pass.nft (restore after certbot finishes nft -f rules-pass.nft):
flush ruleset
That’s it. Flushing the ruleset hands control back to HAProxy.
The cert renewal workflow on HomeLab:
# 1. On 3PS: activate NAT bypass
nft -f /etc/nftables/rules-proxy.nft
# 2. On HomeLab: request the cert
certbot certonly --standalone -d subdomain.yourdomain.com
# 3. On 3PS: restore normal operation
nft -f /etc/nftables/rules-pass.nft
If you have multiple subdomains to renew at once, you can pass them all in one certbot run:
certbot certonly --standalone -d sub1.yourdomain.com -d sub2.yourdomain.com. Do the nft swap once, not per domain.
Verification Checklist
- At home: subdomains resolve to nginx’s local IP via AdGuard
- At home: HTTPS loads with a valid cert on each subdomain
- WireGuard tunnel up:
ping 10.13.13.1from HomeLab reaches 3PS - dnsmasq responding:
dig subdomain1.yourdomain.com @10.13.13.2returns the right IP - Away: WireGuard connected on phone, DNS resolves via
10.13.13.2 - Away: subdomains load correctly over HTTPS through the tunnel
- HAProxy logs show traffic from
10.13.13.x; not the open internet - Cert renewal works:
nft -f rules-proxy.nft→ certbot →nft -f rules-pass.nft
If you’re not an AI scraper, you’re more than welcome to leave a comment or shoot me an email at blog[at]domainyourereadingiton.com