Your homelab is only useful if you can reach it. But opening ports on your router to the public internet is how homelabs get compromised, cryptomined, or turned into spam relays.
There is a better way. Two of them, actually.
WireGuard is a modern VPN protocol built directly into the Linux kernel. Fast, minimal, cryptographically solid. You control everything — the server, the keys, the routing.
Tailscale is WireGuard with a coordination layer on top. It handles key exchange, NAT traversal, and device management for you. Setup takes minutes instead of hours.
This guide covers both, completely. By the end you will understand the tradeoffs, have both running, and know exactly which one to use for each situation in your homelab.
The Core Problem Both Solve
Your homelab sits behind your home router. To reach it from outside — from your phone, your work laptop, a hotel Wi-Fi — you have three options:
Option A — Open a port on your router (bad)
Internet → router:22 → your server SSH
Every port scanner on the internet will find it within hours. Your SSH logs will fill with brute-force attempts. Eventually someone gets in.
Option B — VPN tunnel (good)
Internet → VPN server → encrypted tunnel → your homelab
Only authenticated devices can connect. No ports exposed. The surface area for attack is a single authenticated endpoint.
Option C — Tailscale mesh (also good, different tradeoffs)
Device A ←→ Tailscale coordination ←→ Device B
↘ direct peer-to-peer connection ↗
No VPN server to maintain. Devices connect directly to each other through NAT, authenticated by Tailscale’s control plane.
Both B and C are the right answer. Which one depends on your requirements.
WireGuard vs Tailscale: When to Use Which
| WireGuard | Tailscale | |
|---|---|---|
| Setup time | 30–60 min | 5 min |
| External dependency | None — fully self-hosted | Tailscale’s control plane |
| Works through strict NAT | Needs port forward | Yes — no port forward needed |
| Device limit (free) | Unlimited | 3 devices (free), 20 on Personal |
| Subnet routing | Manual config | Built-in, 2 clicks |
| Exit node (route all traffic) | Manual config | Built-in |
| Auditability | Complete — you own everything | Partial — key exchange via Tailscale |
| Best for | Production, privacy-first, full control | Rapid access, many devices, simplicity |
My actual setup: Tailscale for daily access (phone, laptop, quick remote work). WireGuard for the always-on tunnel from a VPS, and for anything where I don’t want any external dependency.
You don’t have to choose. Run both. They coexist without conflict.
Part 1 — WireGuard
Step 1 — Install WireGuard
On the server (Ubuntu/Debian):
sudo apt update
sudo apt install wireguard wireguard-tools -y
# Verify kernel module is loaded
lsmod | grep wireguard
# Should print: wireguard ...
WireGuard is built into the Linux kernel since 5.6. On Ubuntu 22.04+ it’s already there — the package just adds the userspace tools.
Step 2 — Generate Key Pairs
WireGuard uses public-key cryptography. Every peer — server and client — needs its own keypair.
# Generate server keys
cd /etc/wireguard
umask 077 # restrict permissions
wg genkey | tee server_private.key | wg pubkey > server_public.key
# Generate a client keypair (do this for each device)
wg genkey | tee client1_private.key | wg pubkey > client1_public.key
# View the keys
cat server_private.key # keep this secret
cat server_public.key # share this with clients
cat client1_private.key # keep this secret
cat client1_public.key # share this with the server
Never share a private key. The private key stays on the device that generated it. Only public keys are exchanged between peers.
Step 3 — Server Configuration
sudo nano /etc/wireguard/wg0.conf
[Interface]
# The VPN IP address for this server
Address = 10.10.0.1/24
# Port WireGuard listens on (open this on your router if behind NAT)
ListenPort = 51820
# Your server's private key
PrivateKey = <contents of server_private.key>
# Enable IP forwarding so clients can route through the server
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# ── Peer: Laptop ─────────────────────────────────────────────
[Peer]
# Client's public key
PublicKey = <contents of client1_public.key>
# IP this client gets on the VPN
AllowedIPs = 10.10.0.2/32
# ── Peer: Phone ──────────────────────────────────────────────
[Peer]
PublicKey = <phone_public_key>
AllowedIPs = 10.10.0.3/32
Replace eth0 with your actual network interface name — check with ip link.
Enable IP forwarding permanently:
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Step 4 — Start WireGuard
# Start the interface
sudo wg-quick up wg0
# Enable on boot
sudo systemctl enable wg-quick@wg0
# Check status
sudo wg show
wg show prints every peer, their public key, their allowed IPs, and
when they last sent a handshake. If you see latest handshake: X seconds ago
after connecting a client — the tunnel is live.
Open the firewall:
sudo ufw allow 51820/udp
sudo ufw reload
If your homelab is behind a home router, also add a port forward in your router settings: UDP port 51820 → your server’s LAN IP.
Step 5 — Client Configuration (Linux laptop)
sudo apt install wireguard -y
sudo nano /etc/wireguard/wg0.conf
[Interface]
# This client's VPN IP
Address = 10.10.0.2/24
# This client's private key
PrivateKey = <contents of client1_private.key>
# DNS server to use when connected (optional — use your server's VPN IP)
DNS = 10.10.0.1
[Peer]
# Server's public key
PublicKey = <contents of server_public.key>
# Your server's public IP (or dynamic DNS hostname) + WireGuard port
Endpoint = YOUR.PUBLIC.IP.ADDRESS:51820
# Route homelab traffic through the VPN
# Use 0.0.0.0/0 to route ALL traffic through the tunnel (full VPN)
# Use 10.10.0.0/24,192.168.1.0/24 to route only homelab traffic (split tunnel)
AllowedIPs = 10.10.0.0/24, 192.168.1.0/24
# Keep the connection alive through NAT
PersistentKeepalive = 25
Connect:
sudo wg-quick up wg0
# Test — ping the server over the VPN
ping 10.10.0.1
# Check connection
sudo wg show
Step 6 — Client Configuration (Phone)
Install the WireGuard app from your phone’s app store. The easiest way to configure it is with a QR code.
On the server, generate a QR code for the phone config:
# Install qrencode
sudo apt install qrencode -y
# Create the phone config
cat > /tmp/phone.conf << 'EOF'
[Interface]
Address = 10.10.0.3/24
PrivateKey = <phone_private_key>
DNS = 10.10.0.1
[Peer]
PublicKey = <server_public_key>
Endpoint = YOUR.PUBLIC.IP:51820
AllowedIPs = 10.10.0.0/24, 192.168.1.0/24
PersistentKeepalive = 25
EOF
# Display as QR code
qrencode -t ansiutf8 < /tmp/phone.conf
# Clean up the temp file (contains private key)
rm /tmp/phone.conf
On your phone: WireGuard app → + → Scan QR code → scan → connect.
Step 7 — Dynamic DNS (if you don’t have a static IP)
Most home ISPs give you a dynamic IP that changes occasionally. Without a
static IP, your WireGuard Endpoint goes stale.
Free solution — DuckDNS:
# Install curl if needed
sudo apt install curl -y
# Create update script
cat > /usr/local/bin/duckdns-update.sh << 'EOF'
#!/bin/bash
curl -s "https://www.duckdns.org/update?domains=YOURSUBDOMAIN&token=YOUR_TOKEN&ip=" \
>> /var/log/duckdns.log 2>&1
EOF
chmod +x /usr/local/bin/duckdns-update.sh
# Run every 5 minutes via cron
(crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/duckdns-update.sh") | crontab -
Get your token and subdomain at duckdns.org (free). Your WireGuard
endpoint becomes yoursubdomain.duckdns.org:51820 — works even when
your home IP changes.
Part 2 — Tailscale
Step 1 — Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
That’s it. One line installs the daemon and CLI on any Debian/Ubuntu system.
Step 2 — Authenticate
sudo tailscale up
This prints an authentication URL. Open it in a browser, log in with Google, GitHub, or email — your choice. The machine is now part of your Tailscale network (called a tailnet).
Check it worked:
tailscale status
You’ll see your machine listed with a 100.x.x.x Tailscale IP. Every
device on your tailnet gets a stable IP in the 100.64.0.0/10 range —
these never change, even if your home IP does.
Step 3 — Add More Devices
Install Tailscale on every device you want on the network — same one-liner
or the app store version for phones. After tailscale up and
authentication, all devices can reach each other via their 100.x.x.x IPs.
No port forwarding. No router config. Tailscale handles NAT traversal using the DERP relay network as fallback when direct connection isn’t possible.
Step 4 — Access Your Entire Homelab via Subnet Routing
By default, Tailscale only connects the devices that have it installed. Subnet routing lets one machine act as a gateway so you can reach your entire LAN — including devices that don’t have Tailscale.
On your homelab server:
# Advertise your LAN subnet
sudo tailscale up --advertise-routes=192.168.1.0/24
# Enable IP forwarding (required)
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
In the Tailscale admin console (tailscale.com/admin):
- Find your server → click the three dots → Edit route settings
- Enable the advertised subnet
Now from any Tailscale device you can reach 192.168.1.x — your Proxmox
web UI, your OpenMediaVault, your printers, everything — without installing
Tailscale on each one.
Step 5 — MagicDNS (Access by Hostname)
Tailscale can give every machine a DNS name so you don’t have to remember IPs.
In the admin console: DNS → enable MagicDNS.
Now instead of ssh user@100.64.x.x you can:
ssh user@homelab-server
ssh user@proxmox
And in your browser: http://proxmox:8006 opens the Proxmox web UI from
anywhere on your tailnet.
Step 6 — Serve a Local Service Publicly (with HTTPS)
Tailscale Serve exposes a local service on your tailnet with automatic HTTPS — the same feature used in the Ollama guide.
# Expose Open WebUI on your tailnet
tailscale serve --bg https / http://localhost:3000
# Expose Grafana
tailscale serve --bg https:3001 / http://localhost:3001
# List what's being served
tailscale serve status
Each service gets a URL like https://your-machine.tail1234.ts.net with
a valid TLS certificate. No reverse proxy, no Let’s Encrypt setup, no
Nginx config.
For public exposure (outside your tailnet):
tailscale funnel --bg https / http://localhost:3000
Funnel makes the service reachable from the public internet — useful for sharing a demo or webhook endpoint temporarily. Turn it off when done:
tailscale funnel --bg off
Step 7 — SSH via Tailscale (No Keys Needed)
Tailscale SSH replaces key-based SSH auth with Tailscale identity. No more
managing authorized_keys.
# Enable Tailscale SSH on the server
sudo tailscale up --ssh
From any other Tailscale device:
# Connect using your Tailscale username
ssh user@homelab-server
Tailscale handles authentication. Access is controlled through the admin console — you can restrict which users can SSH into which machines, require re-authentication for sensitive machines, and review SSH session logs.
Part 3 — General SSH Hardening
Whether you use WireGuard, Tailscale, or both — your SSH config should be hardened regardless. These settings reduce your attack surface significantly.
sudo nano /etc/ssh/sshd_config
# Disable root login — never allow direct root SSH
PermitRootLogin no
# Disable password auth — keys or Tailscale only
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
# Only allow specific users (replace with your username)
AllowUsers yourusername
# Use only modern key types
PubkeyAcceptedKeyTypes ssh-ed25519,ecdsa-sha2-nistp256,rsa-sha2-512,rsa-sha2-256
# Disconnect idle sessions after 10 minutes
ClientAliveInterval 300
ClientAliveCountMax 2
# Reduce login grace time
LoginGraceTime 20
# Disable X11 and other unnecessary features
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
# Limit authentication attempts
MaxAuthTries 3
MaxSessions 5
Apply the changes:
sudo systemctl reload sshd
# Test in a NEW terminal before closing your current session
ssh -i ~/.ssh/your_key yourusername@yourserver
Always test in a new terminal. If something is misconfigured you’ll lock yourself out — the existing session keeps you in while you fix it.
Part 4 — UFW Firewall Baseline
A proper firewall baseline complements both VPN setups.
# Default: deny all incoming, allow all outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing
# SSH — only from your LAN and Tailscale subnet
sudo ufw allow from 192.168.1.0/24 to any port 22
sudo ufw allow from 100.64.0.0/10 to any port 22 # Tailscale range
# WireGuard port (if using WireGuard)
sudo ufw allow 51820/udp
# Homelab services — LAN only
sudo ufw allow from 192.168.1.0/24 to any port 8006 # Proxmox
sudo ufw allow from 192.168.1.0/24 to any port 3001 # Grafana
sudo ufw allow from 192.168.1.0/24 to any port 9090 # Prometheus
sudo ufw allow from 192.168.1.0/24 to any port 5678 # n8n
# Enable
sudo ufw enable
# Review
sudo ufw status verbose
With this in place: SSH is only reachable from your LAN and your Tailscale devices. All homelab services are LAN-only. WireGuard handles external access. The internet sees a machine with no open ports except optionally WireGuard’s UDP port.
Checking Everything is Working
WireGuard:
# Server — see connected peers
sudo wg show
# Client — verify tunnel is up
sudo wg show
# Test connectivity through tunnel
ping 10.10.0.1 # ping the server VPN IP from client
ssh user@10.10.0.1 # SSH over VPN
curl http://10.10.0.1:3001 # reach Grafana over VPN
Tailscale:
# See all devices on your tailnet
tailscale status
# Check connectivity to another device
tailscale ping homelab-server
# View your Tailscale IP
tailscale ip -4
# Check routes being advertised/accepted
tailscale status --json | jq '.Peer[] | {name: .HostName, routes: .PrimaryRoutes}'
Troubleshooting
WireGuard: handshake never completes:
# Check if WireGuard port is reachable from outside
# (run this from a different network, e.g. phone hotspot)
nc -zvu YOUR.PUBLIC.IP 51820
# Check firewall
sudo ufw status | grep 51820
# Check if your public IP is correct
curl https://api.ipify.org
Most WireGuard connection failures are: wrong public IP in the client Endpoint, firewall blocking UDP 51820, or missing port forward on the router.
WireGuard: connected but can’t reach LAN devices:
IP forwarding is probably off:
cat /proc/sys/net/ipv4/ip_forward
# Should be 1 — if it's 0:
sudo sysctl -w net.ipv4.ip_forward=1
Tailscale: devices connected but can’t reach each other:
# Check if direct connection is established or going through relay
tailscale ping --verbose homelab-server
# Look for "direct" vs "via DERP relay"
If stuck on relay, the devices are behind symmetric NAT. This is normal for some ISPs — Tailscale still works, just slightly higher latency.
Tailscale: subnet routes not working:
Make sure you approved the routes in the admin console AND have IP forwarding enabled on the subnet router machine. Both are required.
Summary: My Recommended Setup
External access
└── Tailscale — phone, laptop, quick remote work
└── MagicDNS for human-readable hostnames
└── SSH via Tailscale on sensitive machines
Always-on tunnel
└── WireGuard — VPS → homelab permanent link
└── Useful for self-hosted services that need stable external IPs
Firewall (UFW)
└── Default deny incoming
└── SSH from LAN + Tailscale range only
└── All services LAN-only
└── WireGuard UDP port open
SSH hardening
└── No root login
└── No password auth
└── Key-only or Tailscale SSH
With this stack in place your homelab is reachable from anywhere, locked down from the internet, and auditable. Your attack surface is essentially zero — there are no open ports for scanners to find, and no passwords to brute-force.
That is how you run a homelab that stays yours.
[discussion]
Comments are powered by Giscus — backed by GitHub Discussions. Sign in with GitHub to join the conversation.