Back to Cheatsheets
Cheatsheet

WireGuard VPN Cheatsheet

WireGuard is a secure network tunnel operating at layers 3 and 4, designed as a modern alternative to IPsec and OpenVPN. It uses state-of-the-art cryptography (Curve25519, ChaCha20, Poly1305, BLAKE2s, SipHash24) and aims to be faster, simpler, leaner, and more useful.

Installation

Package Installation

# Debian / Ubuntu
apt update && apt install -y wireguard wireguard-tools resolvconf
 
# RHEL / Fedora / CentOS
dnf install -y wireguard-tools
 
# Arch Linux
pacman -S wireguard-tools
 
# Alpine Linux
apk add wireguard-tools
 
# Check installed version
wg --version
 
# Load kernel module
modprobe wireguard
lsmod | grep wireguard
 
# Enable module at boot
echo "wireguard" > /etc/modules-load.d/wireguard.conf

Kernel Support Verification

# Check if kernel supports WireGuard
uname -r
 
# For older kernels, install wireguard-dkms
apt install -y wireguard-dkms
 
# Verify module works
modinfo wireguard
 
# Check kernel config
zgrep CONFIG_WIREGUARD /proc/config.gz 2>/dev/null || cat /boot/config-$(uname -r) | grep CONFIG_WIREGUARD

Key Generation

Server Keys

# Generate private key
wg genkey | tee /etc/wireguard/server.key
# Output (private key): uM9F1qA7d...=
 
# Set restrictive permissions
chmod 600 /etc/wireguard/server.key
 
# Generate public key from private key
cat /etc/wireguard/server.key | wg pubkey | tee /etc/wireguard/server.pub
# Output (public key): 7J3pYkL...=
 
# One-liner: generate both keys at once
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
 
# Generate pre-shared key (optional, for extra encryption layer)
wg genpsk > /etc/wireguard/psk.key

Client Keys

# Generate client keys (same process as server)
wg genkey | tee /etc/wireguard/client1.key | wg pubkey > /etc/wireguard/client1.pub
 
# Set permissions
chmod 600 /etc/wireguard/client1.key
 
# Generate keys in memory (no files left on disk)
CLIENT_PRIV=$(wg genkey)
CLIENT_PUB=$(echo $CLIENT_PRIV | wg pubkey)
echo "Private: $CLIENT_PRIV"
echo "Public:  $CLIENT_PUB"

Server Configuration

Basic wg0.conf

# /etc/wireguard/wg0.conf — Server configuration
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>
 
# Enable IP forwarding (required)
# sysctl -w net.ipv4.ip_forward=1
# sysctl -w net.ipv6.conf.all.forwarding=1
 
# PostUp rules — run when interface comes up
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
 
# PostDown rules — run when interface goes down
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
 
# Optional: DNS for clients (if using full tunnel)
# DNS = 1.1.1.1, 8.8.8.8
 
# Optional: MTU setting (default 1420, reduce if issues)
# MTU = 1280
 
[Peer]
# Client 1
PublicKey = <client1-public-key>
AllowedIPs = 10.0.0.2/32
 
[Peer]
# Client 2
PublicKey = <client2-public-key>
AllowedIPs = 10.0.0.3/32

IPv6 Support

# Dual-stack wg0.conf
[Interface]
Address = 10.0.0.1/24, fd00::1/64
ListenPort = 51820
PrivateKey = <server-private-key>
 
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostUp = ip6tables -A FORWARD -i wg0 -j ACCEPT
PostUp = ip6tables -A FORWARD -o wg0 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
 
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
# ... (corresponding Down rules)
 
[Peer]
PublicKey = <client-public-key>
AllowedIPs = 10.0.0.2/32, fd00::2/128

Client Configuration

Linux Client wg0.conf

# /etc/wireguard/wg0.conf — Linux client
[Interface]
PrivateKey = <client-private-key>
Address = 10.0.0.2/24
DNS = 1.1.1.1, 8.8.8.8
 
