homelab-wireguard-vpn
WireGuard VPN server setup, peer configuration, key generation, split tunneling vs full tunnel routing, and remote access to a home network from mobile and laptop clients.
Homelab WireGuard VPN
WireGuard is a fast, modern VPN protocol. It is the right choice for remote access to a home network — simpler to configure than OpenVPN and faster than most alternatives.
All configuration examples show common setups. Review each command — especially the iptables forwarding rules and key file permissions — before applying them to your system, and make changes in a maintenance window.
When to Use
- Setting up WireGuard server on a Raspberry Pi, Linux host, pfSense, or router
- Generating WireGuard keypairs and writing peer config files
- Configuring remote access from a phone or laptop to a home network
- Explaining split tunneling (route only home traffic) vs full tunnel (route all traffic)
- Troubleshooting WireGuard connections that will not come up
- Automating peer configuration generation for multiple clients
How WireGuard Works
Your phone (WireGuard client) │ │ Encrypted UDP tunnel (port 51820) │Your home router (WireGuard server — needs a public IP or DDNS) │ Your home network (192.168.1.0/24, NAS, Pi, etc.)
Every device has a keypair (public + private key).The server knows each client's public key.The client knows the server's public key + endpoint (IP:port).Traffic is encrypted end-to-end with no central server or certificate authority.Server Setup (Linux)
# Install WireGuardsudo apt update && sudo apt install wireguard -y
# Generate server keypair — create files with private permissions from the startsudo mkdir -p /etc/wireguardsudo sh -c 'umask 077; wg genkey > /etc/wireguard/server_private.key'sudo sh -c 'wg pubkey < /etc/wireguard/server_private.key > /etc/wireguard/server_public.key'
# Write server config — substitute the actual private key value# Do not store private keys in version control or share themsudo tee /etc/wireguard/wg0.conf << 'EOF'[Interface]Address = 10.8.0.1/24 # VPN subnet — server gets .1ListenPort = 51820PrivateKey = <paste_server_private_key_here>
# Scoped forwarding rules: allow VPN traffic in/out, not a blanket FORWARD ACCEPTPostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPTPostUp = iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPTPostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADEPostDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPTPostDown = iptables -D FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPTPostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]# Phone — replace with the actual phone public keyPublicKey = <phone_public_key>AllowedIPs = 10.8.0.2/32
[Peer]# Laptop — replace with the actual laptop public keyPublicKey = <laptop_public_key>AllowedIPs = 10.8.0.3/32EOFsudo chmod 600 /etc/wireguard/wg0.conf
# Replace eth0 with your actual outbound interface name# Check with: ip route show default
# Enable IP forwarding (required for routing traffic through the server)echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.confsudo sysctl --system
# Start WireGuard and enable on bootsudo wg-quick up wg0sudo systemctl enable wg-quick@wg0Client Configuration
# Generate a unique keypair for each client device# Run on the client, or on the server and transfer the private key securely — never in plaintextumask 077wg genkey | tee phone_private.key | wg pubkey > phone_public.key
# Client config file (phone_wg0.conf):[Interface]PrivateKey = <phone_private_key>Address = 10.8.0.2/32DNS = 192.168.1.2 # Optional: use Pi-hole for DNS over the tunnel
[Peer]PublicKey = <server_public_key>Endpoint = your-home-ip.ddns.net:51820 # Your public IP or DDNS hostnameAllowedIPs = 192.168.1.0/24 # Split tunnel: only home network traffic# AllowedIPs = 0.0.0.0/0, ::/0 # Full tunnel: all traffic through VPN
PersistentKeepalive = 25 # Keep NAT hole open (required for mobile clients)Split Tunnel vs Full Tunnel
# Split tunnel: AllowedIPs = 192.168.1.0/24 Only traffic destined for your home network goes through the VPN. Internet traffic (YouTube, Spotify) goes directly — better performance on mobile. Best for: "I just want to reach my NAS and Pi from anywhere."
# Full tunnel: AllowedIPs = 0.0.0.0/0, ::/0 ALL traffic goes through your home internet connection. Useful for: piggybacking home DNS/Pi-hole ad blocking. Downside: home upload speed becomes your bottleneck everywhere.
# Multi-subnet split tunnel (most common homelab use case): AllowedIPs = 192.168.10.0/24, 192.168.20.0/24, 192.168.30.0/24, 10.8.0.0/24 Routes all your VLANs through the tunnel; internet stays direct.Key Generation and Peer Management
import subprocess
def generate_keypair() -> tuple[str, str]: """Generate a WireGuard keypair. Returns (private_key, public_key).""" private = subprocess.check_output(["wg", "genkey"]).decode().strip() public = subprocess.run( ["wg", "pubkey"], input=private.encode(), capture_output=True ).stdout.decode().strip() return private, public
def generate_preshared_key() -> str: return subprocess.check_output(["wg", "genpsk"]).decode().strip()
def build_client_config( client_private_key: str, client_vpn_ip: str, # e.g. "10.8.0.3" server_public_key: str, server_endpoint: str, # e.g. "home.example.com:51820" allowed_ips: str = "192.168.1.0/24", dns: str = "",) -> str: dns_line = f"DNS = {dns}\n" if dns else "" return f"""[Interface]PrivateKey = {client_private_key}Address = {client_vpn_ip}/32{dns_line}[Peer]PublicKey = {server_public_key}Endpoint = {server_endpoint}AllowedIPs = {allowed_ips}PersistentKeepalive = 25"""
def build_server_peer_block( client_public_key: str, client_vpn_ip: str, comment: str = "",) -> str: comment_line = f"# {comment}\n" if comment else "" return f"""{comment_line}[Peer]PublicKey = {client_public_key}AllowedIPs = {client_vpn_ip}/32"""Keep private keys out of source control. If you use this script, write key material to files with mode 600 and never log or print it.
pfSense / OPNsense WireGuard
# pfSense: VPN → WireGuard → Add Tunnel Interface Keys: Generate (creates keypair automatically) Listen Port: 51820 Interface Address: 10.8.0.1/24
# Add Peer (one per client): Public Key: <client public key> Allowed IPs: 10.8.0.2/32
# Assign the WireGuard interface: Interfaces → Assignments → Add (select wg0) Enable interface, no IP needed (it is set in the tunnel config)
# Firewall rules: WAN → Allow UDP port 51820 inbound (so clients can reach the server) WireGuard interface → Allow traffic to LAN networks you want reachableDDNS (Dynamic DNS) for Home Servers
Most home internet connections have a dynamic IP. Use DDNS so your VPN endpoint stays reachable after an IP change.
# Option 1: Cloudflare DDNS — store credentials in a secrets file, not inline# docker-compose entry using an env file: ddns-updater: image: qmcgaw/ddns-updater env_file: ./ddns.env # store zone_id and token here, not in compose restart: unless-stopped
# ddns.env (chmod 600, not committed to git):# SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id# SETTINGS_CLOUDFLARE_TOKEN=your_api_token
# Option 2: DuckDNS (free, simple) Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org) Store token in /etc/ddns.env (mode 600), then use a small root-owned script:
# /usr/local/bin/update-duckdns #!/bin/sh set -eu . /etc/ddns.env curl --fail --silent --show-error --max-time 10 \ --get "https://www.duckdns.org/update" \ --data-urlencode "domains=myhome" \ --data-urlencode "token=${DUCKDNS_TOKEN}" \ --data-urlencode "ip="
# Cron job: */5 * * * * /usr/local/bin/update-duckdns >/dev/null 2>&1Troubleshooting
# Check WireGuard status and last handshakesudo wg show
# If "latest handshake" is never or very old, the tunnel is not connected.# Check:# 1. Is UDP port 51820 open on the router/firewall?sudo ufw status # or check pfSense/UniFi firewall rules
# 2. Is the server public key in the client config correct?sudo wg show wg0 public-key # Compare to what is in the client config
# 3. Is IP forwarding enabled on the server?cat /proc/sys/net/ipv4/ip_forward # Should be 1
# 4. Does the client AllowedIPs cover the IP you are trying to reach?# If AllowedIPs = 192.168.1.0/24 and you are trying to reach 192.168.3.5, it will not route.
# Check kernel logs for WireGuard errorsdmesg | grep wireguard
# Restart WireGuardsudo wg-quick down wg0 && sudo wg-quick up wg0Anti-Patterns
# BAD: Storing private keys in version control or sharing them# Private keys are equivalent to passwords — never commit them to git
# BAD: Using AllowedIPs = 0.0.0.0/0 on mobile without considering the impact# Full tunnel routes all mobile traffic through your home upload — usually slow
# BAD: Not setting PersistentKeepalive on mobile clients# Mobile clients behind NAT drop idle tunnels without it
# BAD: Opening port 51820 in the firewall but forgetting IP forwarding on the server# Tunnel connects but no traffic routes — confusing to debug
# BAD: Sharing a keypair across multiple client devices# Each device must have its own unique keypair — shared keys break the security model
# BAD: Using a broad "FORWARD ACCEPT" iptables rule# Scope forwarding rules to the wg0 interface and direction onlyBest Practices
- Generate a unique keypair per client device — never reuse keys
- Use split tunneling (
AllowedIPs = <home subnets>) for mobile - Set
PersistentKeepalive = 25on all mobile clients - Use DDNS if your ISP assigns a dynamic IP; store credentials in env files, not inline
- Use scoped iptables forwarding rules (inbound on wg0 only) rather than a blanket FORWARD ACCEPT
- Add Pi-hole’s IP as
DNS =in client configs to get ad blocking over the VPN - Rotate the server keypair periodically and update all client configs
Related Skills
- homelab-network-setup
- homelab-vlan-segmentation
- homelab-pihole-dns