WireGuard VPN Setup: Secure Remote Access to Your Infrastructure
WireGuard has fundamentally changed VPN technology. Where OpenVPN requires hundreds of lines of configuration, complex certificate management, and careful cipher selection, WireGuard achieves the same result with a few dozen lines of configuration and a single, modern cryptographic protocol. It is fast, simple, auditable (about 4,000 lines of code in the Linux kernel module), and has been part of the mainline Linux kernel since version 5.6.
For self-hosted infrastructure, WireGuard is the best way to securely access your homelab services from anywhere. This guide covers everything from basic single-client setup to multi-site architectures.
WireGuard vs. OpenVPN
| Feature | WireGuard | OpenVPN |
|---|---|---|
| Code complexity | ~4,000 lines | ~100,000 lines |
| Protocol | UDP only | UDP or TCP |
| Encryption | ChaCha20, Curve25519, BLAKE2s | Configurable (many options) |
| Performance | Near wire speed (kernel module) | Good (userspace) |
| Connection establishment | ~100ms | ~5-10 seconds |
| Configuration | ~15-20 lines | ~50-100 lines |
| Roaming support | Built-in (seamless IP changes) | Reconnection required |
| Stealth / obfuscation | Limited | Better (TCP mode, stunnel) |
When to choose OpenVPN: If you need TCP transport (to bypass restrictive firewalls), traffic obfuscation, or compatibility with very old devices, OpenVPN remains relevant. For everything else, WireGuard is the better choice.
Installation
# Debian/Ubuntu
sudo apt install -y wireguard
# Fedora/RHEL
sudo dnf install -y wireguard-tools
# Arch Linux
sudo pacman -S wireguard-tools
# Verify kernel module is available
sudo modprobe wireguard
lsmod | grep wireguard
Key Generation
WireGuard uses Curve25519 key pairs. Each peer (server and every client) needs its own key pair:
# Generate server key pair
wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key
chmod 600 /etc/wireguard/server_private.key
# Generate client key pair
wg genkey | tee client1_private.key | wg pubkey > client1_public.key
# Generate a preshared key for additional security (optional, quantum-resistant)
wg genpsk > client1_preshared.key
# View keys
cat /etc/wireguard/server_public.key
cat client1_public.key
Server Configuration
# /etc/wireguard/wg0.conf (Server)
[Interface]
# Server's private key
PrivateKey = SERVER_PRIVATE_KEY_HERE
# VPN subnet address for the server
Address = 10.0.0.1/24
# UDP port to listen on
ListenPort = 51820
# Enable IP forwarding and NAT when the interface comes up
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
# Save configuration changes made at runtime
SaveConfig = false
# Client 1: Laptop
[Peer]
PublicKey = CLIENT1_PUBLIC_KEY_HERE
PresharedKey = CLIENT1_PRESHARED_KEY_HERE
# IP address assigned to this client
AllowedIPs = 10.0.0.2/32
# Client 2: Phone
[Peer]
PublicKey = CLIENT2_PUBLIC_KEY_HERE
AllowedIPs = 10.0.0.3/32
# Client 3: Remote server (site-to-site)
[Peer]
PublicKey = CLIENT3_PUBLIC_KEY_HERE
AllowedIPs = 10.0.0.4/32, 192.168.2.0/24
Endpoint = remote-server.example.com:51820
PersistentKeepalive = 25
# Enable IP forwarding (required for routing traffic through the VPN)
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
echo "net.ipv6.conf.all.forwarding = 1" | sudo tee -a /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
# Start WireGuard
sudo wg-quick up wg0
# Enable on boot
sudo systemctl enable wg-quick@wg0
# Check status
sudo wg show
Client Configuration
# Client configuration (laptop/desktop)
# /etc/wireguard/wg0.conf
[Interface]
PrivateKey = CLIENT1_PRIVATE_KEY_HERE
Address = 10.0.0.2/24
DNS = 192.168.1.100 # Pi-hole on your homelab
[Peer]
PublicKey = SERVER_PUBLIC_KEY_HERE
PresharedKey = CLIENT1_PRESHARED_KEY_HERE
Endpoint = your-public-ip-or-ddns:51820
# Route ALL traffic through the VPN (full tunnel)
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
Split Tunneling
Full tunneling routes all client traffic through the VPN, which uses your home internet bandwidth for everything. Split tunneling only routes traffic destined for your homelab network through the VPN:
# Split tunnel: only route homelab traffic through VPN
[Interface]
PrivateKey = CLIENT1_PRIVATE_KEY_HERE
Address = 10.0.0.2/24
# Note: DNS is NOT set here for split tunnel
# Your local DNS handles everything else
[Peer]
PublicKey = SERVER_PUBLIC_KEY_HERE
Endpoint = your-public-ip-or-ddns:51820
# Only route these subnets through the VPN:
AllowedIPs = 10.0.0.0/24, 192.168.1.0/24
PersistentKeepalive = 25
# Full tunnel: route everything through VPN
# AllowedIPs = 0.0.0.0/0, ::/0
Firewall Configuration
# Allow WireGuard traffic through the firewall
sudo ufw allow 51820/udp comment "WireGuard"
# If using iptables directly:
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Allow traffic from the VPN subnet to the LAN
sudo iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
# Make iptables rules persistent
sudo apt install -y iptables-persistent
sudo netfilter-persistent save
Mobile Clients
WireGuard has official apps for iOS and Android. Generate a configuration file and display it as a QR code for easy mobile setup:
# Install qrencode
sudo apt install -y qrencode
# Create a mobile client configuration
cat > phone.conf <<EOF
[Interface]
PrivateKey = PHONE_PRIVATE_KEY_HERE
Address = 10.0.0.3/24
DNS = 192.168.1.100
[Peer]
PublicKey = SERVER_PUBLIC_KEY_HERE
Endpoint = your-public-ip-or-ddns:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF
# Generate QR code for the mobile app to scan
qrencode -t ansiutf8 < phone.conf
# Or generate a PNG image
qrencode -t png -o phone-wireguard.png < phone.conf
Docker Integration with wg-easy
wg-easy provides a web UI for managing WireGuard clients, making it much simpler to add and remove peers:
# docker-compose.yml for wg-easy
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy
container_name: wg-easy
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
environment:
WG_HOST: your-ddns-hostname.example.com
PASSWORD_HASH: '$$2a$$12$$hashed_password_here'
WG_PORT: 51820
WG_DEFAULT_ADDRESS: 10.8.0.x
WG_DEFAULT_DNS: 192.168.1.100
WG_ALLOWED_IPS: 0.0.0.0/0, ::/0
WG_PERSISTENT_KEEPALIVE: 25
ports:
- "51820:51820/udp"
- "51821:51821/tcp" # Web UI
volumes:
- wg_easy_data:/etc/wireguard
volumes:
wg_easy_data:
Site-to-Site VPN
Connect two networks (e.g., your home and a VPS) so devices on either network can communicate directly:
# Site A: Home (192.168.1.0/24) - wg0.conf
[Interface]
PrivateKey = SITE_A_PRIVATE_KEY
Address = 10.0.0.1/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = SITE_B_PUBLIC_KEY
Endpoint = vps.example.com:51820
AllowedIPs = 10.0.0.2/32, 10.10.10.0/24
PersistentKeepalive = 25
# Site B: VPS (10.10.10.0/24) - wg0.conf
[Interface]
PrivateKey = SITE_B_PRIVATE_KEY
Address = 10.0.0.2/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = SITE_A_PUBLIC_KEY
Endpoint = home.example.com:51820
AllowedIPs = 10.0.0.1/32, 192.168.1.0/24
PersistentKeepalive = 25
Dynamic DNS for Home Connections
Most home internet connections have dynamic IP addresses. Use a DDNS service so your VPN clients can always find your server:
# Using DuckDNS (free)
# Add to crontab:
*/5 * * * * curl -s "https://www.duckdns.org/update?domains=myhomelab&token=YOUR-TOKEN&ip=" > /dev/null
# Or run ddclient in Docker
docker run -d \
--name ddclient \
--restart unless-stopped \
-v ddclient_config:/config \
lscr.io/linuxserver/ddclient:latest
# /config/ddclient.conf
protocol=cloudflare
zone=example.com
login=token
password=your-cloudflare-api-token
home.example.com
Monitoring WireGuard
# Check WireGuard status
sudo wg show
# Output includes:
# - Each peer's public key
# - Latest handshake time
# - Data transferred
# - Endpoint IP:port
# Monitor with Prometheus using wireguard_exporter
docker run -d \
--name wg-exporter \
--cap-add NET_ADMIN \
--network host \
mindflavor/prometheus-wireguard-exporter
# Then add to prometheus.yml:
# - job_name: wireguard
# static_configs:
# - targets: ['localhost:9586']
With usulnet's agent architecture, you can manage Docker containers across sites connected by WireGuard. The agent on each remote site communicates with the master over your VPN, giving you unified container management across all your locations.
Security Best Practices
- Use preshared keys for an additional layer of post-quantum security.
- Restrict AllowedIPs to the minimum necessary. Do not give every client access to your entire network.
- Rotate keys periodically. Generate new key pairs quarterly.
- Use a non-standard port (not 51820) to reduce scan noise.
- Monitor handshake timestamps. A peer that has not handshaked recently may indicate a compromised or decommissioned device.
- Immediately remove peers for lost or decommissioned devices.