WireGuard VPN

Why This Matters

Your company has developers working from coffee shops, home offices, and airports. They need to reach internal services -- databases, staging environments, monitoring dashboards -- that should never be exposed to the public internet. You could use SSH tunnels for each service, but managing dozens of tunnels is painful and fragile. You need a VPN.

For decades, OpenVPN and IPsec were the standard choices. They work, but they are complex: OpenVPN has a 100,000+ line codebase, certificates to manage, and performance overhead. IPsec configuration can fill entire books on its own.

WireGuard is a modern VPN that takes a radically different approach: simplicity. Its entire codebase is about 4,000 lines of code. It is built into the Linux kernel since version 5.6. It uses state-of-the-art cryptography, and it is fast -- often significantly faster than OpenVPN or IPsec. Configuration is a single file.

If you need a VPN on Linux in 2025, WireGuard should be your first choice.


Try This Right Now

# Check if WireGuard is available on your kernel
modprobe wireguard && echo "WireGuard module loaded" || echo "Not available"

# Check if wg tools are installed
wg --version

# If not installed, install it:
# Debian/Ubuntu:
# sudo apt install wireguard

# RHEL/CentOS/Fedora:
# sudo dnf install wireguard-tools

# Arch:
# sudo pacman -S wireguard-tools

Distro Note:

  • Ubuntu 20.04+, Debian 11+, Fedora 32+: WireGuard is in the kernel. Just install wireguard-tools for the userspace utilities.
  • CentOS/RHEL 8: You may need to install kmod-wireguard from ELRepo or use the DKMS module: sudo dnf install elrepo-release && sudo dnf install kmod-wireguard.
  • CentOS/RHEL 9+: WireGuard is in the kernel. Install wireguard-tools.
  • Older kernels (< 5.6): WireGuard is available as a DKMS module via the WireGuard PPA or ELRepo.

WireGuard vs OpenVPN

+------------------------------------------------------------------+
|  Feature           | WireGuard          | OpenVPN               |
|--------------------|--------------------|----------------------|
|  Codebase          | ~4,000 lines       | ~100,000+ lines      |
|  Protocol          | UDP only           | UDP or TCP           |
|  Cryptography      | Modern, fixed      | Configurable (TLS)   |
|                    | (Curve25519,       | (many cipher options)|
|                    | ChaCha20, Poly1305)|                      |
|  Performance       | Excellent (kernel) | Good (userspace)     |
|  Configuration     | Simple INI-like    | Complex config files |
|  Key management    | Public/private keys| Certificates (PKI)   |
|  Connection model  | Peer-to-peer       | Client-server        |
|  Stealth           | No response to     | Depends on config    |
|                    | unauthenticated    |                      |
|                    | packets            |                      |
|  Roaming           | Built-in           | Reconnect needed     |
|  Kernel integration| In-kernel (5.6+)   | Userspace (tun/tap)  |
+------------------------------------------------------------------+

WireGuard's philosophy: there are no configurable cipher suites. It uses one fixed set of modern, audited algorithms. If a vulnerability is found, a new version replaces the algorithms entirely. This eliminates the entire category of "misconfigured crypto" bugs.


Core Concepts

WireGuard thinks in terms of peers, not "clients" and "servers." Every machine that participates in a WireGuard network is a peer. Each peer has:

  1. A private key (kept secret)
  2. A public key (shared with other peers)
  3. An IP address on the VPN tunnel
  4. A list of allowed IPs (what traffic routes through the tunnel to this peer)
+--------------------+                    +--------------------+
|    Peer A          |   WireGuard        |    Peer B          |
|                    |   Tunnel           |                    |
|  Private key: kA   |<==================>|  Private key: kB   |
|  Public key:  KA   |   UDP port 51820   |  Public key:  KB   |
|  VPN IP: 10.0.0.1  |                    |  VPN IP: 10.0.0.2  |
|                    |                    |                    |
|  AllowedIPs for B: |                    |  AllowedIPs for A: |
|    10.0.0.2/32     |                    |    10.0.0.1/32     |
+--------------------+                    +--------------------+

The Cryptokey Routing Table

WireGuard's most elegant concept is the cryptokey routing table. It maps public keys to allowed IP addresses. When WireGuard receives an encrypted packet, it decrypts it with the peer's key and checks if the source IP is in that peer's AllowedIPs. If not, the packet is dropped.

Similarly, when WireGuard needs to send a packet, it looks up the destination IP in the AllowedIPs of all peers and encrypts it with the matching peer's public key.

Cryptokey Routing Table:
+-----------------------------------+
| Public Key        | AllowedIPs    |
|-------------------|---------------|
| KB (Peer B)       | 10.0.0.2/32  |
| KC (Peer C)       | 10.0.0.3/32, |
|                   | 192.168.2.0/24|
+-----------------------------------+

