A modern, high-performance VPN protocol built into the Linux kernel that uses state-of-the-art cryptography with a minimal codebase.
Addresses below are RFC 5737 documentation ranges or placeholders - swap in your own.
Table of Contents#
- Overview
- Key Features
- Components
- Installation
- Basic Configuration
- Advanced Configuration
- Key Rotation
- Firewall Integration
- Censorship Bypass
- Logging and Debugging
- Troubleshooting
- See Also
- Sources
1. Overview#
WireGuard is a VPN protocol that blends high-speed cryptography with simplicity, aiming for better performance and ease of use compared to traditional VPN solutions like IPsec and OpenVPN. It operates as a kernel module (built into Linux since kernel 5.6) and establishes encrypted point-to-point tunnels using modern cryptographic primitives.
The entire codebase is roughly 4,000 lines of code, making it feasible to audit. It uses Curve25519 for key exchange, ChaCha20 for symmetric encryption, Poly1305 for authentication, BLAKE2s for hashing, and SipHash24 for hashtable keys.
2. Key Features#
- Efficiency and simplicity - lightweight codebase (~4,000 lines) that facilitates security auditing
- Speed - kernel-level implementation with state-of-the-art cryptographic algorithms
- Cross-platform support - available for Linux, Windows, macOS, FreeBSD, OpenBSD, iOS, and Android
- Strong security - mandatory modern cryptography with no cipher negotiation (no attack surface from legacy algorithms)
- Cryptokey routing - each peer is identified by its public key, and allowed IPs act as both ACL and routing table
- Stealth - sends no response to unauthenticated packets, making the interface invisible to port scanners
- Roaming - peers can change IP addresses transparently; the tunnel updates automatically
3. Components#
3.1. Keys and Cryptography#
- Private key - a 256-bit Curve25519 secret key used exclusively for decrypting received data
- Public key - derived from the private key; shared with peers so they can encrypt data destined for this interface
- Preshared key (optional) - a symmetric key shared between two peers that adds a post-quantum resistance layer on top of the standard Curve25519 exchange
3.2. Interface Configuration#
- Address - the VPN IP address(es) assigned to the local WireGuard interface
- ListenPort - the UDP port on which the interface awaits incoming connections (default: 51820)
- PersistentKeepalive - interval (in seconds) for sending keepalive packets to maintain NAT mappings
3.3. Peer Configuration#
- PublicKey - the remote peer's public key
- Endpoint - the remote peer's IP address and port (optional for peers that initiate connections)
- AllowedIPs - IP ranges that are permitted through this peer; doubles as a routing directive
- PresharedKey - optional symmetric key for additional encryption layer
4. Installation#
# Arch Linux
sudo pacman -S wireguard-tools
# Ubuntu/Debian
sudo apt update && sudo apt install wireguard
# CentOS/Fedora
sudo dnf install wireguard-tools
# FreeBSD
pkg install wireguard-toolsWireGuard is built into the Linux kernel since 5.6. The
wireguard-toolspackage provides thewgandwg-quickuserspace utilities. On older kernels, install thewireguard-dkmspackage as well.
5. Basic Configuration#
5.1. Key Generation#
Generate a private and public key pair:
wg genkey | tee privatekey | wg pubkey > publickeyGenerate a preshared key for an additional symmetric encryption layer:
wg genpsk > presharedkeySet proper permissions on key files:
chmod 600 privatekey presharedkey5.2. Server Configuration#
Create /etc/wireguard/wg0.conf:
[Interface]
Address = 192.0.2.1/24
PrivateKey = <server-private-key>
ListenPort = 51820
[Peer]
PublicKey = <client-public-key>
PresharedKey = <preshared-key>
AllowedIPs = 192.0.2.2/32For dual-stack (IPv4 + IPv6), specify both addresses. Use a /64 prefix for the IPv6 subnet to follow standard addressing conventions:
[Interface]
Address = 192.0.2.1/24, fd00:wg::1/64
PrivateKey = <server-private-key>
ListenPort = 51820IPv6 recommendation: Use a /64 from the
fd00::/8Unique Local Address range for private WireGuard networks. If your ISP provides a public /48 or /56, delegate a /64 to the tunnel. Avoid subnets smaller than /64, as some features (SLAAC, neighbor discovery) expect a /64 boundary.
5.3. Client Configuration#
[Interface]
Address = 192.0.2.2/24, fd00:wg::2/64
PrivateKey = <client-private-key>
DNS = 192.0.2.1
[Peer]
PublicKey = <server-public-key>
PresharedKey = <preshared-key>
Endpoint = <server-public-ip>:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25AllowedIPs = 0.0.0.0/0, ::/0routes all traffic through the tunnel (full tunnel)AllowedIPs = 192.0.2.0/24routes only the VPN subnet (split tunnel)PersistentKeepalive = 25keeps NAT mappings alive for peers behind firewalls
5.4. Starting and Stopping#
Using wg-quick:
sudo wg-quick up wg0
sudo wg-quick down wg0Using systemd (recommended for servers):
sudo systemctl start wg-quick@wg0
sudo systemctl stop wg-quick@wg0
sudo systemctl enable wg-quick@wg06. Advanced Configuration#
6.1. Masquerading (NAT for Internet Access)#
To enable masquerading so VPN clients can reach the internet through the server, choose one approach based on your firewall backend:
iptables (PostUp/PostDown):
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o ens2 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o ens2 -j MASQUERADEnftables (PostUp/PostDown):
PostUp = nft add table ip nat; nft add chain ip nat postrouting '{ type nat hook postrouting priority srcnat; }'; nft add rule ip nat postrouting oifname "%i" masquerade
PostDown = nft delete table ip natfirewalld (no PostUp needed, persistent):
sudo firewall-cmd --zone=public --add-interface=wg0 --permanent
sudo firewall-cmd --zone=public --add-masquerade --permanent
sudo firewall-cmd --reloadufw (edit before.rules):
# Add to /etc/ufw/before.rules before *filter:
*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 192.0.2.0/24 -o ens2 -j MASQUERADE
COMMIT# /etc/default/ufw - change:
DEFAULT_FORWARD_POLICY="ACCEPT"sudo ufw reload6.2. Persistent Firewall Rules (Recommended over PostUp/PostDown)#
PostUp/PostDown rules are removed when the interface goes down. For persistent rules that survive interface cycling, configure your firewall backend directly.
nftables:
# /etc/nftables.d/wireguard.nft
table inet wg_nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname "ens2" ip saddr 192.0.2.0/24 masquerade
}
chain forward {
type filter hook forward priority 0; policy accept;
iifname "wg0" accept
oifname "wg0" ct state established,related accept
}
}sudo nft -f /etc/nftables.d/wireguard.nft
# Add include to /etc/nftables.conf for boot persistenceiptables:
sudo iptables -A FORWARD -i wg0 -j ACCEPT
sudo iptables -A FORWARD -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
sudo iptables -t nat -A POSTROUTING -s 192.0.2.0/24 -o ens2 -j MASQUERADE
# Persist (Arch)
iptables-save | sudo tee /etc/iptables/iptables.rules && sudo systemctl enable iptables
# Persist (Debian)
sudo netfilter-persistent savefirewalld and ufw rules from Section 6.1 are already persistent.
6.3. Multiple Peers#
Add additional [Peer] blocks to the same configuration file:
[Peer]
# Laptop
PublicKey = <laptop-public-key>
AllowedIPs = 192.0.2.2/32
[Peer]
# Phone
PublicKey = <phone-public-key>
AllowedIPs = 192.0.2.3/32
[Peer]
# Site-to-site gateway
PublicKey = <gateway-public-key>
AllowedIPs = 192.0.2.4/32, 198.51.100.0/24
PersistentKeepalive = 256.4. DNS Configuration#
For full-tunnel configurations, set a DNS server on the client:
[Interface]
DNS = 192.0.2.1
# Or use public resolvers:
# DNS = 1.1.1.1, 2606:4700:4700::1111wg-quick sets the DNS using resolvconf or systemd-resolved depending on the system. If using systemd-resolved:
# Check current DNS routing
resolvectl status wg07. Key Rotation#
WireGuard does not have built-in key rotation. You must rotate keys manually or with automation.
7.1. Manual Key Rotation Procedure#
On the peer being rotated:
# Generate new keys
wg genkey | tee /etc/wireguard/new_privatekey | wg pubkey > /etc/wireguard/new_publickey
# Update the local config with the new private key
# Edit /etc/wireguard/wg0.conf: replace PrivateKey value
# Distribute the new public key to all peers that reference this peer
# On each remote peer, update the PublicKey value in the [Peer] section
# Restart the interface
sudo systemctl restart wg-quick@wg07.2. Live Key Rotation (No Downtime)#
Use the wg set command for hot-swapping without restarting the interface:
# On the server, update the peer's public key
sudo wg set wg0 peer <old-public-key> remove
sudo wg set wg0 peer <new-public-key> allowed-ips 192.0.2.2/32
# On the peer, update the private key
sudo wg set wg0 private-key /etc/wireguard/new_privatekeyAfter live rotation, update the configuration files to match. Otherwise, the next
wg-quick down/upcycle will revert to the old keys. AvoidSaveConfig = truein production, as it writes runtime state back to the config file and can cause unexpected changes.
7.3. Preshared Key Rotation#
# Generate a new preshared key
wg genpsk > /etc/wireguard/new_psk
# Update both sides simultaneously (brief interruption expected)
# On server:
sudo wg set wg0 peer <client-pubkey> preshared-key /etc/wireguard/new_psk
# On client:
sudo wg set wg0 peer <server-pubkey> preshared-key /etc/wireguard/new_psk7.4. Rotation Schedule Recommendations#
| Key type | Recommended interval | Notes |
|---|---|---|
| Private/public keypair | Every 90 days | Rotate more frequently for high-security environments |
| Preshared key | Every 30 days | Both peers must update simultaneously |
| Server keypair | Every 6-12 months | Requires updating all client configs |
8. Firewall Integration#
8.1. Required Firewall Rules#
At minimum, allow the WireGuard listen port and forwarding:
# Allow WireGuard UDP port
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT
# Allow forwarding between WireGuard and the physical interface
sudo iptables -A FORWARD -i wg0 -o ens2 -j ACCEPT
sudo iptables -A FORWARD -i ens2 -o wg0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT8.2. Restricting Access Between Peers#
By default, WireGuard allows all traffic between connected peers. To restrict peer-to-peer communication:
table inet wg_filter {
chain forward {
type filter hook forward priority 0; policy drop;
# Allow peers to reach the server subnet only
iifname "wg0" oifname "ens2" accept
iifname "ens2" oifname "wg0" ct state established,related accept
# Block peer-to-peer (wg0 -> wg0 is implicitly dropped)
}
}9. Censorship Bypass#
In censored environments where WireGuard's UDP traffic is blocked, route it through a VLESS or WebSocket tunnel. See the Censorship Bypass Guide Section 9 for:
- Direct stacking (sing-box TUN + WireGuard)
- OPNsense gateway routing
- wstunnel (WireGuard UDP over WebSocket)
Key considerations:
- Set
PersistentKeepalive = 45(not 25) to reduce traffic pattern fingerprinting - Use specific
AllowedIPs(not0.0.0.0/0) to avoid routing conflicts with the outer tunnel - WireGuard's UDP handshake survives tunnel restarts automatically
10. Logging and Debugging#
10.1. Enable Kernel Debug Logging#
WireGuard runs in the kernel and uses dynamic debug for logging:
# Enable verbose WireGuard logging
echo 'module wireguard +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# View logs
sudo dmesg -wT | grep wireguard
# Or with journalctl
sudo journalctl -k --grep=wireguard -fDisable debug logging when finished, as it generates significant output:
echo 'module wireguard -p' | sudo tee /sys/kernel/debug/dynamic_debug/control
10.2. Interface Status#
# Show interface status, peers, transfer stats, and last handshake
sudo wg show
sudo wg show wg0
# Show only specific fields
sudo wg show wg0 latest-handshakes
sudo wg show wg0 transfer
sudo wg show wg0 endpoints10.3. Packet Capture#
# Capture on the WireGuard interface (decrypted traffic)
sudo tcpdump -i wg0 -n
# Capture on the physical interface (encrypted UDP packets)
sudo tcpdump -i ens2 -n udp port 5182010.4. Common Debug Checks#
# Verify the kernel module is loaded
lsmod | grep wireguard
# Check if IP forwarding is enabled
sysctl net.ipv4.ip_forward
sysctl net.ipv6.conf.all.forwarding
# Enable IP forwarding (temporary)
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
# Enable IP forwarding (persistent)
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 --systemTroubleshooting#
| Issue | Cause | Solution |
|---|---|---|
| No handshake occurs | Firewall blocking UDP port, wrong endpoint, or key mismatch | Check sudo wg show for latest-handshakes; verify firewall allows the ListenPort; confirm public keys match |
| Handshake succeeds but no traffic | AllowedIPs misconfigured or IP forwarding disabled | Verify AllowedIPs on both sides cover the desired ranges; enable net.ipv4.ip_forward=1 |
| Connection drops after minutes | NAT timeout evicting the UDP mapping | Set PersistentKeepalive = 25 on the peer behind NAT |
| DNS resolution fails on client | DNS not set in the Interface section, or resolver not reachable via tunnel | Add DNS = <server-ip> to the client config; verify the DNS server is accessible through the tunnel |
| "RTNETLINK answers: Operation not permitted" | Missing CAP_NET_ADMIN or wireguard module not loaded | Run with sudo; verify lsmod | grep wireguard shows the module |
| Slow throughput | MTU issues causing fragmentation | Set MTU = 1420 (or lower) in the Interface section; test with ping -M do -s 1392 <peer-ip> |
| Peers cannot reach each other (only server) | Missing forwarding rules between wg0 interfaces | Add iptables/nftables FORWARD rules allowing wg0-to-wg0 traffic |
| "Key is not the correct length" | Corrupted or improperly copied key | Regenerate the key pair; ensure no trailing whitespace or newlines in the config |