WireGuard Tutorial: A Practical Guide to Modern VPN Setup

Learn WireGuard from scratch - concepts, configuration, common topologies, and practical examples for setting up secure VPN tunnels between Linux machines.

The OpenVPN config file was 87 lines. Cipher suites, TLS auth keys, certificate paths, compression settings, route-nopull directives. I copy-pasted it from a tutorial, changed three values, and prayed.

It didn’t work.

Four hours later—after debugging certificate chains, realizing the server wanted tls-auth but the client config had tls-crypt, and discovering that my firewall was blocking UDP 1194—I finally got a connection. The tunnel dropped every 20 minutes because of a keep-alive mismatch.

Then I discovered WireGuard. My first working config was 10 lines. It took 5 minutes. It hasn’t dropped in two years.

This is what VPN configuration should feel like.

Why WireGuard?

Simplicity: A WireGuard config file is ~10 lines. An OpenVPN config is often 50+.

Performance: WireGuard runs in the kernel, not userspace. Benchmarks show 3-4x throughput vs OpenVPN.

Security: Modern cryptography only (Curve25519, ChaCha20, Poly1305). No cipher negotiation, no legacy options.

Stealth: Silent when not in use. Doesn’t respond to unauthenticated packets.

Core Concepts

Before configuring anything, understand these five concepts:

WireGuard mental model - peers, keys, and encrypted tunnel

1. Interface

WireGuard creates a virtual network interface (like wg0). This interface has:

  • A private key
  • A listening port (optional)
  • An IP address within your VPN network
wg0: 10.0.0.1/24  ←  This machine's VPN IP

2. Peers

A peer is any other machine in your WireGuard network. Each peer has:

  • A public key (how you identify them)
  • Allowed IPs (what traffic to send to them)
  • An endpoint (optional - their public IP:port)
Peer A ←──────────────→ Peer B
       encrypted tunnel

3. Key Pairs

Each machine has a public/private key pair:

  • Private key: Secret, never shared, stays on the machine
  • Public key: Derived from private key, shared with peers

You give your public key to peers. They give you theirs. That’s how you authenticate each other.

4. AllowedIPs

This is the trickiest concept. AllowedIPs serves two purposes:

Outbound: “What destination IPs should be routed through this peer?” Inbound: “What source IPs is this peer allowed to claim?”

Example:

[Peer]
AllowedIPs = 10.0.0.2/32

This means:

  • Send traffic destined for 10.0.0.2 through this peer
  • Accept traffic from this peer only if it claims to be from 10.0.0.2

5. Endpoint

The public IP:port where a peer can be reached. Only needed for one side if the other is behind NAT.

Endpoint = 203.0.113.5:51820

If both peers are behind NAT, one must have a publicly reachable endpoint.

Installation

Debian/Ubuntu

sudo apt update
sudo apt install -y wireguard

Fedora/RHEL

sudo dnf install -y wireguard-tools

Arch

sudo pacman -S wireguard-tools

macOS

brew install wireguard-tools

Verify Installation

which wg
# /usr/bin/wg

wg --version
# wireguard-tools v1.0.20210914

Your First WireGuard Tunnel

Let’s connect two machines: Server (has public IP) and Client (behind NAT).

Point-to-point WireGuard tunnel between client and server

Step 1: Generate Keys (Both Machines)

On both the server and client:

cd /etc/wireguard
umask 077
wg genkey | tee privatekey | wg pubkey > publickey

View the keys:

cat privatekey
# aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5zA7bC9=

cat publickey
# XyZ9aB7cD5eF3gH1iJ9kL7mN5oP3qR1sT9uV7wX5yZ=

Write down both public keys — you’ll exchange them.

Step 2: Configure Server

Create /etc/wireguard/wg0.conf on the server:

[Interface]
# Server's private key
PrivateKey = <SERVER_PRIVATE_KEY>

# Server's VPN IP address
Address = 10.0.0.1/24

# Port to listen on
ListenPort = 51820

[Peer]
# Client's public key
PublicKey = <CLIENT_PUBLIC_KEY>

