2026-04-02 · 10 min read · Serhii Mazurok
Running Kubernetes across multiple providers (Hetzner, AWS, bare-metal) requires a private network layer. WireGuard gives us that - fast, simple, and auditable.
When you create a Kubernetes cluster, nodes need to communicate over a private network. Cloud providers offer VPC/VNet, but:
We needed something that works everywhere, is fast, and easy to automate.
| Feature | WireGuard | IPSec | OpenVPN |
|---|---|---|---|
| Config complexity | ~10 lines | Hundreds of lines | Moderate |
| Kernel-space | Yes | Yes | No (userspace) |
| Performance | ~950 Mbps | ~800 Mbps | ~500 Mbps |
| Handshake | 1 RTT | Multiple RTTs | Multiple RTTs |
| Code size | ~4K lines | ~400K lines | ~100K lines |
| Attack surface | Minimal | Large | Moderate |
WireGuard operates in kernel space (built into Linux since 5.6), uses modern cryptography (Curve25519, ChaCha20, Poly1305), and is conceptually simple: each peer has a public key and a list of allowed IPs.
Our mesh network architecture for a 3-node cluster:
┌──────────────┐ WireGuard tunnel ┌──────────────┐
│ Node 1 │◄────────────────────────►│ Node 2 │
│ 10.10.0.1 │ │ 10.10.0.2 │
│ Hetzner │ │ Hetzner │
└──────┬───────┘ └──────┬───────┘
│ │
│ WireGuard tunnel │
│ ┌──────────────┐ │
└───►│ Node 3 │◄────────────────────┘
│ 10.10.0.3 │
│ AWS EC2 │
└──────────────┘
Each node has a WireGuard interface (wg0) with a private IP from the 10.10.0.0/24 range. Every node peers with every other node - a full mesh.
Our automated provisioning follows these steps:
1. Generate keys - each node generates a Curve25519 key pair 2. Exchange public keys - via the Segla control plane API (encrypted at rest) 3. Configure peers - each node receives the public key + public endpoint of every other node 4. Establish tunnels - WireGuard establishes connections automatically 5. Health check - we verify connectivity between all peers before proceeding with K8s installation
# Generated config on Node 1
[Interface]
PrivateKey = <node1-private-key>
Address = 10.10.0.1/24
ListenPort = 51820
[Peer]
PublicKey = <node2-public-key>
Endpoint = 167.235.12.46:51820
AllowedIPs = 10.10.0.2/32
PersistentKeepalive = 25
[Peer]
PublicKey = <node3-public-key>
Endpoint = 3.121.45.67:51820
AllowedIPs = 10.10.0.3/32
PersistentKeepalive = 25
Once the WireGuard mesh is up, Kubernetes sees all nodes on the same 10.10.0.0/24 network. We configure:
--node-ip=10.10.0.X)10.244.0.0/16) routes through the WireGuard overlayThis means the kube-apiserver is never exposed on the public internet. Access goes through our tunnel proxy at .
We benchmarked WireGuard overhead on Hetzner CX41 instances (same datacenter):
| Metric | Direct | WireGuard | Overhead |
|---|---|---|---|
| TCP throughput | 980 Mbps | 945 Mbps | ~3.6% |
| Latency (p50) | 0.28 ms | 0.31 ms | +0.03 ms |
| Latency (p99) | 0.42 ms | 0.48 ms | +0.06 ms |
For cross-provider (Hetzner → AWS Frankfurt), the WireGuard overhead is negligible compared to the base internet latency (~1-2 ms).
We monitor the mesh with:
last_handshake_seconds, transfer_bytes_received, transfer_bytes_sent# Alert rule
- alert: WireGuardPeerDown
expr: time() - wireguard_last_handshake_seconds > 180
for: 2m
labels:
severity: critical
annotations:
summary: "WireGuard peer {{ $labels.peer }} is unreachable"
1. WireGuard is the right choice for multi-provider Kubernetes networking - fast, simple, auditable 2. Full mesh works well for clusters up to ~50 nodes. Beyond that, consider a hub-and-spoke topology 3. Automate key exchange through your control plane, never manually 4. Monitor handshake age - it's the best indicator of tunnel health 5. Bind K8s components to WireGuard IPs to keep the control plane off the public internet