WireGuard

Modern kernel-level VPN — simple, fast, and cryptographically sound

01

Overview

WireGuard is a modern VPN protocol implemented as a Linux kernel module. Its entire codebase is roughly 4,000 lines of code (excluding crypto primitives) — compared to ~70,000 for OpenVPN or ~400,000 for IPsec (StrongSwan). This minimal attack surface makes it far easier to audit and dramatically less likely to contain bugs.

WireGuard uses exclusively modern cryptography: Curve25519 for key exchange, ChaCha20 for symmetric encryption, Poly1305 for authentication, and BLAKE2s for hashing. There is no cipher negotiation — if a vulnerability is found in any primitive, the entire protocol version is updated. This eliminates the downgrade attacks that plague TLS and IPsec.

Core UDP-Based

WireGuard operates entirely over UDP (default port 51820). There is no TCP fallback. This avoids the TCP-over-TCP meltdown problem that affects OpenVPN in TCP mode. The trade-off is that WireGuard may be blocked on networks that only allow TCP 80/443.

Core Peer Model

WireGuard does not have a concept of "client" and "server." Every node is a peer. Each peer has a public/private keypair and a list of AllowedIPs. A peer that listens on a port and has other peers connecting to it acts as a "server" by convention, but the protocol is symmetric.

Performance Kernel-Level

WireGuard runs inside the Linux kernel (merged in 5.6, released March 2020), which means packets never cross the kernel/userspace boundary. This gives it significantly higher throughput and lower latency than userspace OpenVPN. Note: OpenVPN Data Channel Offload (DCO) was merged into the Linux kernel (6.16) in 2025, giving OpenVPN a kernel datapath as well, narrowing the performance gap considerably.

Simplicity Configuration

A WireGuard config file is typically 10-15 lines. Compare this to OpenVPN configs that can span 50+ lines with certificate paths, cipher suites, and TLS settings. WireGuard uses simple base64-encoded public keys instead of a full PKI/certificate infrastructure.

WireGuard vs OpenVPN vs IPsec

AspectWireGuardOpenVPNIPsec
Codebase~4,000 lines~70,000 lines~400,000 lines
ProtocolUDP onlyUDP or TCPESP/AH (IP protocol 50/51)
EncryptionChaCha20-Poly1305Negotiated (AES-GCM, ChaCha20, etc.)Negotiated (AES-GCM, AES-CBC, etc.)
Key exchangeCurve25519TLS with certificatesIKEv2 with certificates/PSK
PerformanceKernel-space, very fastUserspace (or kernel with DCO)Kernel-space, fast
ComplexityMinimal configModerate (certs, PKI)High (IKE phases, SA, SPD)
NAT traversalBuilt-in (UDP)Built-inRequires NAT-T (UDP 4500)
02

Full Tunnel VPN

The most common WireGuard use case: route all traffic from your device through a remote cloud VM (VPS). The remote VM acts as the VPN gateway. Your real IP is hidden — websites see the VPS IP instead. All traffic between you and the VPS is encrypted, making this ideal for privacy, bypassing geo-restrictions, or securing traffic on public WiFi.

The key configuration element is AllowedIPs = 0.0.0.0/0, ::/0 on the client side. This tells WireGuard to route all IPv4 and IPv6 traffic through the tunnel.

Server config (cloud VM)

# /etc/wireguard/wg0.conf on the VPS (e.g., 203.0.113.1)
[Interface]
PrivateKey = <server-private-key>
Address = 10.0.0.1/24
ListenPort = 51820

# NAT: masquerade tunnel traffic out the public interface
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT
# Add ip6tables rules if routing IPv6 (client AllowedIPs includes ::/0)
PostUp = ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# Client
PublicKey = <client-public-key>
AllowedIPs = 10.0.0.2/32

Client config (your laptop/phone)

# /etc/wireguard/wg0.conf on the client
[Interface]
PrivateKey = <client-private-key>
Address = 10.0.0.2/24
DNS = 1.1.1.1, 9.9.9.9

[Peer]
# VPS server
PublicKey = <server-public-key>
Endpoint = 203.0.113.1:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
How it works