# Client's VPN IP (what IPs can this peer use?)
AllowedIPs = 10.0.0.2/32

Replace:

  • <SERVER_PRIVATE_KEY> — contents of server’s privatekey file
  • <CLIENT_PUBLIC_KEY> — the client’s public key

Step 3: Configure Client

Create /etc/wireguard/wg0.conf on the client:

[Interface]
# Client's private key
PrivateKey = <CLIENT_PRIVATE_KEY>

# Client's VPN IP address
Address = 10.0.0.2/24

[Peer]
# Server's public key
PublicKey = <SERVER_PUBLIC_KEY>

# Server's public IP and port
Endpoint = <SERVER_PUBLIC_IP>:51820

# Server's VPN IP (route this through the tunnel)
AllowedIPs = 10.0.0.1/32

# Keep connection alive (needed when behind NAT)
PersistentKeepalive = 25

Replace:

  • <CLIENT_PRIVATE_KEY> — contents of client’s privatekey file
  • <SERVER_PUBLIC_KEY> — the server’s public key
  • <SERVER_PUBLIC_IP> — the server’s public IP (e.g., 203.0.113.5)

Step 4: Open Firewall on Server

Allow incoming UDP on port 51820:

# UFW
sudo ufw allow 51820/udp

# firewalld
sudo firewall-cmd --permanent --add-port=51820/udp
sudo firewall-cmd --reload

# iptables
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT

Step 5: Start WireGuard

On the server first:

sudo wg-quick up wg0

Then on the client:

sudo wg-quick up wg0

Step 6: Verify Connection

Check status on either machine:

sudo wg show

Expected output (on client):

interface: wg0
  public key: XyZ9aB7cD5eF3gH1iJ9kL7mN5oP3qR1sT9uV7wX5yZ=
  private key: (hidden)
  listening port: 43210

peer: AbC1dE2fG3hI4jK5lM6nO7pQ8rS9tU0vW1xY2zA3bC=
  endpoint: 203.0.113.5:51820
  allowed ips: 10.0.0.1/32
  latest handshake: 5 seconds ago    ← This confirms it's working
  transfer: 1.24 KiB received, 928 B sent
  persistent keepalive: every 25 seconds

“latest handshake” confirms the tunnel is established.

Test connectivity:

# From client
ping -c 4 10.0.0.1

# From server
ping -c 4 10.0.0.2

Enable on Boot

Make WireGuard start automatically:

sudo systemctl enable wg-quick@wg0

Now wg0 will come up after every reboot.

Common Topologies

Point-to-Point (Two Machines)

What we just set up. Two machines connected directly.

Machine A ←────────→ Machine B

Use case: Accessing a home server from a VPS.

Hub and Spoke (Star Topology)

One central server, multiple clients. Clients connect to the hub, not to each other.

        ┌──── Client 1

Hub ────┼──── Client 2

        └──── Client 3

Hub config has multiple [Peer] sections:

[Interface]
PrivateKey = <HUB_PRIVATE>
Address = 10.0.0.1/24
ListenPort = 51820

[Peer]
PublicKey = <CLIENT1_PUBLIC>
AllowedIPs = 10.0.0.2/32

[Peer]
PublicKey = <CLIENT2_PUBLIC>
AllowedIPs = 10.0.0.3/32

[Peer]
PublicKey = <CLIENT3_PUBLIC>
AllowedIPs = 10.0.0.4/32

Each client only needs the hub as a peer.

Use case: Remote access for a team to a central server.

Full Mesh

Every machine connects to every other machine. Complex but allows direct communication between any pair.

    A ──── B
    │╲    ╱│
    │ ╲  ╱ │
    │  ╲╱  │
    │  ╱╲  │
    │ ╱  ╲ │
    │╱    ╲│
    C ──── D

Each machine has N-1 peer sections (where N is total machines).

Use case: Small team needing peer-to-peer access.

Site-to-Site

Connect two networks (not just two machines). Requires IP forwarding and routing.