Packet to 10.0.0.2 --> encrypt with KB --> send to Peer B's endpoint
Packet to 192.168.2.50 --> encrypt with KC --> send to Peer C's endpoint

Hands-On: Point-to-Point Tunnel

Let's set up a basic WireGuard tunnel between two machines.

Step 1: Generate Keys on Both Machines

On Peer A (the "server"):

# Generate private key
wg genkey | sudo tee /etc/wireguard/private.key
sudo chmod 600 /etc/wireguard/private.key

# Derive public key from private key
sudo cat /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key

On Peer B (the "client"):

wg genkey | sudo tee /etc/wireguard/private.key
sudo chmod 600 /etc/wireguard/private.key
sudo cat /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key

Now exchange public keys between the two machines. You need:

  • Peer A's public key on Peer B
  • Peer B's public key on Peer A

Think About It: Why do we generate keys separately on each machine rather than generating all keys on one machine and distributing them? Think about what would happen if the private key were intercepted during transfer.

Generating keys locally means the private key never crosses the network. If you generate all keys on one machine and transfer them, the private keys could be intercepted, logged, or cached on intermediate systems. The private key should ideally never exist anywhere except on the machine that owns it.

Step 2: Configure Peer A (Server)

Create /etc/wireguard/wg0.conf on Peer A:

[Interface]
# Peer A's private key
PrivateKey = <PEER_A_PRIVATE_KEY>
Address = 10.0.0.1/24
ListenPort = 51820

[Peer]
# Peer B's public key
PublicKey = <PEER_B_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32

Step 3: Configure Peer B (Client)

Create /etc/wireguard/wg0.conf on Peer B:

[Interface]
# Peer B's private key
PrivateKey = <PEER_B_PRIVATE_KEY>
Address = 10.0.0.2/24

[Peer]
# Peer A's public key
PublicKey = <PEER_A_PUBLIC_KEY>
Endpoint = 203.0.113.50:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25

Key differences on the client side:

  • Endpoint: Peer A's public IP and port. Peer B needs to know where to reach Peer A. Peer A does not need an Endpoint for Peer B because it will learn Peer B's address from incoming packets.
  • PersistentKeepalive: Sends a keepalive packet every 25 seconds. This is essential when Peer B is behind NAT, to keep the NAT mapping alive.

Step 4: Bring Up the Tunnel

On both machines:

# Start the tunnel
sudo wg-quick up wg0

# Check status
sudo wg show

# Test connectivity
ping -c 3 10.0.0.1    # From Peer B
ping -c 3 10.0.0.2    # From Peer A

Step 5: Verify the Tunnel

sudo wg show

Expected output on Peer A:

interface: wg0
  public key: <PEER_A_PUBLIC_KEY>
  private key: (hidden)
  listening port: 51820

peer: <PEER_B_PUBLIC_KEY>
  endpoint: 198.51.100.20:43210
  allowed ips: 10.0.0.2/32
  latest handshake: 12 seconds ago
  transfer: 1.24 KiB received, 956 B sent

If you see "latest handshake" with a recent timestamp, the tunnel is working.

Step 6: Enable on Boot

sudo systemctl enable wg-quick@wg0

Bringing the Tunnel Down

sudo wg-quick down wg0

Safety Warning: The WireGuard configuration file contains your private key. Set strict permissions:

sudo chmod 600 /etc/wireguard/wg0.conf
sudo chown root:root /etc/wireguard/wg0.conf

Routing All Traffic Through the VPN

To use WireGuard as a full VPN (all internet traffic goes through the tunnel), modify the client configuration:

On Peer B (client):

[Interface]
PrivateKey = <PEER_B_PRIVATE_KEY>
Address = 10.0.0.2/24
DNS = 1.1.1.1

[Peer]
PublicKey = <PEER_A_PUBLIC_KEY>
Endpoint = 203.0.113.50:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

The key change is AllowedIPs = 0.0.0.0/0, ::/0 -- this means "route ALL traffic (IPv4 and IPv6) through this peer."

On Peer A (server), enable forwarding and NAT:

[Interface]
PrivateKey = <PEER_A_PRIVATE_KEY>
Address = 10.0.0.1/24
ListenPort = 51820
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

Also enable IP forwarding on the server:

echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf

The PostUp and PostDown hooks in the config file run shell commands when the interface comes up or goes down. Here we add and remove NAT rules automatically.

Think About It: After setting AllowedIPs to 0.0.0.0/0, you notice your DNS queries are leaking -- they bypass the VPN and go to your local DNS server. Why, and how does the DNS = 1.1.1.1 line in the Interface section fix this?

wg-quick reads the DNS directive and configures the system's DNS resolver to use the specified server. Since all traffic (including DNS on port 53) is routed through AllowedIPs = 0.0.0.0/0, DNS queries now go through the tunnel to the VPN server, which forwards them to 1.1.1.1. Without the DNS directive, the system might still use the local DNS server configured by DHCP.