The PostUp iptables rules on the server enable NAT masquerading: packets arriving from the WireGuard tunnel (10.0.0.0/24) are rewritten to appear as if they originate from the server's public IP. Without these rules, return traffic from the internet would not know how to reach 10.0.0.2. The PostDown rules clean up when the interface is brought down. Make sure net.ipv4.ip_forward = 1 is set in /etc/sysctl.d/ (or /etc/sysctl.conf). If routing IPv6, also set net.ipv6.conf.all.forwarding = 1.

Enable IP forwarding on the server

# Enable immediately
sysctl -w net.ipv4.ip_forward=1

# Persist across reboots
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-wireguard.conf
sysctl -p /etc/sysctl.d/99-wireguard.conf

# If routing IPv6 traffic (AllowedIPs includes ::/0), also enable:
sysctl -w net.ipv6.conf.all.forwarding=1
echo "net.ipv6.conf.all.forwarding = 1" >> /etc/sysctl.d/99-wireguard.conf
03

Site-to-Site / Subnet Routing

Use case: connect a remote office subnet (e.g., 10.0.2.0/24) to HQ (e.g., 10.0.1.0/24) or to a cloud VPC. Unlike a full tunnel, you only route traffic destined for the remote subnet through the tunnel — all other traffic goes out the local internet connection normally.

The key difference from a full tunnel is AllowedIPs: instead of 0.0.0.0/0, each side lists only the remote subnet(s). Both sides need ip_forward=1 because they are acting as routers for their respective LANs.

HQ Network WireGuard Tunnel Remote Office 10.0.1.0/24 10.0.2.0/24 [Workstation] [Workstation] 10.0.1.50 10.0.2.50 | | | | [HQ Gateway] ---- wg0: 10.100.0.1 <======> 10.100.0.2: wg0 ---- [Remote GW] 10.0.1.1 AllowedIPs=10.0.2.0/24 AllowedIPs=10.0.1.0/24 10.0.2.1 Endpoint=remote Endpoint=hq

HQ gateway config

# /etc/wireguard/wg0.conf on HQ gateway (public IP: 198.51.100.1)
[Interface]
PrivateKey = <hq-private-key>
Address = 10.100.0.1/24
ListenPort = 51820

PostUp = iptables -t nat -A POSTROUTING -s 10.0.2.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.0.2.0/24 -o eth0 -j MASQUERADE

[Peer]
# Remote office gateway
PublicKey = <remote-public-key>
Endpoint = 203.0.113.50:51820
AllowedIPs = 10.0.2.0/24, 10.100.0.2/32

Remote office gateway config

# /etc/wireguard/wg0.conf on remote gateway (public IP: 203.0.113.50)
[Interface]
PrivateKey = <remote-private-key>
Address = 10.100.0.2/24
ListenPort = 51820

PostUp = iptables -t nat -A POSTROUTING -s 10.0.1.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.0.1.0/24 -o eth0 -j MASQUERADE

[Peer]
# HQ gateway
PublicKey = <hq-public-key>
Endpoint = 198.51.100.1:51820
AllowedIPs = 10.0.1.0/24, 10.100.0.1/32
Requirements on both sides

Both gateways must have net.ipv4.ip_forward = 1 enabled. The WireGuard peer is acting as a router for its LAN — packets arriving from the tunnel need to be forwarded to local hosts. You also need iptables MASQUERADE rules (or static routes on all LAN hosts pointing to the WireGuard gateway) so that return traffic finds its way back through the tunnel. Without MASQUERADE, LAN hosts would try to reply directly to the remote subnet, which they have no route to.

04

Bastion Host Pattern

When the remote office has no direct inbound internet access — behind CGNAT, a restrictive firewall, or a consumer ISP that blocks port forwarding — you cannot set an Endpoint pointing to it. The solution is a bastion (relay) server in the cloud that both HQ and the remote office connect to.

The remote office initiates the WireGuard connection outbound to the bastion. Because the connection is outbound, CGNAT and firewalls do not block it. The bastion then relays traffic between HQ and the remote office. The remote office uses PersistentKeepalive = 25 to keep the NAT mapping alive.