[Peer]
PublicKey = <server-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0    # Full tunnel (all traffic)
# AllowedIPs = 10.0.0.0/24, 192.168.1.0/24  # Split tunnel (specific subnets)
PersistentKeepalive = 25

macOS / iOS / Android Client Config

# Same format, but usually imported via QR code or file
# For Apple platforms: use WireGuard app from App Store
# For Android: use WireGuard app from Play Store
 
# Mobile config — use DNS and exclude private IPs from tunnel
[Interface]
PrivateKey = <client-private-key>
Address = 10.0.0.4/24
DNS = 1.1.1.1
 
[Peer]
PublicKey = <server-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
# Exclude local network (Android supports ExcludedIPs)
# ExcludedIPs = 192.168.0.0/16, 10.0.0.0/8
PersistentKeepalive = 25

Management Commands

Interface Control

# Start WireGuard interface
wg-quick up wg0
 
# Stop WireGuard interface
wg-quick down wg0
 
# Restart
wg-quick down wg0 && wg-quick up wg0
 
# Start via systemd
systemctl start wg-quick@wg0
 
# Enable at boot
systemctl enable wg-quick@wg0
 
# Status
systemctl status wg-quick@wg0
 
# Check if interface is up
ip link show wg0
 
# Show IP addresses on wg0
ip addr show wg0

Status & Monitoring

# Show WireGuard status (peers, transfer, latest handshake)
wg show
 
# Show brief summary
wg show wg0
 
# Show dump (machine-parsable format)
wg show wg0 dump
 
# Show all interfaces
wg show all
 
# Show per-peer transfer counts
wg show wg0 transfer
 
# Show peers only
wg show wg0 peers
 
# Show latest handshake times
wg show wg0 latest-handshakes
 
# Real-time monitoring (watch)
watch -n 1 wg show
 
# JSON output (for parsing)
wg show wg0 json

Manual Configuration

# Add peer without restarting
wg set wg0 peer <public-key> allowed-ips 10.0.0.5/32
 
# Remove peer
wg set wg0 peer <public-key> remove
 
# Change endpoint at runtime
wg set wg0 peer <public-key> endpoint new-vpn.example.com:51820
 
# Set persistent keepalive
wg set wg0 peer <public-key> persistent-keepalive 25
 
# Generate new private key on running interface
wg set wg0 private-key /etc/wireguard/server.key
 
# Reload configuration without disconnecting
wg syncconf wg0 <(wg-quick strip wg0)

Routing

AllowedIPs Explained

# AllowedIPs = 10.0.0.2/32
# Only traffic to 10.0.0.2 goes through WireGuard (client's own IP)
# Used for point-to-point connections
 
# AllowedIPs = 0.0.0.0/0, ::/0
# ALL IPv4 and IPv6 traffic through WireGuard — full tunnel
# Everything goes through the VPN (default gateway via tunnel)
 
# AllowedIPs = 10.0.0.0/24, 192.168.1.0/24
# Split tunnel — only traffic to these subnets goes through WireGuard
# Everything else uses the normal internet connection
 
# AllowedIPs = 0.0.0.0/0, ::/0, 192.168.0.0/16
# Full tunnel but still access local network
# Client routes 0.0.0.0/0 through tunnel but local network is directly accessible

Split Tunnel vs Full Tunnel

# Full tunnel configuration (client)
[Peer]
PublicKey = <server-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0    # All traffic via VPN
 
# Split tunnel configuration (client)
[Peer]
PublicKey = <server-public-key>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.0.0.0/24, 10.0.1.0/24, 192.168.1.0/24
# Only specific subnets via VPN, rest goes directly to internet
 
# Split tunnel — access company network only
AllowedIPs = 172.16.0.0/12, 10.10.0.0/16
 
# Disable default route manipulation (Table=off)
[Interface]
PrivateKey = <client-private-key>
Address = 10.0.0.2/24
Table = off
 
# Use custom routing table
[Interface]
PrivateKey = <client-private-key>
Address = 10.0.0.2/24
Table = 1234
# Then add rules manually: ip rule add from 10.0.0.2 table 1234