Network A                           Network B
192.168.1.0/24                      192.168.2.0/24
     │                                   │
     │                                   │
  Gateway A ◄───── WireGuard ─────► Gateway B
  10.0.0.1                          10.0.0.2

Gateway A config:

[Interface]
PrivateKey = <A_PRIVATE>
Address = 10.0.0.1/24
ListenPort = 51820

# Enable IP forwarding
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
PublicKey = <B_PUBLIC>
Endpoint = <B_PUBLIC_IP>:51820
AllowedIPs = 10.0.0.2/32, 192.168.2.0/24  # Include remote network

The key is AllowedIPs including the remote network’s CIDR.

Routing All Traffic Through WireGuard

To route all internet traffic through the tunnel (like a traditional VPN):

Client config:

[Peer]
PublicKey = <SERVER_PUBLIC>
Endpoint = <SERVER_IP>:51820
AllowedIPs = 0.0.0.0/0, ::/0    # Route everything
PersistentKeepalive = 25

Server needs NAT enabled:

[Interface]
PrivateKey = <SERVER_PRIVATE>
Address = 10.0.0.1/24
ListenPort = 51820

PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
PublicKey = <CLIENT_PUBLIC>
AllowedIPs = 10.0.0.2/32

Note: Replace eth0 with your server’s actual internet-facing interface.

DNS Configuration

When routing all traffic, you also want DNS to go through the tunnel:

[Interface]
PrivateKey = <PRIVATE_KEY>
Address = 10.0.0.2/24
DNS = 1.1.1.1, 8.8.8.8    # Or your preferred DNS

wg-quick will configure the system resolver automatically.

Adding and Removing Peers

Add a Peer Without Restarting

# Generate keys for new peer
wg genkey | tee newpeer_private | wg pubkey > newpeer_public

# Add peer to running interface
sudo wg set wg0 peer $(cat newpeer_public) allowed-ips 10.0.0.5/32

Remove a Peer

sudo wg set wg0 peer <PEER_PUBLIC_KEY> remove

Save Running Config to File

sudo wg-quick save wg0

This writes the current running config (including dynamically added peers) to /etc/wireguard/wg0.conf.

Debugging

Check Interface Status

sudo wg show

Look for:

  • latest handshake — recent timestamp means connection is alive
  • transfer — bytes sent/received
  • endpoint — where the peer is connecting from

No Handshake?

Possible causes:

  1. Firewall blocking UDP 51820 — check both sides
  2. Wrong endpoint — verify the public IP
  3. Key mismatch — double-check public keys are exchanged correctly
  4. NAT without keepalive — add PersistentKeepalive = 25

Handshake Works, But No Ping?

  1. AllowedIPs misconfigured — must include the peer’s VPN IP
  2. Firewall blocking ICMP — try curl instead of ping
  3. Routing issue — check ip route on both sides

View Interface Details

ip addr show wg0
ip route | grep wg0

Debug Logging

For kernel-level debugging:

echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control
dmesg -w | grep wireguard

Turn off when done:

echo module wireguard -p > /sys/kernel/debug/dynamic_debug/control

Security Best Practices

Protect Private Keys

chmod 600 /etc/wireguard/wg0.conf
chmod 600 /etc/wireguard/privatekey

Only root should read these files.

Use Unique Keys Per Peer

Don’t reuse keys across machines. Generate fresh key pairs for each device.

Restrict AllowedIPs

Only allow the IPs the peer actually needs:

# Good: specific IP
AllowedIPs = 10.0.0.2/32

# Risky: entire subnet
AllowedIPs = 10.0.0.0/24

# Dangerous: route everything
AllowedIPs = 0.0.0.0/0

Rotate Keys Periodically

WireGuard keys don’t expire. Rotate them annually or when a device is compromised:

  1. Generate new key pair
  2. Update configs on both sides
  3. Restart WireGuard

Preshared Keys (Extra Security)

For post-quantum resistance, add a preshared key:

wg genpsk > preshared.key

Add to both peer configs:

