Introduction
WireGuard has revolutionized VPN deployments with its streamlined codebase (~4,000 lines), kernel-level performance, and modern cryptography. Unlike legacy protocols like OpenVPN or IPsec, WireGuard operates as a first-class Linux kernel module (since 5.6) and uses a single opinionated cryptographic suite — ChaCha20-Poly1305 for encryption, Curve25519 for key exchange, BLAKE2s for hashing, and SipHash24 for hashtable keys.
This guide covers everything you need to set up a WireGuard VPN server, configure clients across platforms, manage peers, implement split tunneling, and troubleshoot common issues. For deeper dives into specific topics, see the companion articles on WireGuard performance tuning, advanced use cases, and WireGuard 2.0 architecture.
How WireGuard Works
WireGuard is built on two key design concepts: Cryptokey Routing and Built-in Roaming.
Cryptokey Routing
At the heart of WireGuard is the association of public keys with a list of allowed IP addresses. Each interface has a private key and a list of peers. Each peer has a public key. When sending packets, the AllowedIPs list acts as a routing table — the kernel selects the peer whose AllowedIPs matches the packet destination. When receiving packets, AllowedIPs acts as an access control list — only source IPs matching the list are accepted after decryption.
[Interface] wg0
PrivateKey = <server_private>
┌────────────────────────────────────────────┐
│ [Peer] PublicKey = <client1> │
│ AllowedIPs = 10.0.0.2/32 │
├────────────────────────────────────────────┤
│ [Peer] PublicKey = <client2> │
│ AllowedIPs = 10.0.0.3/32, 10.0.1.0/24 │
└────────────────────────────────────────────┘
This means no separate authentication protocol, no certificate authorities, no complex PKI — just a list of peers, their public keys, and the IP ranges they are allowed to use. This simplicity is why WireGuard’s attack surface is so small.
Built-in Roaming
WireGuard clients contain an initial endpoint (server IP:port) so they know where to send encrypted data before receiving any. Servers have no initial endpoints for peers — they discover each peer’s endpoint by examining where correctly authenticated data originates. When a client changes networks (Wi-Fi to cellular, for example), it sends the next packet from its new IP, and the server automatically updates the endpoint. There is no re-handshake, no connection drop, and no configuration change needed.
Prerequisites
- A Linux server (Ubuntu 22.04+, Debian 12+, or any distro with kernel 5.6+) with a public IP address
- Root or
sudoprivileges - UDP port
51820open on the server’s firewall wireguard-toolspackage for thewgandwg-quickcommands- Basic familiarity with Linux networking (ip, iptables, systemctl)
Installation Across Platforms
macOS
# Via Homebrew
brew install wireguard-tools
# Or via App Store: WireGuard
Windows
# Via winget
winget install WireGuard.WireGuard
# Or download from https://www.wireguard.com/install/
Server Setup
Step 1: Install WireGuard
Update the system and install the WireGuard package. On modern kernels (5.6+), the module ships with the kernel — the package only installs the userspace tools.
sudo apt update
sudo apt install wireguard -y
Verify the installation:
wg --version
# wireguard-tools v1.0.20230427
Step 2: Enable IP Forwarding
For the server to route traffic between the VPN tunnel and the internet, enable IP forwarding in the kernel.
sudo sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
sudo sed -i 's/#net.ipv6.conf.all.forwarding=1/net.ipv6.conf.all.forwarding=1/' /etc/sysctl.conf
sudo sysctl -p
Step 3: Generate Server Keys
WireGuard uses public-key cryptography for peer authentication. Generate a private key and derive the public key.
cd /etc/wireguard
umask 077
wg genkey | tee server_private.key | wg pubkey > server_public.key
The umask 077 ensures the private key file is only readable by root. Keep this key secret — anyone with access to it can impersonate your server.
Step 4: Create the Server Configuration
Create /etc/wireguard/wg0.conf. Replace eth0 with the server’s internet-facing interface (check with ip route show default).
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <contents of server_private.key>
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
PostUp and PostDown run when the interface comes up or down. The MASQUERADE rule rewrites the source IP of outbound packets so replies return through the server — this is what enables clients to reach the internet through the VPN.
Step 5: Start the Server
Enable the service to start on boot and start it immediately.
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
Check the status:
sudo wg show
# interface: wg0
# public key: <server_public_key>
# private key: (hidden)
# listening port: 51820
The server is now listening on UDP port 51820, waiting for client connections.
Client Setup
Step 1: Generate Client Keys
On the server (or a secure machine), generate a key pair for each client.
wg genkey | tee client1_private.key | wg pubkey > client1_public.key
Step 2: Add the Client as a Server Peer
Append a [Peer] section to the server’s /etc/wireguard/wg0.conf.
[Peer]
PublicKey = <client1_public_key>
AllowedIPs = 10.0.0.2/32
The AllowedIPs value 10.0.0.2/32 assigns this client the VPN IP 10.0.0.2. Each client must have a unique IP within the tunnel subnet (10.0.0.0/24 in this example).
Reload the configuration without disconnecting existing peers:
sudo wg addconf wg0 <(wg-quick strip wg0)
Or restart the interface:
sudo systemctl restart wg-quick@wg0
Step 3: Create the Client Configuration
Create a configuration file for the client device. This file contains the client’s private key, its VPN IP, and the server’s public key and endpoint.
[Interface]
Address = 10.0.0.2/24
PrivateKey = <client1_private_key>
DNS = 1.1.1.1, 8.8.8.8
[Peer]
PublicKey = <server_public_key>
Endpoint = <SERVER_PUBLIC_IP>:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
Key fields explained:
| Field | Purpose |
|---|---|
Address |
The VPN IP assigned to this client |
PrivateKey |
The client’s private key (never shared) |
DNS |
DNS servers pushed to the client when the tunnel is active |
PublicKey |
The server’s public key (authenticates the server) |
Endpoint |
The server’s public IP and port (where to connect) |
AllowedIPs = 0.0.0.0/0, ::/0 |
Route all traffic through the VPN (full tunnel). Change to specific subnets for split tunneling |
PersistentKeepalive = 25 |
Send a keepalive every 25 seconds to maintain NAT/firewall mappings |
Step 4: Start the Client
On Linux, use wg-quick:
sudo wg-quick up ./wg0-client1.conf
Verify the connection:
sudo wg
# interface: wg0
# public key: <client1_public_key>
# private key: (hidden)
# listening port: 21841
#
# peer: <server_public_key>
# endpoint: <SERVER_PUBLIC_IP>:51820
# allowed ips: 0.0.0.0/0, ::/0
# latest handshake: 2 seconds ago
# transfer: 1.23 KiB received, 456 B sent
A recent latest handshake confirms the tunnel is established.
On Windows, macOS, iOS, and Android, download the official WireGuard app and import this configuration file. Mobile apps also support QR code scanning (see below).
What wg-quick Does Internally
When you run wg-quick up wg0, it performs these steps behind the scenes:
- Reads the config file at
/etc/wireguard/wg0.conf - Creates a network interface named
wg0(ip link add wg0 type wireguard) - Assigns the private key and configures peers (
wg setconf wg0 <(wg-quick strip wg0)) - Assigns the IP address (
ip addr add 10.0.0.1/24 dev wg0) - Brings the interface up (
ip link set wg0 up) - Adds routes based on
AllowedIPs - Configures DNS via
resolvconforsystemd-resolved - Runs
PostUpcommands - Resolves peer endpoints (DNS names in
Endpointare resolved at this point)
Understanding this helps when debugging — if wg-quick fails, you can run these steps manually to pinpoint the issue.
QR Code Generation for Mobile Clients
Mobile WireGuard apps can import configurations by scanning a QR code. This is the fastest way to deploy to phones and tablets.
# Install qrencode if not already installed
sudo apt install qrencode -y
# Generate a QR code from the client config file
qrencode -t ansiutf8 < wg0-client1.conf
Scan the resulting ANSI QR code with the WireGuard mobile app. The app automatically parses the config and creates the tunnel.
Adding More Clients
To add a second client, repeat the client setup steps with a unique IP and key pair.
On the server, the config grows by one [Peer] block:
[Peer]
PublicKey = <client1_public_key>
AllowedIPs = 10.0.0.2/32
[Peer]
PublicKey = <client2_public_key>
AllowedIPs = 10.0.0.3/32
Client 2’s configuration is identical to client 1’s, except for its private key, IP address, and a different public key on the server.
For a full tunnel (all traffic through VPN), use AllowedIPs = 0.0.0.0/0, ::/0 in the client config. For split tunneling (only specific subnets), set AllowedIPs to only the subnets that should go through the tunnel — this is covered in detail in the WireGuard use cases article.
Dynamic Configuration Without Restart
Adding or removing peers normally requires restarting wg-quick, which drops existing connections. WireGuard supports dynamic reconfiguration using wg syncconf, which applies changes without disconnecting active peers.
# Strip the config file to canonical format and sync
sudo wg syncconf wg0 <(wg-quick strip wg0)
The wg syncconf command compares the new configuration with the current state and only applies additions, removals, and modifications. Existing peers with unchanged keys maintain their established sessions.
To add a single peer without editing the config file:
sudo wg set wg0 peer <CLIENT_PUBLIC_KEY> allowed-ips 10.0.0.4/32
To remove a peer:
sudo wg set wg0 peer <CLIENT_PUBLIC_KEY> remove
These commands take effect immediately. However, they are ephemeral — they do not modify the config file. To make changes permanent, edit /etc/wireguard/wg0.conf and run wg syncconf.
Split Tunneling with DNS
Split tunneling routes only specific traffic through the VPN while the rest uses the client’s normal internet connection. This is useful when you only need to access resources on the remote network.
Set AllowedIPs to only the VPN subnet and the target network:
[Peer]
PublicKey = <server_public_key>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.0.0.0/24, 192.168.1.0/24
With split tunneling, DNS becomes a challenge. Internal DNS names should resolve through the VPN, but public DNS should use the normal resolver. On Linux with systemd-resolved, use PostUp:
[Interface]
Address = 10.0.0.2/24
PrivateKey = <client1_private_key>
PostUp = resolvectl dns wg0 10.0.0.1; resolvectl domain wg0 ~internal.company.com
PostDown = resolvectl revert wg0
The ~ prefix on the domain tells systemd-resolved to query the VPN DNS only for names ending in internal.company.com. All other lookups go to the default resolver.
WireGuard with Docker (wg-easy)
wg-easy packages WireGuard with a web UI for peer management, config download, and QR code generation. This is the fastest way to deploy WireGuard without editing config files.
# docker-compose.yml
services:
wg-easy:
image: ghcr.io/wg-easy/wg-easy:15
container_name: wg-easy
environment:
- WG_HOST=vpn.example.com
- PASSWORD_HASH=$$2y$$10$$... # bcrypt hash — generate with `docker run --rm -it ghcr.io/wg-easy/wg-easy wgpw 'your-password'`
- WG_DEFAULT_DNS=1.1.1.1
- WG_DEFAULT_ADDRESS=10.8.0.x
- WG_ALLOWED_IPS=10.8.0.0/24, 192.168.1.0/24
volumes:
- wg-easy-data:/etc/wireguard
ports:
- "51820:51820/udp"
- "51821:51821/tcp"
cap_add:
- NET_ADMIN
- SYS_MODULE
sysctls:
- net.ipv4.ip_forward=1
restart: unless-stopped
volumes:
wg-easy-data:
docker compose up -d
The web UI is available at http://your-server:51821. From there, create clients, download config files, and generate QR codes — all without touching the command line.
WireGuard with systemd-networkd
For systems already managed by systemd-networkd, native WireGuard support avoids the need for wg-quick. The configuration is split into two files: a .netdev file for the WireGuard device, and a .network file for addressing and routing.
# /etc/systemd/network/50-wg0.netdev
[NetDev]
Name=wg0
Kind=wireguard
Description=WireGuard VPN Server
[WireGuard]
ListenPort=51820
PrivateKeyFile=/etc/wireguard/server_private.key
[WireGuardPeer]
PublicKey=<client1_public_key>
AllowedIPs=10.0.0.2/32
[WireGuardPeer]
PublicKey=<client2_public_key>
AllowedIPs=10.0.0.3/32
# /etc/systemd/network/50-wg0.network
[Match]
Name=wg0
[Network]
Address=10.0.0.1/24
IPForward=yes
# Set permissions and apply
chmod 640 /etc/systemd/network/50-wg0.netdev
chown root:systemd-network /etc/systemd/network/50-wg0.netdev
sudo systemctl restart systemd-networkd
This approach is declarative, survives reboots, and integrates natively with systemd’s network management. The IPForward=yes option replaces the manual sysctl setting for this interface.
Preshared Keys (Post-Quantum Mitigation)
Adding a preshared key provides an additional layer of symmetric encryption, making WireGuard resistant to potential future quantum computing attacks on Curve25519. The preshared key is mixed into the key exchange using a Diffie-Hellman-like construction, providing a symmetric fallback if elliptic-curve cryptography is ever broken.
# Generate a preshared key
wg genpsk > preshared.key
[Peer]
PublicKey = <client-public-key>
PresharedKey = <base64-encoded-preshared-key>
AllowedIPs = 10.0.0.2/32
Use preshared keys for any peer that handles sensitive data. The performance overhead is negligible, and the security benefit is significant for long-lived deployments.
Firewall Configuration
Proper firewall rules are essential for WireGuard to function correctly, especially when clients need internet access through the tunnel.
UFW (Ubuntu)
# Allow WireGuard UDP port
sudo ufw allow 51820/udp
# Enable IP forwarding in UFW
sudo nano /etc/ufw/sysctl.conf
# Uncomment: net/ipv4/ip_forward=1
sudo ufw reload
firewalld (CentOS/RHEL)
# Create a dedicated WireGuard zone
sudo firewall-cmd --permanent --new-zone=wireguard
sudo firewall-cmd --permanent --zone=wireguard --add-service=wireguard
sudo firewall-cmd --reload
For firewalld, also ensure masquerading is enabled on the external zone if clients route internet traffic through the VPN:
sudo firewall-cmd --permanent --zone=public --add-masquerade
sudo firewall-cmd --reload
Performance Tuning
WireGuard already delivers excellent performance out of the box, but several tuning options can help in specific scenarios.
Kernel Network Buffers
Adjusting kernel buffer sizes improves throughput for high-latency links. Add these to /etc/sysctl.conf:
net.core.rmem_max = 2500000
net.core.wmem_max = 2500000
net.ipv4.tcp_rmem = 4096 87380 2500000
net.ipv4.tcp_wmem = 4096 65536 2500000
sudo sysctl -p
BBR Congestion Control
BBR (Bottleneck Bandwidth and Round-trip propagation time) often outperforms CUBIC on VPN links with variable bandwidth:
sudo sysctl net.ipv4.tcp_congestion_control=bbr
sudo sysctl net.core.default_qdisc=fq
Make permanent by adding to /etc/sysctl.conf:
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
MTU Optimization
The default WireGuard MTU is 1420 bytes, but the optimal value depends on your link. Test with ping:
# Find the optimal MTU size
ping -M do -s 1472 <server-ip>
Start with -s 1472 and reduce by 8 until packets pass without fragmentation. Add 28 bytes (IP+ICMP header) to the working -s value to get the interface MTU. Set it in the config:
[Interface]
MTU = 1420
Use Cases
WireGuard adapts to a wide range of deployment scenarios beyond simple client-server setups.
Site-to-Site VPN
Connect two office networks through a persistent WireGuard tunnel. Each site acts as both server and client for its respective subnet.
# Site A configuration
[Interface]
PrivateKey = <site-a-private>
Address = 10.0.0.1/24
ListenPort = 51820
[Peer]
PublicKey = <site-b-public>
AllowedIPs = 10.0.1.0/24 # Site B network
Endpoint = site-b.example.com:51820
PersistentKeepalive = 25
# Site B configuration
[Interface]
PrivateKey = <site-b-private>
Address = 10.0.1.1/24
[Peer]
PublicKey = <site-a-public>
AllowedIPs = 10.0.0.0/24 # Site A network
Endpoint = site-a.example.com:51820
PersistentKeepalive = 25
Each site needs IP forwarding enabled and appropriate NAT/masquerade rules if the tunnel traffic must reach beyond the local subnet.
Road Warrior (Mobile Users)
A central server supporting multiple remote peers that change networks frequently — the most common WireGuard deployment pattern.
[Interface]
PrivateKey = <server-private>
Address = 10.0.0.1/24
ListenPort = 51820
# Mobile user
[Peer]
PublicKey = <mobile-public>
AllowedIPs = 10.0.0.100/32
# Laptop user
[Peer]
PublicKey = <laptop-public>
AllowedIPs = 10.0.0.101/32
Road warrior clients must include PersistentKeepalive = 25 to maintain NAT mappings on hotel or airport networks. The server automatically discovers each peer’s current endpoint from the first authenticated packet — no configuration update needed when the client roams.
Monitoring and Management
WireGuard provides real-time status information through the wg command. These commands are useful for monitoring and debugging.
# Show all interfaces and peers
sudo wg show
# Show byte counters per peer
sudo wg show wg0 transfer
# Show last handshake time per peer (useful for detecting disconnections)
sudo wg show wg0 latest-handshakes
# Dump all state in machine-parseable format
sudo wg show all dump
For automated monitoring, script the handshake check:
# Alert if any peer has not handshaken in 5 minutes
sudo wg show wg0 latest-handshakes | tail -n +2 | while read -r peer timestamp; do
age=$(($(date +%s) - timestamp))
if [ "$age" -gt 300 ]; then
echo "WARNING: Peer $peer last handshake ${age}s ago"
fi
done
For Prometheus-based monitoring, deploy the wireguard_exporter:
docker run -d -p 9586:9586 \
-v /etc/wireguard:/etc/wireguard:ro \
--cap-add NET_ADMIN \
mindflavor/prometheus_wireguard_exporter
Troubleshooting
No Handshake (Connection Not Established)
# Verify the server is listening
sudo ss -ulnp | grep 51820
# Check that the firewall allows UDP 51820
sudo iptables -L -n | grep 51820
# Test UDP connectivity from the client
nc -zvu <SERVER_PUBLIC_IP> 51820
# Verify the client's endpoint is correct
sudo wg show wg0 | grep endpoint
Common causes: firewall blocking UDP 51820, wrong server IP or port in the client config, client behind a symmetric NAT without PersistentKeepalive.
Handshake Succeeds but No Traffic Flows
# Check IP forwarding on the server
sysctl net.ipv4.ip_forward
# Must be 1
# Verify iptables MASQUERADE rule
sudo iptables -t nat -L POSTROUTING -n
# Check routes on the client
ip route show
# Ping the VPN gateway
ping -c 3 10.0.0.1
Common causes: IP forwarding disabled, missing MASQUERADE rule, AllowedIPs mismatch between client and server.
DNS Not Working
# Check DNS configuration in the WireGuard interface
resolvectl status wg0
# Test DNS manually
nslookup example.com 10.0.0.1
# Override DNS in the client config
# [Interface]
# DNS = 10.0.0.1 # Use the VPN server as DNS
Performance Issues
# Check for packet loss on the tunnel interface
ip -s link show wg0
# Verify MTU setting (try 1420)
# In [Interface]:
# MTU = 1420
# Check CPU usage during transfer
top -d 1
If ksoftirqd shows high CPU, the server is struggling with encryption. Consider reducing MTU or upgrading hardware. See the WireGuard performance guide for detailed tuning.
Port Conflicts
# Check if port 51820 is already in use
sudo ss -ulnp | grep 51820
# Change the port in both server and client configs
# Server: ListenPort = 51821
# Client: Endpoint = <SERVER_IP>:51821
Conclusion
WireGuard delivers on its promise of a simpler, faster, and more secure VPN protocol. Its minimal codebase (~4,000 lines), kernel-level performance, and modern cryptographic suite make it an excellent choice over legacy protocols like OpenVPN and IPsec for both site-to-site and remote access deployments.
Key advantages:
- Modern cryptography — ChaCha20-Poly1305, Curve25519, BLAKE2s with no negotiation or fallback
- Simple configuration — One interface, multiple peers, no certificate authorities or PKI
- Cross-platform — Native on Linux, macOS, Windows, iOS, Android, and BSD
- Built-in roaming — Endpoint discovery with no reconnection delay when clients change networks
- Kernel-native performance — Minimal CPU overhead and line-rate throughput on modern hardware
For organizations evaluating VPN solutions in 2026, WireGuard should be the default choice for new deployments. Pair it with a management layer like wg-easy or NetBird for multi-site deployments, and complement it with wireguard_exporter for observability.
Resources
- WireGuard Quick Start — Official setup guide
- WireGuard Documentation — Protocol overview, installation, and performance
- WireGuard GitHub — Source code and issue tracker
- wg-easy — Web UI for WireGuard management
- WireGuard NT v0.11 Release (April 2026) — Latest Windows client updates
Comments