Advanced Routing

# Policy-based routing (using fwmark)
[Interface]
PrivateKey = <client-private-key>
Address = 10.0.0.2/24
Table = 51820
FwMark = 0x51820
 
# Then add routing rules
ip rule add not fwmark 0x51820 table 51820
ip rule add table main suppress_prefixlength 0
 
# Multiple interfaces on same machine
# wg0: 10.0.0.0/24 — office network
# wg1: 10.0.1.0/24 — data center

Advanced Features

Pre-Shared Keys (PSK)

# Generate a pre-shared key
wg genpsk > /etc/wireguard/peer1-psk.key
 
# Add PSK to server config
[Peer]
PublicKey = <client-public-key>
PresharedKey = <peer1-psk contents>
AllowedIPs = 10.0.0.2/32
 
# Add PSK to client config
[Peer]
PublicKey = <server-public-key>
PresharedKey = <same-psk-contents>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.0.0.0/24
 
# PSK adds an additional layer of symmetric encryption
# Protects against potential future quantum attacks on Curve25519
# Also mitigates身份 verification issues if the public key is compromised

Multi-Peer Server Configuration

# /etc/wireguard/wg0.conf — Multi-client setup
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>
 
# Each client gets a unique IP and key
[Peer]
# Alice — laptop
PublicKey = <alice-public-key>
PresharedKey = <alice-psk>
AllowedIPs = 10.0.0.2/32
 
[Peer]
# Bob — phone
PublicKey = <bob-public-key>
PresharedKey = <bob-psk>
AllowedIPs = 10.0.0.3/32
 
[Peer]
# Charlie — home server
PublicKey = <charlie-public-key>
PresharedKey = <charlie-psk>
AllowedIPs = 10.0.0.4/32, 192.168.1.0/24
 
# Peers can communicate with each other (requires IP forwarding on server)
# Alice (10.0.0.2) can ping Bob (10.0.0.3) through the server

Dynamic DNS Endpoint

# Use a cron job to resolve DDNS hostname periodically
cat > /etc/cron.hourly/wireguard-refresh << 'SCRIPT'
#!/bin/bash
# Refresh WireGuard peer endpoints with dynamic DNS
ENDPOINT="vpn.example.com:51820"
PEER="<server-public-key>"
 
# Check if endpoint needs updating
CURRENT_IP=$(wg show wg0 endpoints | awk '{print $2}' | cut -d: -f1)
RESOLVED_IP=$(dig +short $ENDPOINT | head -n1)
 
if [ "$CURRENT_IP" != "$RESOLVED_IP" ] && [ -n "$RESOLVED_IP" ]; then
  wg set wg0 peer $PEER endpoint "${RESOLVED_IP}:51820"
  logger "WireGuard: Updated endpoint to ${RESOLVED_IP}"
fi
SCRIPT
chmod +x /etc/cron.hourly/wireguard-refresh
 
# Alternative: use a systemd timer
# /etc/systemd/system/wg-refresh.service
# /etc/systemd/system/wg-refresh.timer

NAT Traversal

# WireGuard handles NAT traversal natively (UDP punch-through)
# Server must have port forwarded or be on public IP
 
# PersistentKeepalive keeps NAT mapping alive
[Peer]
PersistentKeepalive = 25   # Send keepalive every 25 seconds
 
# For symmetric NATs, use lower keepalive interval
PersistentKeepalive = 5
 
# Double NAT scenarios — use relay or port forwarding
# Option 1: Port forward on both NAT devices
# Option 2: Use a relay server with public IP (TCP tunnel)
 
# Force WireGuard over TCP (using udp2raw or similar)
# Not natively supported — use udp2raw for TCP fallback
# udp2raw-tunnel -c -l 0.0.0.0:3333 -r server:51820 --raw-mode faketcp

QR Code Generation

For Mobile Clients

# Generate QR code directly from config file
cat /etc/wireguard/client1.conf | qrencode -t ansiutf8
 
# Save QR code as PNG image
cat /etc/wireguard/client1.conf | qrencode -o /root/wg-client1.png
 