Multi-Peer Setup (Hub and Spoke)

A common architecture: one VPN server (hub) with multiple clients (spokes).

                    +-------------------+
                    |    VPN Server     |
                    |   10.0.0.1/24     |
                    |  (Public IP)      |
                    +---+-------+---+---+
                        |       |   |
              +---------+   +---+   +---------+
              |             |                 |
        +-----+----+  +----+-----+  +--------+---+
        | Client A |  | Client B |  | Client C   |
        | 10.0.0.2 |  | 10.0.0.3 |  | 10.0.0.4   |
        +----------+  +----------+  +-------------+

Server Configuration

# /etc/wireguard/wg0.conf on VPN Server

[Interface]
PrivateKey = <SERVER_PRIVATE_KEY>
Address = 10.0.0.1/24
ListenPort = 51820
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]
# Client A
PublicKey = <CLIENT_A_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32

[Peer]
# Client B
PublicKey = <CLIENT_B_PUBLIC_KEY>
AllowedIPs = 10.0.0.3/32

[Peer]
# Client C
PublicKey = <CLIENT_C_PUBLIC_KEY>
AllowedIPs = 10.0.0.4/32

Client A Configuration

# /etc/wireguard/wg0.conf on Client A

[Interface]
PrivateKey = <CLIENT_A_PRIVATE_KEY>
Address = 10.0.0.2/24
DNS = 1.1.1.1

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = 203.0.113.50:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25

With AllowedIPs = 10.0.0.0/24, Client A can reach the server and all other clients through the VPN, but regular internet traffic goes directly (split tunneling).

For full tunnel (all traffic through VPN), change to AllowedIPs = 0.0.0.0/0, ::/0.

Adding a New Peer Without Restarting

You can add peers on the fly without restarting the WireGuard interface:

# On the server, add a new peer dynamically
sudo wg set wg0 peer <NEW_CLIENT_PUBLIC_KEY> allowed-ips 10.0.0.5/32

# Save the running config to the config file
sudo wg-quick save wg0

Allowing Clients to Communicate with Each Other

By default in the hub-and-spoke model, client-to-client traffic flows through the server. For this to work, IP forwarding must be enabled on the server, and the iptables FORWARD rule must be in place.

If client A wants to reach client B (10.0.0.3), the packet flow is:

Client A (10.0.0.2) --> VPN Server (10.0.0.1) --> Client B (10.0.0.3)

On each client, AllowedIPs must include the other clients' IPs (or the entire VPN subnet like 10.0.0.0/24).


Site-to-Site VPN

WireGuard can connect entire networks, not just individual hosts. Say Office A has the network 192.168.1.0/24 and Office B has 192.168.2.0/24.

Office A Gateway

[Interface]
PrivateKey = <OFFICE_A_PRIVATE_KEY>
Address = 10.0.0.1/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT

[Peer]
PublicKey = <OFFICE_B_PUBLIC_KEY>
Endpoint = office-b.example.com:51820
AllowedIPs = 10.0.0.2/32, 192.168.2.0/24
PersistentKeepalive = 25

Office B Gateway

[Interface]
PrivateKey = <OFFICE_B_PRIVATE_KEY>
Address = 10.0.0.2/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT

[Peer]
PublicKey = <OFFICE_A_PUBLIC_KEY>
Endpoint = office-a.example.com:51820
AllowedIPs = 10.0.0.1/32, 192.168.1.0/24
PersistentKeepalive = 25

Each gateway must also have IP forwarding enabled and must be the default gateway (or have static routes) for machines on their respective LANs.

On machines in Office A, add a route:

sudo ip route add 192.168.2.0/24 via 192.168.1.1   # Office A gateway's LAN IP

Troubleshooting WireGuard

Check 1: Is the Interface Up?

sudo wg show
ip addr show wg0

If wg show outputs nothing, the interface is not up:

sudo wg-quick up wg0
# Check for errors in the output

Check 2: Is the Handshake Happening?

sudo wg show

Look at the latest handshake field. If it says "none" or the timestamp is very old, the peers have not successfully established a session.

Common causes:

  • Firewall blocking UDP port 51820
  • Incorrect public key
  • Endpoint address is wrong
  • NAT not being traversed (add PersistentKeepalive)

Check 3: Firewall Rules

# Ensure UDP port 51820 is open on the server
sudo iptables -L -n | grep 51820
sudo ss -ulnp | grep 51820

# If using ufw:
sudo ufw allow 51820/udp

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

Check 4: IP Forwarding

If peers can ping each other but cannot reach networks behind a peer:

cat /proc/sys/net/ipv4/ip_forward
# Should be 1

Check 5: Key Mismatch

