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-toolsfor the userspace utilities.- CentOS/RHEL 8: You may need to install
kmod-wireguardfrom 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:
- A private key (kept secret)
- A public key (shared with other peers)
- An IP address on the VPN tunnel
- 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.1line 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:
- The server's firewall is blocking UDP port 51820.
- The server's WireGuard interface is not running.
- The server has the wrong public key for Peer B in its config.
- 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
-
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.
-
Full tunnel: Modify the setup so that all traffic from the client routes through the VPN. Visit
ifconfig.meorcurl ifconfig.meto verify your exit IP has changed to the server's IP. -
Multi-peer: Add a third peer to your setup. Verify that all three peers can ping each other through the VPN.
-
Performance test: Install
iperf3on 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 -
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.