HQ (10.0.1.0/24) Cloud Bastion Remote Office (10.0.2.0/24) (203.0.113.99) (behind CGNAT, no public IP) [HQ Gateway] ----wg0---> [Bastion Server] <---wg0---- [Remote Gateway] 10.100.0.1 10.100.0.254 10.100.0.2 AllowedIPs for HQ: PersistentKeepalive = 25 10.0.1.0/24 (initiates outbound) AllowedIPs for Remote: 10.0.2.0/24

Bastion server config

# /etc/wireguard/wg0.conf on bastion (203.0.113.99)
[Interface]
PrivateKey = <bastion-private-key>
Address = 10.100.0.254/24
ListenPort = 51820

# Forward traffic between peers
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -o wg0 -j ACCEPT

[Peer]
# HQ gateway
PublicKey = <hq-public-key>
Endpoint = 198.51.100.1:51820
AllowedIPs = 10.0.1.0/24, 10.100.0.1/32

[Peer]
# Remote office (no Endpoint -- it connects to us)
PublicKey = <remote-public-key>
AllowedIPs = 10.0.2.0/24, 10.100.0.2/32

HQ gateway config

# /etc/wireguard/wg0.conf on HQ gateway
[Interface]
PrivateKey = <hq-private-key>
Address = 10.100.0.1/24
ListenPort = 51820

[Peer]
# Bastion server
PublicKey = <bastion-public-key>
Endpoint = 203.0.113.99:51820
AllowedIPs = 10.0.2.0/24, 10.100.0.254/32, 10.100.0.2/32
PersistentKeepalive = 25

Remote office gateway config

# /etc/wireguard/wg0.conf on remote office gateway (behind CGNAT)
[Interface]
PrivateKey = <remote-private-key>
Address = 10.100.0.2/24

[Peer]
# Bastion server
PublicKey = <bastion-public-key>
Endpoint = 203.0.113.99:51820
AllowedIPs = 10.0.1.0/24, 10.100.0.254/32, 10.100.0.1/32
PersistentKeepalive = 25
Key detail

Notice the bastion's remote office peer has no Endpoint. WireGuard learns the remote office's endpoint dynamically when it receives the first handshake packet. The PersistentKeepalive = 25 on the remote office side sends a keepalive packet every 25 seconds, which keeps the NAT mapping alive and tells the bastion where to send return traffic. The iptables -A FORWARD -i wg0 -o wg0 rule on the bastion allows traffic to flow between WireGuard peers (wg0 in, wg0 out).

05

The Split-Route CIDR Hack

A subtle problem: you are physically sitting on 10.0.2.0/24 (your local LAN), and your WireGuard config has AllowedIPs = 10.0.2.0/24 to reach the remote office that also uses 10.0.2.0/24. When WireGuard creates a route for 10.0.2.0/24 via the tunnel interface, your OS will prefer this route for all traffic to that subnet — including traffic to your local LAN hosts. You lose local connectivity.

The problem

Your local machine has two routes for the same 10.0.2.0/24 subnet: one via eth0 (directly connected) and one via wg0 (WireGuard). Depending on the OS and metric, WireGuard's route may win. Your SSH session to 10.0.2.5 (a local machine) suddenly goes through the VPN tunnel and either fails or reaches the wrong host on the remote side.

Approach 1: Two more-specific routes (split the /24)

Replace the single 10.0.2.0/24 in AllowedIPs with two routes that are more specific:

[Peer]
# Instead of: AllowedIPs = 10.0.2.0/24
# Use two /25 subnets that together cover all of 10.0.2.0/24:
AllowedIPs = 10.0.2.0/25, 10.0.2.128/25

Why this works: wg-quick uses policy routing (routing table and rules) rather than simply adding routes to the main table. It places WireGuard routes in a separate routing table and adds fwmark-based rules so that traffic already marked as WireGuard traffic is excluded. The /25 routes in the WireGuard routing table are more specific than the directly-connected /24, so remote traffic matches and goes through the tunnel. Local traffic to on-link hosts on eth0 is resolved directly via ARP and uses the connected route.