# One-liner to generate config and QR
cat > /tmp/wg-phone.conf << EOF
[Interface]
PrivateKey = $(wg genkey)
Address = 10.0.0.5/24
DNS = 1.1.1.1
 
[Peer]
PublicKey = $(cat /etc/wireguard/server.pub)
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
EOF
qrencode -t ansiutf8 < /tmp/wg-phone.conf
 
# Install qrencode if needed
apt install -y qrencode
 
# Scan QR code with WireGuard mobile app to import

Troubleshooting

Connection Issues

# Check if interface is up
wg show wg0
 
# Check for recent handshake
wg show wg0 latest-handshakes
# If latest handshake is > 2 minutes, peer may not be connecting
 
# Check endpoint connectivity
nc -zv vpn.example.com 51820
# or
telnet vpn.example.com 51820
 
# Test UDP connectivity
nmap -sU -p 51820 vpn.example.com
 
# Check if WireGuard port is reachable from outside
# Use a remote tool or: timeout 3 bash -c 'echo > /dev/udp/vpn.example.com/51820'
 
# Check firewall on server
iptables -L -n -v | grep 51820
ufw status | grep 51820
firewall-cmd --list-ports | grep 51820

MTU Issues

# Default MTU is 1420 bytes (avoids fragmentation for most paths)
# Check current MTU
ip link show wg0 | grep mtu
 
# Test with different MTU values
# Common values: 1420 (default), 1280 (safe for IPv6), 1500 (if no encapsulation issues)
 
# Find optimal MTU
ping -M do -s 1472 -c 3 10.0.0.1   # Max size for 1500 MTU tunnel
ping -M do -s 1392 -c 3 10.0.0.1   # For 1420 tunnel MTU
 
# Set MTU in config
[Interface]
PrivateKey = <private-key>
Address = 10.0.0.2/24
MTU = 1280
 
# Common MTU issues: web pages loading partially, SSH hangs, VoIP breaks
# Reduction to 1280 usually fixes all path MTU issues

Debugging & Verbose Logging

# Enable dynamic debug for WireGuard
echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control
 
# View kernel debug messages
dmesg -wH | grep wireguard
 
# Or use journalctl
journalctl -k -f | grep wireguard
 
# Check for errors in systemd service
journalctl -u wg-quick@wg0 -n 50
 
# Peer not connecting — verify keys
# Check that the public key on one side matches the private key on the other
echo "Server private: $(cat /etc/wireguard/server.key | wg pubkey)"
echo "Client expects: $(cat /etc/wireguard/server.pub)"
# They MUST match
 
# Check for config syntax errors
wg-quick up wg0 2>&1
 
# Test configuration without bringing up interface
wg addconf wg0 <(wg-quick strip wg0) 2>&1

Firewall Rules

# Required firewall rules (server side)
# Allow WireGuard UDP port
iptables -A INPUT -p udp --dport 51820 -j ACCEPT
ip6tables -A INPUT -p udp --dport 51820 -j ACCEPT
 
# Allow forwarding between wg0 and eth0
iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
iptables -A FORWARD -i eth0 -o wg0 -j ACCEPT
 
# NAT for outbound traffic
iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
 
# UFW example
ufw allow 51820/udp
 
# firewalld example
firewall-cmd --add-port=51820/udp --permanent
firewall-cmd --reload
 
# Kill switch (client side) — drop all traffic if VPN drops
# In client config:
PostUp = iptables -I OUTPUT ! -o wg0 -m mark ! --mark $(wg show wg0 fwmark) -m state --state NEW -j REJECT
PostDown = iptables -D OUTPUT ! -o wg0 -m mark ! --mark $(wg show wg0 fwmark) -m state --state NEW -j REJECT

Scenario Examples

Remote Access VPN

# Single server with multiple remote clients
# Server (office): 10.0.0.1/24
# Client laptop:   10.0.0.2/24
# Client phone:    10.0.0.3/24
# Client desktop:  10.0.0.4/24
 