The most common configuration error is a key mismatch. Double-check:

  • Peer A's config has Peer B's public key (not private!)
  • Peer B's config has Peer A's public key
  • Neither config has the wrong key pasted
# Verify a public key matches a private key
echo "<PRIVATE_KEY>" | wg pubkey
# Output should match the corresponding public key

Check 6: Listen for Packets

# On the server, watch for incoming WireGuard traffic
sudo tcpdump -i eth0 udp port 51820 -n

# You should see UDP packets arriving from the client's IP

If no packets arrive, the issue is upstream of the server (client firewall, NAT, ISP blocking UDP).

Check 7: AllowedIPs Configuration

A subtle but common problem: if the AllowedIPs on the server do not include the client's VPN IP, the server will decrypt packets from the client but then drop them because the source IP is not in the allowed list.

# Verify AllowedIPs for each peer
sudo wg show wg0

Debug This

Scenario: You have set up WireGuard between a server (Peer A, public IP 203.0.113.50) and a laptop (Peer B). sudo wg-quick up wg0 succeeds on both sides. But from Peer B, ping 10.0.0.1 (Peer A's VPN IP) fails. You check:

# On Peer B:
$ sudo wg show
interface: wg0
  public key: <PEER_B_PUB>
  private key: (hidden)
  listening port: 43210

peer: <PEER_A_PUB>
  endpoint: 203.0.113.50:51820
  allowed ips: 10.0.0.1/32
  latest handshake: (none)
  transfer: 0 B received, 920 B sent

What does "latest handshake: (none)" and "0 B received, 920 B sent" tell you?

Diagnosis: Peer B is sending packets (920 B sent = initiation handshake attempts) but receiving nothing back (0 B received). The handshake has never completed. This means Peer A is not responding. Possible causes:

  1. The server's firewall is blocking UDP port 51820.
  2. The server's WireGuard interface is not running.
  3. The server has the wrong public key for Peer B in its config.
  4. There is a NAT device in front of the server that is not forwarding port 51820.

Fix approach:

# SSH into Peer A and check:
sudo wg show                    # Is wg0 running?
sudo ss -ulnp | grep 51820     # Is WireGuard listening?
sudo iptables -L -n | grep 51820   # Is firewall blocking?
sudo tcpdump -i eth0 udp port 51820 -c 5   # Are packets arriving?

If tcpdump shows packets arriving but wg show shows no handshake, the key configuration is wrong. If tcpdump shows no packets, the issue is the network path (firewall or NAT between the two).


What Just Happened?

+-------------------------------------------------------------------+
|                     Chapter 37 Recap                               |
+-------------------------------------------------------------------+
|                                                                   |
|  * WireGuard is a modern VPN: simple, fast, in-kernel,            |
|    and uses state-of-the-art cryptography.                        |
|                                                                   |
|  * It uses a peer model, not client/server. Each peer has         |
|    a public/private key pair and a list of AllowedIPs.            |
|                                                                   |
|  * The cryptokey routing table maps public keys to IP             |
|    ranges, combining authentication and routing.                  |
|                                                                   |
|  * Configuration is a single INI-style file in                    |
|    /etc/wireguard/wg0.conf.                                       |
|                                                                   |
|  * wg-quick manages the interface lifecycle. Enable on boot       |
|    with systemctl enable wg-quick@wg0.                            |
|                                                                   |
|  * Route all traffic through VPN with AllowedIPs = 0.0.0.0/0.    |
|    Requires NAT and IP forwarding on the server.                  |
|                                                                   |
|  * Multi-peer setups use hub-and-spoke topology.                  |
|    Peers can be added dynamically with wg set.                    |
|                                                                   |
|  * Troubleshooting: check handshake status, firewall rules,       |
|    IP forwarding, and key correctness.                            |
|                                                                   |
+-------------------------------------------------------------------+

Try This

  1. Basic tunnel: Set up a WireGuard tunnel between two machines (VMs, cloud instances, or even two containers). Verify that you can ping across the tunnel.

  2. Full tunnel: Modify the setup so that all traffic from the client routes through the VPN. Visit ifconfig.me or curl ifconfig.me to verify your exit IP has changed to the server's IP.

  3. Multi-peer: Add a third peer to your setup. Verify that all three peers can ping each other through the VPN.

  4. Performance test: Install iperf3 on both ends. Run a bandwidth test through the WireGuard tunnel vs directly. How much overhead does WireGuard add?

    # On server: iperf3 -s
    # On client: iperf3 -c 10.0.0.1
    
  5. Bonus challenge: Set up a site-to-site VPN. Create two virtual networks (192.168.1.0/24 and 192.168.2.0/24) each behind a WireGuard gateway. Configure routing so that hosts on one network can reach hosts on the other network through the WireGuard tunnel, without WireGuard being installed on the individual hosts.