The Latency Spikes and Dropped SSH Sessions Nobody Asked For
Last quarter, our distributed setup hit a wall. Backups between our US-East and EU-Central nodes crawled to a halt, often capping out around 140 Mbps on a 1 Gbps pipe. SSH sessions froze during large PostgreSQL schema migrations, and staging queries regularly timed out. Our OpenVPN tunnels were chewing through CPU cycles and bleeding nearly 45% of our raw bandwidth.
Worse, every developer’s outbound traffic routed through a single central gateway just to reach one private Redis instance at 192.168.50.12. A simple npm install or Zoom call competed directly with production syncs. That gateway routinely pegged two CPU cores at 90% utilization during peak work hours.
Root Cause: Protocol Bloat and Monolithic Routing
Two systemic flaws choked our network performance:
1. User-Space Context Switching and TLS Overhead
Traditional SSL/TLS VPNs live in user space. Every single packet crosses the kernel-user boundary twice: once when captured by the virtual TUN/TAP interface, and again when encrypted and handed back to the network socket. This rapid context switching destroys L1/L2 CPU cache locality and drives load averages through the roof. When cross-cloud routes suffered 1% to 2% packet loss, OpenVPN’s stateful handshake machine stalled, stalling active TCP sessions behind it.
2. The ‘Route Everything’ Default
Our client profiles blindly pushed 0.0.0.0/0 across the tunnel. Redirecting the default gateway meant routine external package downloads, video streams, and public API calls dragged through our staging gateway. Without isolated routing tables, internal management traffic crippled public transit.
Benchmarking the Field: OpenVPN vs. IPsec vs. WireGuard
We tested all three options on identical 4-core, 8 GB RAM nodes running Ubuntu 22.04 LTS across a 1 Gbps link using iperf3:
- OpenVPN (v2.5, UDP, AES-256-GCM): Topped out at 165 Mbps while pinning one CPU core at 100%. Managing an internal Certificate Authority and dealing with certificate rotation added tedious operational friction.
- IPsec / strongSwan (IKEv2, AES-128-GCM): Fast and kernel-driven, hitting 780 Mbps at 24% CPU load. However, the configuration syntax in
swanctl.confis dense. Rekeying across stateful AWS NAT gateways broke handshakes twice during our two-week burn-in test. - WireGuard (Kernel Module): Saturated 935 Mbps with only 4% overall CPU usage. Built straight into Linux 5.6+, it relies on modern primitives: Curve25519 for key exchange, ChaCha20-Poly1305 for authenticated encryption, and BLAKE2s for hashing. Its codebase spans fewer than 4,000 lines of readable C. WireGuard maintains quiet, UDP-based peer relationships, cutting handshake overhead down to zero until data actually needs to move.
The Production Blueprint: Kernel WireGuard with Split Tunneling
We staged this rollout across two production nodes: Node A (Gateway: 203.0.113.10) and Node B (Remote Worker: 198.51.100.4). Rather than route all internet traffic through the tunnel, we restricted transit strictly to private subnets.
Step 1: Install Utilities and Generate Cryptographic Keys
Install the kernel control tools on both machines:
# Ubuntu / Debian
sudo apt update && sudo apt install -y wireguard wireguard-tools
# RHEL / Rocky Linux 9 / AlmaLinux 9
sudo dnf install -y epel-release
sudo dnf install -y wireguard-tools
WireGuard uses public-key cryptography inspired by Noise and SSH. On both nodes, generate a private key and derive its corresponding public key with strict file permissions:
umask 077
wg genkey | tee privatekey | wg pubkey > publickey
Step 2: Configure the Gateway Server (Node A)
Create /etc/wireguard/wg0.conf on Node A. In this architecture, Node A uses internal tunnel IP 10.100.0.1/24 and listens on UDP port 51820:
[Interface]
Address = 10.100.0.1/24
ListenPort = 51820
PrivateKey = <NODE_A_PRIVATE_KEY>
# Forward packets and handle NAT for the internal private subnet (192.168.50.0/24)
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <NODE_B_PUBLIC_KEY>
AllowedIPs = 10.100.0.2/32
Persist packet forwarding in the kernel so Node A can route packets between interfaces:
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
Step 3: Configure the Client with Split Tunneling (Node B)
To eliminate bandwidth hogging on the gateway, configure Node B with precise routing. By scoping AllowedIPs to internal subnets only (10.100.0.0/24 and 192.168.50.0/24), wg-quick automatically injects specific kernel routes. All other outbound traffic stays on Node B’s local default route.
Create /etc/wireguard/wg0.conf on Node B:
[Interface]
Address = 10.100.0.2/24
PrivateKey = <NODE_B_PRIVATE_KEY>
[Peer]
PublicKey = <NODE_A_PUBLIC_KEY>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.100.0.0/24, 192.168.50.0/24
PersistentKeepalive = 25
The PersistentKeepalive = 25 parameter sends an empty authenticated packet every 25 seconds. This keeps stateful NAT tables alive in cloud firewalls, ensuring incoming connections reach Node B even behind aggressive provider NATs.
Step 4: Bring Up the Link and Inspect the Handshake
Start the interfaces using wg-quick and configure systemd to start them at boot time:
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0
Verify the link state on either node:
sudo wg show wg0
You should see an established session and bidirectional traffic counters:
interface: wg0
public key: hK9sB8r3G2...x8=
listening port: 51820
peer: rF3wK91mP0...m1=
endpoint: 198.51.100.4:51820
latest handshake: 18 seconds ago
transfer: 4.82 MiB received, 11.29 MiB sent
Test the route from Node B. Only packets bound for the internal network should traverse wg0:
ip route get 192.168.50.12
# Output: 192.168.50.12 dev wg0 src 10.100.0.2
ip route get 1.1.1.1
# Output: 1.1.1.1 via 198.51.100.1 dev eth0 src 198.51.100.4
Step 5: Defense-in-Depth with Pre-Shared Keys and Scoped Firewalls
When running mesh or point-to-point links across heterogeneous cloud providers, add a layer of symmetric encryption to guard against future quantum cryptanalysis of Curve25519 handshakes. Generate a pre-shared key (PSK):
wg genpsk > preshared.key
Place PresharedKey = <CONTENTS_OF_FILE> under the [Peer] section in both configuration files. WireGuard mixes this 256-bit secret into the handshake hashing without introducing measurable latency.
Next, restrict access to the UDP listener. Do not expose port 51820 to the public internet if your peer has a static public IP. Lock it down to the exact remote origin:
# UFW (Ubuntu / Debian)
sudo ufw allow proto udp from 198.51.100.4 to any port 51820 comment "WireGuard Peer Node B"
# firewalld (RHEL / Rocky / AlmaLinux)
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="198.51.100.4" port port="51820" protocol="udp" accept'
sudo firewall-cmd --reload
Measurable Wins in Production
This migration solved our bottlenecks immediately. Point-to-point latency between our cross-region nodes settled from 48ms down to 19ms, while ping jitter dropped from ±14ms to under 0.8ms. Under full 1 Gbps database backup replication, gateway CPU load remained flat below 6%. Split tunneling freed up our central pipe entirely, letting developers work with private databases at native speeds without choking public internet connections.