How kernel routing decides

wg-quick sets up policy routing with fwmark rules: WireGuard-bound traffic is marked and routed through a separate table containing the /25 routes. Traffic originating locally to on-link /24 destinations on eth0 is resolved via ARP on the connected interface and does not enter the WireGuard routing table. The net effect: local hosts use eth0 and remote hosts (on the other side of the tunnel) use wg0 — exactly what you want.

Approach 2: Use a broader /23 route

Instead of making routes more specific, go the other direction — use a less specific route:

[Peer]
# Instead of: AllowedIPs = 10.0.2.0/24
# Use a broader /23 that still covers 10.0.2.0/24:
AllowedIPs = 10.0.2.0/23

A /23 covers 10.0.2.0 through 10.0.3.255. Since your local LAN has a more specific /24 route on eth0, the local route takes priority for local traffic. The /23 via wg0 catches everything else in that range.

Trade-off

The /23 approach is simpler but routes a larger range through the tunnel (10.0.3.0/24 will also go through wg0, even if nothing lives there). The /25 split approach is more precise and commonly used. Choose based on your network's addressing plan.

06

Peer Management

Generating keys

# Generate a private key (set umask first to restrict permissions)
umask 077
wg genkey > private.key

# Derive the public key from the private key
wg pubkey < private.key > public.key

# Generate both in one line
wg genkey | tee private.key | wg pubkey > public.key

# Generate a preshared key (optional, adds symmetric layer for defense-in-depth)
wg genpsk > preshared.key

Adding a peer to a running interface

# Add a new peer without restarting the tunnel
wg set wg0 peer <client-public-key> \
  allowed-ips 10.0.0.3/32 \
  endpoint 203.0.113.50:51820

# Add a peer with a preshared key
wg set wg0 peer <client-public-key> \
  allowed-ips 10.0.0.3/32 \
  preshared-key /etc/wireguard/preshared.key

Removing a peer

# Remove a peer by public key
wg set wg0 peer <client-public-key> remove

QR code for mobile clients

# Generate a QR code from a config file (scannable by WireGuard mobile app)
qrencode -t ansiutf8 < /etc/wireguard/client-phone.conf

# Or pipe it to a PNG file
qrencode -t png -o client-phone-qr.png < /etc/wireguard/client-phone.conf

Script: generate a new peer

#!/bin/bash
# Usage: ./add-peer.sh <peer-name> <tunnel-ip>
# Example: ./add-peer.sh phone 10.0.0.3

PEER_NAME="$1"
TUNNEL_IP="$2"
SERVER_PUBKEY="$(cat /etc/wireguard/public.key)"
SERVER_ENDPOINT="203.0.113.1:51820"
DNS="1.1.1.1, 9.9.9.9"

# Generate keys
wg genkey | tee "/etc/wireguard/${PEER_NAME}-private.key" \
  | wg pubkey > "/etc/wireguard/${PEER_NAME}-public.key"

PEER_PRIVKEY=$(cat "/etc/wireguard/${PEER_NAME}-private.key")
PEER_PUBKEY=$(cat "/etc/wireguard/${PEER_NAME}-public.key")

# Create client config
cat > "/etc/wireguard/${PEER_NAME}.conf" <<EOF
[Interface]
PrivateKey = ${PEER_PRIVKEY}
Address = ${TUNNEL_IP}/24
DNS = ${DNS}

[Peer]
PublicKey = ${SERVER_PUBKEY}
Endpoint = ${SERVER_ENDPOINT}
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF

# Add peer to running server interface
wg set wg0 peer "${PEER_PUBKEY}" allowed-ips "${TUNNEL_IP}/32"

# Save to server config so it persists across restarts
cat >> /etc/wireguard/wg0.conf <<EOF

[Peer]
# ${PEER_NAME}
PublicKey = ${PEER_PUBKEY}
AllowedIPs = ${TUNNEL_IP}/32
EOF