[Peer]
PublicKey = <PEER_PUBLIC>
PresharedKey = <PRESHARED_KEY>
AllowedIPs = 10.0.0.2/32

Both sides must have the same preshared key.

Quick Reference

Commands

ActionCommand
Start interfacesudo wg-quick up wg0
Stop interfacesudo wg-quick down wg0
Show statussudo wg show
Show configsudo wg showconf wg0
Generate private keywg genkey
Derive public keywg pubkey < privatekey
Generate preshared keywg genpsk
Add peer dynamicallysudo wg set wg0 peer <PUB> allowed-ips <IP>
Remove peersudo wg set wg0 peer <PUB> remove
Enable on bootsudo systemctl enable wg-quick@wg0

Config File Structure

[Interface]
PrivateKey = <this machine's private key>
Address = <this machine's VPN IP>/24
ListenPort = 51820                    # optional, for servers
DNS = 1.1.1.1                         # optional, for clients
PostUp = <command to run after up>    # optional
PostDown = <command to run after down> # optional

[Peer]
PublicKey = <peer's public key>
AllowedIPs = <IPs to route to this peer>
Endpoint = <peer's public IP:port>     # optional if peer connects to us
PersistentKeepalive = 25               # optional, for NAT traversal
PresharedKey = <shared secret>         # optional, extra security

Common AllowedIPs Patterns

PatternMeaning
10.0.0.2/32Only this specific IP
10.0.0.0/24Entire VPN subnet
192.168.1.0/24Remote LAN (site-to-site)
0.0.0.0/0, ::/0All traffic (full tunnel)

When WireGuard Doesn’t Fit

WireGuard isn’t the answer to every VPN problem. Here’s when you might need something else:

No built-in user authentication. WireGuard authenticates by public key, not username/password. If you need Active Directory integration or RADIUS authentication, you’ll need to layer something on top (like a management tool) or use OpenVPN.

Static IP assignment. Each peer has a fixed IP in the config. There’s no DHCP-style dynamic assignment. For large organizations with hundreds of users, this means more config management.

No TCP fallback. WireGuard is UDP-only. If you’re behind a restrictive corporate firewall that blocks all UDP, WireGuard won’t work. OpenVPN can fall back to TCP 443, masquerading as HTTPS traffic.

Limited obfuscation. WireGuard’s packet format is distinctive. In countries that actively block VPN protocols, it can be detected and blocked. Tools like Shadowsocks or obfsproxy are designed for this use case.

No session persistence. Rebooting a peer requires the other side to re-establish the connection. For most setups with PersistentKeepalive, this happens automatically. But if you need guaranteed sub-second failover, you might need more infrastructure.

WireGuard works best when:

  • You control both endpoints
  • You have a small-to-medium number of peers (tens, not thousands)
  • You value simplicity and performance over enterprise features
  • UDP can reach your server

For personal VPNs, homelab connectivity, and small team infrastructure—WireGuard is almost always the right choice.

Summary

WireGuard’s power is in its simplicity:

  1. Generate keyswg genkey | tee priv | wg pubkey > pub
  2. Exchange public keys — never share private keys
  3. Write configs — ~10 lines per machine
  4. Start itwg-quick up wg0
  5. Enable on bootsystemctl enable wg-quick@wg0

The mental model is straightforward: each machine has an interface with a private key, and a list of peers identified by their public keys. AllowedIPs determines both routing and authentication.

Start with a simple two-machine setup, verify it works, then expand to more complex topologies as needed.


Remember that 87-line OpenVPN config? Here’s the WireGuard equivalent:

[Interface]
PrivateKey = <your-key>
Address = 10.0.0.2/24

[Peer]
PublicKey = <server-key>
Endpoint = server.example.com:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25

Seven lines. Works on the first try. Doesn’t drop.

About the Author

Ashish Anand

Ashish Anand

Founder & Lead Developer

Full-stack developer with 10+ years experience in Python, JavaScript, and DevOps. Creator of DevGuide.dev. Previously worked at Microsoft. Specializes in developer tools and automation.