# Server wg0.conf
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <server-private>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
 
[Peer]
# Laptop
PublicKey = <laptop-public>
AllowedIPs = 10.0.0.2/32
 
[Peer]
# Phone
PublicKey = <phone-public>
AllowedIPs = 10.0.0.3/32
 
[Peer]
# Desktop
PublicKey = <desktop-public>
AllowedIPs = 10.0.0.4/32
 
# Client laptop wg0.conf
[Interface]
PrivateKey = <laptop-private>
Address = 10.0.0.2/24
DNS = 10.0.0.1
 
[Peer]
PublicKey = <server-public>
Endpoint = office.example.com:51820
AllowedIPs = 10.0.0.0/24, 192.168.1.0/24
PersistentKeepalive = 25
# This provides access to office resources without routing all traffic

Site-to-Site VPN

# Connect two office networks: 10.0.1.0/24 and 10.0.2.0/24
# Site A (HQ):    10.0.1.0/24, WireGuard IP 10.0.0.1
# Site B (Branch): 10.0.2.0/24, WireGuard IP 10.0.0.2
 
# Site A wg0.conf
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <site-a-private>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
 
[Peer]
# Site B
PublicKey = <site-b-public>
AllowedIPs = 10.0.0.2/32, 10.0.2.0/24
Endpoint = branch.example.com:51820
 
# Site B wg0.conf
[Interface]
Address = 10.0.0.2/24
ListenPort = 51820
PrivateKey = <site-b-private>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
 
[Peer]
# Site A
PublicKey = <site-a-public>
AllowedIPs = 10.0.0.1/32, 10.0.1.0/24
Endpoint = hq.example.com:51820
 
# On both sites: enable IP forwarding and add routes
sysctl -w net.ipv4.ip_forward=1
ip route add 10.0.2.0/24 via 10.0.0.2   # On Site A routers
ip route add 10.0.1.0/24 via 10.0.0.1   # On Site B routers

Kill Switch Configuration

# Prevent traffic leaks if WireGuard disconnects
# Option 1: iptables kill switch (client config)
 
[Interface]
PrivateKey = <client-private>
Address = 10.0.0.2/24
DNS = 1.1.1.1
 
PostUp = iptables -I OUTPUT ! -o wg0 -m mark ! --mark $(wg show wg0 fwmark) -m state --state NEW -j REJECT
PostUp = iptables -I INPUT -i wg0 -j ACCEPT
PostUp = iptables -I OUTPUT -o wg0 -j ACCEPT
 
PostDown = iptables -D OUTPUT ! -o wg0 -m mark ! --mark $(wg show wg0 fwmark) -m state --state NEW -j REJECT
PostDown = iptables -D INPUT -i wg0 -j ACCEPT
PostDown = iptables -D OUTPUT -o wg0 -j ACCEPT
 
[Peer]
PublicKey = <server-public>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
 
# Option 2: nftables kill switch
table inet killswitch {
  chain preraw {
    type filter hook prerouting priority -300;
    policy accept;
    iif != wg0 udp dport 51820 ct state new notrack accept
    iif != wg0 pkttype host accept
    iif != wg0 meta mark 0x00000f09 accept
  }
  chain output {
    type filter hook output priority 0; policy drop;
    oif wg0 accept
    oif lo accept
    udp dport 51820 accept
    meta mark 0x00000f09 accept
  }
}
 
# Option 3: Android/iOS WireGuard app has built-in kill switch
# Settings > WireGuard > "Block untunneled traffic" (Android)

NAT Bypass with Persistent Keepalive

# Client behind carrier-grade NAT (CGNAT) or double NAT
[Peer]
PublicKey = <server-public>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
# Keeps NAT mapping alive on client side
 
# Server sees connection from client's NAT public IP
# Client sees server at its public IP
# If keepalive stops, server may not be able to reach client
 
# For symmetric NATs where bidirectional initiation needed:
# Both sides can have PersistentKeepalive
# Or use a third party for UDP hole punching
 
# See current NAT type
# apt install stun-client
stun stun.l.google.com 19302