echo "--- Client config: /etc/wireguard/${PEER_NAME}.conf ---"
cat "/etc/wireguard/${PEER_NAME}.conf"
echo ""
echo "--- QR Code (scan with WireGuard mobile app) ---"
qrencode -t ansiutf8 < "/etc/wireguard/${PEER_NAME}.conf"
07

Common Commands

Interface management

# Bring up a WireGuard interface
wg-quick up wg0

# Bring down a WireGuard interface
wg-quick down wg0

# Enable WireGuard to start on boot
systemctl enable wg-quick@wg0

# Start / stop / restart via systemd
systemctl start wg-quick@wg0
systemctl stop wg-quick@wg0
systemctl restart wg-quick@wg0

# Check service status
systemctl status wg-quick@wg0

Inspecting the tunnel

# Show all WireGuard interfaces and peers
wg show

# Show a specific interface
wg show wg0

# Show only the public key of the local interface
wg show wg0 public-key

# Show peer endpoints and last handshake
wg show wg0 endpoints
wg show wg0 latest-handshakes

# Show transfer stats (bytes sent/received per peer)
wg show wg0 transfer

Troubleshooting

No handshake Firewall / port issue

If wg show shows no latest handshake for a peer, the UDP packets are not reaching the other side. Check: is UDP 51820 (or your configured port) open in the firewall? Is the endpoint IP correct? Is the peer actually running?

  • sudo ufw allow 51820/udp
  • sudo firewall-cmd --add-port=51820/udp --permanent
  • Cloud provider security groups (AWS SG, GCP firewall rules)

Handshake but no traffic Routing / forwarding

If you see a recent handshake but traffic does not flow, check:

  • ip_forward enabled? sysctl net.ipv4.ip_forward
  • AllowedIPs correct on both sides?
  • iptables MASQUERADE rules present?
  • iptables -L FORWARD -v — is traffic being dropped?

DNS issues Leaking or failing

If DNS resolution fails after connecting, ensure the DNS line is set in the client config. wg-quick configures the system resolver. Check with resolvectl status wg0 on systemd systems.

Key mismatch Wrong public key

A common mistake: using the private key where the public key should go (or vice versa). The server's [Peer] section must contain the client's public key, and the client's [Peer] section must contain the server's public key. Never share private keys.

08

Production Checklist

  • Restrict key file permissionschmod 600 /etc/wireguard/wg0.conf and all .key files. Only root should read private keys.
  • Enable on bootsystemctl enable wg-quick@wg0 so the tunnel survives reboots.
  • IP forwarding — persist net.ipv4.ip_forward = 1 in /etc/sysctl.d/ if you are routing traffic (full tunnel, site-to-site, or bastion). Also set net.ipv6.conf.all.forwarding = 1 if routing IPv6.
  • Firewall rules — open UDP on your WireGuard port (default 51820). Open it in both the OS firewall and cloud provider security groups.
  • PostUp/PostDown — use these for iptables rules so they are applied/removed cleanly with the interface.
  • PersistentKeepalive — set to 25 for peers behind NAT or CGNAT. Not needed for peers with static public IPs and open ports.
  • DNS configuration — set the DNS line in client configs to prevent DNS leaks. Use a trusted resolver (1.1.1.1, 9.9.9.9, or your own).
  • Preshared keys — add PresharedKey to peer configs for an additional layer of symmetric-key protection. This adds defense-in-depth against future quantum attacks by mixing a symmetric secret into the handshake, though it does not provide full post-quantum security on its own. For stronger post-quantum resistance, consider tools like Rosenpass that rotate post-quantum PSKs automatically.
  • Monitor handshakes — a healthy WireGuard peer shows a "latest handshake" within the last 2-3 minutes (if traffic is flowing). Set up alerting on stale handshakes.
  • Log carefully — WireGuard is intentionally silent (no logging by default for privacy). Use echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control for temporary kernel-level debug logging.
  • Keep kernel updated — WireGuard is a kernel module. Kernel updates may include WireGuard security fixes.
  • Avoid overlapping subnets — plan your tunnel IPs and AllowedIPs carefully. Overlapping CIDRs cause routing confusion (see Section 05).