Skip to main content

WireGuard VPN: Complete Setup Guide for Server and Clients

Created: March 10, 2026 Larry Qu 15 min read

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 sudo privileges
  • UDP port 51820 open on the server’s firewall
  • wireguard-tools package for the wg and wg-quick commands
  • 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:

  1. Reads the config file at /etc/wireguard/wg0.conf
  2. Creates a network interface named wg0 (ip link add wg0 type wireguard)
  3. Assigns the private key and configures peers (wg setconf wg0 <(wg-quick strip wg0))
  4. Assigns the IP address (ip addr add 10.0.0.1/24 dev wg0)
  5. Brings the interface up (ip link set wg0 up)
  6. Adds routes based on AllowedIPs
  7. Configures DNS via resolvconf or systemd-resolved
  8. Runs PostUp commands
  9. Resolves peer endpoints (DNS names in Endpoint are 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

Comments

👍 Was this article helpful?