# WireGuard Mesh Networking Across Any VPS in 15 Min

> Encrypted server-to-server networking between Hetzner, DigitalOcean, or any mix of providers: a manual WireGuard setup tutorial for 3+ node meshes.

David Viejo · Published 2026-03-12 · Updated 2026-10-23 · 13 min read

Canonical: https://temps.sh/blog/how-to-set-up-wireguard-mesh-between-servers

To connect servers across multiple cloud providers with encrypted private networking, WireGuard mesh is the fastest path: generate a Curve25519 key pair on each node, assign private IPs, and add peer entries pointing to every other node's public IP and port. The result is a multi-provider private LAN with roughly 1,011 Mbps throughput and a codebase small enough to audit in an afternoon.

WireGuard is a VPN protocol built into the Linux kernel since version 5.6. A mesh network takes it one step further — every server connects directly to every other, forming a private LAN that spans cloud providers and geographies. This guide walks through the manual setup, explains the topology tradeoffs, and shows how to eliminate the N-squared configuration burden.

> **TL;DR:** WireGuard's \~4,000-line codebase delivered about 3.9x OpenVPN's throughput in WireGuard's own benchmark (1,011 vs 258 Mbps, [wireguard.com/performance](https://www.wireguard.com/performance/)). For clusters beyond 3-4 servers, automated key exchange tools eliminate the per-node configuration burden. Temps bundles a userspace WireGuard client (boringtun) in its binary, but its relay mode can't be set up on the current release (v0.1.0-beta.56); Temps workers join over a private network you provide, such as the mesh this guide builds.

---

## Why Do You Need Private Networking Between Servers?

Cloud provider VPC networks can't cross vendor boundaries — your AWS instances can't natively reach Hetzner servers. As soon as you run servers at more than one provider, or a home lab next to a VPS, you need your own private network between them.

### Cloud VPCs Are Provider-Locked

AWS VPC peering works great between AWS accounts. But it can't reach your Hetzner box. GCP's networking won't talk to your DigitalOcean droplet. Every provider builds walled gardens around their own infrastructure.

If your architecture spans multiple providers — and it probably should, to avoid vendor lock-in — you need a networking layer that sits above all of them.

### Opening Ports Is a Security Nightmare

The alternative to private networking is opening firewall ports between servers. That means your PostgreSQL instance listens on a public IP. Your Redis cache is reachable from anywhere. Bots scan [all IPv4 addresses in under 45 minutes](https://isc.sans.edu/diary/30962). Every open port will be found.

IP allowlisting helps, but it's fragile. Cloud IPs change. You forget to update the list. One mistake, and your database is exposed to the internet.

### Plain TCP Is Unencrypted

Traffic between servers on the public internet travels in cleartext by default. Someone on the same network path could intercept your database queries, API responses, or session tokens. TLS on every internal service is possible but adds complexity and certificate management overhead.

### Service Mesh Is Overkill for Small Clusters

Istio and Linkerd solve the multi-service networking problem, but they're designed for hundreds of microservices running in Kubernetes. For 2-5 servers that just need to talk securely, a service mesh adds massive operational complexity you don't need. WireGuard handles this with a few config files.

---

## What Makes WireGuard the Right Tool for This?

WireGuard's entire codebase is approximately 4,000 lines of code, compared to OpenVPN's roughly 100,000 lines ([WireGuard whitepaper](https://www.wireguard.com/papers/wireguard.pdf)). That smaller surface area means fewer bugs, easier auditing, and significantly better performance.

### In-Kernel Performance

Since Linux 5.6, WireGuard runs as a kernel module. Packets don't bounce between userspace and kernel space — they're encrypted and forwarded entirely within the kernel. WireGuard's [published benchmark](https://www.wireguard.com/performance/) shows throughput of 1,011 Mbps compared to OpenVPN's 258 Mbps under the same conditions.

That's not a marginal improvement. It's about 3.9x faster. For inter-server traffic like database replication or API calls, that throughput matters.

### Cryptokey Routing

WireGuard's configuration model is refreshingly simple. Each node has a public/private key pair. You list which peers can connect and which IP ranges they're allowed to use. That's it. No certificate authorities, no TLS negotiation, no complex PKI infrastructure.

A peer is just a public key plus an allowed IP range. If the cryptographic identity doesn't match, the packet is dropped silently. There's no handshake to exploit if you're not authorized.

### UDP-Based and NAT-Friendly

WireGuard uses UDP, which means it works through most NAT configurations and firewalls without special rules. It also handles roaming natively — if a server's IP changes, WireGuard re-establishes the connection automatically on the next packet.

But how does this compare to alternatives in practice? Let's break down the options.

### Protocol Comparison

| Feature            | WireGuard            | OpenVPN                | IPSec/IKEv2     |
| ------------------ | -------------------- | ---------------------- | --------------- |
| Codebase size      | \~4,000 lines        | \~100,000 lines        | \~400,000 lines |
| Kernel integration | Yes (Linux 5.6+)     | No (userspace)         | Yes (varies)    |
| Protocol           | UDP                  | UDP or TCP             | UDP             |
| Encryption         | ChaCha20, Curve25519 | OpenSSL (configurable) | Configurable    |
| Configuration      | Simple key pairs     | Complex certificates   | Complex         |
| Throughput         | \~1,011 Mbps         | \~258 Mbps             | \~881 Mbps      |

_Throughput figures from [WireGuard whitepaper](https://www.wireguard.com/papers/wireguard.pdf) benchmarks on identical hardware._

---

## What's the Difference Between Kernel and Userspace WireGuard?

Kernel WireGuard handles packets entirely within the Linux kernel, achieving maximum throughput. Userspace implementations like boringtun — originally built by Cloudflare and open-sourced — run as regular processes, trading a small performance penalty for broader compatibility across platforms including macOS, containers, and older kernels.

### Kernel Module (Fastest)

The kernel module ships with Linux 5.6 and later. On older kernels, you can install it via DKMS. It processes packets at line speed with minimal CPU overhead.

Use kernel WireGuard when:

- Running on Linux 5.6+ servers
- Performance is critical (high-throughput database replication)
- You have root access and can install kernel modules

### Userspace (Most Portable)

Boringtun and wireguard-go are userspace implementations. They create a TUN device and handle encryption in a regular process. Performance is still excellent — far better than OpenVPN — but slightly below the kernel module.

Use userspace WireGuard when:

- Running inside Docker containers (no kernel module access)
- Running on macOS or Windows
- Running on managed infrastructure where you can't modify the kernel
- You need a single-binary deployment with no system dependencies

Userspace WireGuard (boringtun) is well-proven for inter-node communication. The throughput penalty compared to kernel WireGuard is barely noticeable for typical API and database traffic. The real advantage is operational simplicity — no kernel modules to install, no DKMS to maintain, no compatibility issues after kernel upgrades.

---

## How Do Mesh Topologies Work?

In a full mesh, every node connects directly to every other node, requiring N\*(N-1)/2 connections — so 3 nodes need 3 tunnels, but 10 nodes need 45. The topology you choose depends on your cluster size, traffic patterns, and tolerance for single points of failure.

### Hub-and-Spoke

```
         Node B
           |
           |
  Node A --Hub-- Node C
           |
           |
         Node D
```

All traffic routes through a central hub. Node B can reach Node C, but the packets travel through the hub first. This is the simplest to configure — each spoke only needs one peer entry (the hub), and the hub has entries for all spokes.

**Pros:** Easy to set up. Adding a new node means editing only the hub's config.
**Cons:** The hub is a single point of failure. All inter-node traffic doubles (in to hub, out from hub). The hub's bandwidth caps your cluster's throughput.

### Full Mesh

```
  Node A ---- Node B
    | \      / |
    |  \    /  |
    |   \  /   |
    |    \/    |
    |    /\    |
    |   /  \   |
    |  /    \  |
    | /      \ |
  Node D ---- Node C
```

Every node connects directly to every other. Traffic takes the shortest path. No single point of failure. If Node A goes down, B, C, and D still communicate freely.

**Pros:** Resilient. Lowest latency. No bandwidth bottleneck.
**Cons:** Configuration scales quadratically. Adding one node means updating every existing node's config.

### Partial Mesh

Group nodes by region or function. Nodes within a group form a full mesh. Groups connect through gateway nodes. This balances resilience against configuration complexity.

For most setups with 2-5 servers, full mesh is the way to go. The configuration burden is manageable, and you get the best performance and resilience. Beyond 10 nodes, you need automation.

---

## How Do You Set Up a Manual WireGuard Mesh?

A 3-node WireGuard mesh requires generating key pairs on each server, assigning private IPs, and configuring peer entries — a total of 6 peer entries across 3 config files. According to the [WireGuard documentation](https://www.wireguard.com/quickstart/), the entire setup process per node takes just a few commands.

A manual 3-node mesh takes 12-15 minutes if you know what you're doing, and about 30 minutes the first time. Most of that time is spent copying keys between servers.

### Step 1: Install WireGuard on Each Server

On Ubuntu/Debian:

```bash
sudo apt update && sudo apt install -y wireguard
```

On RHEL/Fedora:

```bash
sudo dnf install -y wireguard-tools
```

Verify the installation:

```bash
wg --version
```

### Step 2: Generate Key Pairs

Run this on **each** of your three servers:

```bash
wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey
chmod 600 /etc/wireguard/privatekey
```

Record the keys. You'll need the **public key** from every node on every other node.

```bash
cat /etc/wireguard/publickey
# Example output: aB3dE5fG7hI9jK1lM3nO5pQ7rS9tU1vW3xY5zA7bC=
```

### Step 3: Assign Private IPs

Pick a private subnet. We'll use `10.0.0.0/24`:

| Node     | Private IP | Public IP (example) |
| -------- | ---------- | ------------------- |
| Server A | 10.0.0.1   | 203.0.113.1         |
| Server B | 10.0.0.2   | 198.51.100.2        |
| Server C | 10.0.0.3   | 192.0.2.3           |

### Step 4: Create Config Files

**Server A** (`/etc/wireguard/wg0.conf`):

```ini
[Interface]
PrivateKey = <Server A private key>
Address = 10.0.0.1/24
ListenPort = 51820

[Peer]
# Server B
PublicKey = <Server B public key>
Endpoint = 198.51.100.2:51820
AllowedIPs = 10.0.0.2/32
PersistentKeepalive = 25

[Peer]
# Server C
PublicKey = <Server C public key>
Endpoint = 192.0.2.3:51820
AllowedIPs = 10.0.0.3/32
PersistentKeepalive = 25
```

**Server B** (`/etc/wireguard/wg0.conf`):

```ini
[Interface]
PrivateKey = <Server B private key>
Address = 10.0.0.2/24
ListenPort = 51820

[Peer]
# Server A
PublicKey = <Server A public key>
Endpoint = 203.0.113.1:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25

[Peer]
# Server C
PublicKey = <Server C public key>
Endpoint = 192.0.2.3:51820
AllowedIPs = 10.0.0.3/32
PersistentKeepalive = 25
```

**Server C** (`/etc/wireguard/wg0.conf`):

```ini
[Interface]
PrivateKey = <Server C private key>
Address = 10.0.0.3/24
ListenPort = 51820

[Peer]
# Server A
PublicKey = <Server A public key>
Endpoint = 203.0.113.1:51820
AllowedIPs = 10.0.0.1/32
PersistentKeepalive = 25

[Peer]
# Server B
PublicKey = <Server B public key>
Endpoint = 198.51.100.2:51820
AllowedIPs = 10.0.0.2/32
PersistentKeepalive = 25
```

### Step 5: Start the Tunnels

On each server, restrict the config file (it contains the private key), open UDP 51820 in the firewall, and bring the interface up:

```bash
sudo chmod 600 /etc/wireguard/wg0.conf
sudo ufw allow 51820/udp   # or the equivalent in your cloud firewall
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0
```

### Step 6: Verify Connectivity

From Server A:

```bash
ping -c 3 10.0.0.2
ping -c 3 10.0.0.3
```

Check the WireGuard status:

```bash
sudo wg show
```

You should see recent handshakes and transferred bytes for each peer. If a peer shows no handshake, double-check the endpoint IP, port, and firewall rules (UDP 51820 must be open).

---

## What Happens When You Need to Scale?

Adding node #4 to a 3-node mesh means editing config files on all 3 existing servers and reloading their WireGuard configuration (with `wg syncconf`, without dropping the interface). At 10 nodes, you're managing 45 individual peer entries. Every one of those entries is a key to generate, copy and eventually rotate by hand.

### The Math Gets Ugly

| Nodes | Connections | Config Changes to Add 1 Node |
| ----- | ----------- | ---------------------------- |
| 3     | 3           | Edit 3 files                 |
| 5     | 10          | Edit 5 files                 |
| 10    | 45          | Edit 10 files                |
| 20    | 190         | Edit 20 files                |

Every config change has to be applied on every node. A full `wg-quick down && wg-quick up` briefly drops existing connections, so on a production mesh use `wg syncconf` or `wg set` to add peers live. Even then, it is repetitive and easy to get wrong across 10 nodes.

### What Can Go Wrong

Manual key distribution is error-prone. Copy a public key incorrectly, and two nodes can't establish a tunnel. Forget to add a peer to one node, and you get asymmetric connectivity — A can reach D, but D can't reach A. These bugs are maddening to debug because WireGuard silently drops packets from unknown peers.

The real scaling issue isn't the number of config files — it's the blast radius of a mistake. In a 10-node mesh, one typo in a key can break connectivity for multiple nodes, and there's no centralized view to tell you where the problem is. You end up SSH-ing into every server and running `wg show` one by one. That's why centralized key exchange isn't a nice-to-have. It's a necessity.

### Automation Tools

Several projects solve this problem:

- **Tailscale** — SaaS coordination server, builds WireGuard mesh automatically, uses DERP relays for NAT traversal
- **Netmaker** — Self-hosted WireGuard mesh management with a web UI
- **Nebula** — Lighthouse-based overlay network (not WireGuard, but similar concept)
- **Headscale** — Open-source, self-hosted Tailscale control server

Each trades some flexibility for operational sanity. The question is how much control you want to give up.

---

## Can Temps Set Up WireGuard for You? (Not Yet)

As of v0.1.0-beta.56, Temps' WireGuard mode can't be set up end to end: relay mode needs a relay service to broker the key exchange, and Temps neither hosts a public relay nor ships a relay server. Join workers in direct mode over a private network you provide — for example the mesh from this guide, passing each node's `10.0.0.x` address as `--private-address`. From the next release, `temps join --pair` pairs a worker with the control plane over WireGuard without a relay. The rest of this section describes how the relay mode is designed, not something you can use today.

Temps embeds userspace WireGuard directly in its binary using boringtun (Cloudflare's open-source Rust implementation) via the `defguard_wireguard_rs` crate — no system packages and no kernel modules. By design, relay mode would set up an encrypted WireGuard tunnel between a worker node and the control plane with a single `temps join` command, but that mode is not usable on v0.1.0-beta.56. (In direct mode, for nodes that already share a private network, no tunnel is created and the control plane talks to the worker's private address directly.)

Temps is free to self-host (MIT / Apache 2.0 license), with no platform fee — you only pay for the server you already run it on. Temps Cloud, a hosted control plane for your own servers, is coming soon from $29/month.

### No System Packages Required (By Design)

Traditional WireGuard setup requires `wireguard-tools` and either a kernel module or userspace daemon. Temps compiles boringtun directly into the binary. In relay mode, `temps join` is designed to create a TUN device and handle encryption in-process, with nothing extra to install. That path needs a relay you operate yourself, and it does not work on v0.1.0-beta.56.

```bash
# Direct mode: worker has a private address reachable from the control plane (no tunnel)
temps join <control-plane-url> <join-token> --private-address <worker-ip>

# Relay mode: WireGuard tunnel, key exchange brokered by a relay (none is available on v0.1.0-beta.56)
temps join <cluster-id> <join-token> --relay-url https://relay.example.com
```

### Automatic Key Exchange (Relay Mode, Not Usable Yet)

Manual WireGuard means generating keys on each server and copying public keys everywhere. Relay mode is designed to handle this for you. When a worker joins, it generates a Curve25519 key pair using `x25519-dalek` (pure Rust, no `wg genkey` CLI needed) and sends its public key through your relay. The relay returns the control plane's public key, endpoint and the worker's assigned tunnel address. No SSH-ing between servers. No shared documents listing keys.

### Nodes Behind NAT

Many servers sit behind NAT — especially in cloud environments or home labs. With manual WireGuard, you need a publicly reachable endpoint on at least one side of every tunnel. Temps relay mode brokers the key exchange through a relay you run and point workers at with `--relay-url` (or `TEMPS_RELAY_URL`). There is no public relay: on v0.1.0-beta.56, `--relay-url` defaults to `https://api.temps.sh`, which is not a working relay, so a join without `--private-address` fails. Direct mode over a private network your nodes already share is the path that works today. See [WireGuard mode in the multi-node docs](https://temps.sh/docs/multi-node#wireguard-mode).

### How the Designed Relay Mode Compares to Manual Setup

| Aspect                    | Manual WireGuard               | Tailscale                                           | Temps relay mode (design; not usable on v0.1.0-beta.56)        |
| ------------------------- | ------------------------------ | --------------------------------------------------- | -------------------------------------------------------------- |
| Key generation            | `wg genkey` on each server     | Automatic                                           | Automatic (x25519-dalek)                                       |
| Key distribution          | Copy/paste between servers     | SaaS-managed                                        | API-based exchange                                             |
| Config files              | One per node, manually written | None                                                | None (embedded)                                                |
| Adding a node             | Edit all existing configs      | `tailscale up`                                      | `temps join` (direct mode over your own private network today) |
| NAT traversal             | Requires public endpoints      | DERP relays (SaaS)                                  | Would need a relay you run                                     |
| System dependencies       | wireguard-tools, kernel module | tailscaled daemon                                   | None (single binary)                                           |
| Self-hosted control plane | No                             | Headscale (separate project)                        | You run the relay yourself                                     |
| Pricing                   | Free                           | Personal free up to 6 users; Standard $8/user/month | Free self-host (Temps Cloud coming soon)                       |

With 5+ nodes, manual key distribution means touching every server each time you add one. Automated tools such as Tailscale or Netmaker reduce that to one command on the new node. Temps does not automate the mesh today; use one of them (or this guide's manual mesh) as the private network under Temps' direct-mode `temps join`.

---

## Frequently Asked Questions

### Is WireGuard faster than OpenVPN?

Yes, significantly. WireGuard achieves roughly 1,011 Mbps throughput compared to OpenVPN's 258 Mbps in WireGuard's own LAN benchmark ([wireguard.com/performance](https://www.wireguard.com/performance/)); treat it as vendor-published, not independent. The difference comes from WireGuard's in-kernel implementation on Linux 5.6+, which avoids the userspace-to-kernel context switching that slows OpenVPN down. For inter-server traffic, this gap translates to lower latency and less CPU overhead.

### Can WireGuard work behind a NAT?

WireGuard works behind NAT in most cases, but with a catch. The node behind NAT can initiate connections to peers with public endpoints, but peers can't initiate connections back unless keepalives are enabled. Set `PersistentKeepalive = 25` in your config to maintain the NAT mapping. For double-NAT situations where neither side has a public IP, you need a relay node or a coordination service that provides STUN/TURN-like functionality.

### How do I add a new node to a WireGuard mesh?

For a manual mesh, you need to generate a key pair on the new node, then edit the WireGuard config file on **every existing node** to add the new peer's public key and endpoint. After updating configs, apply the change on each server without dropping the interface, using `wg syncconf wg0 <(wg-quick strip wg0)` (or `wg set wg0 peer <pubkey> allowed-ips <ip>/32 endpoint <ip:port>` for a single peer). In a 10-node mesh, that means editing 10 files and restarting 10 tunnels — which is why automated tools like Tailscale or Netmaker exist.

### What's the difference between WireGuard and Tailscale?

WireGuard is a VPN protocol — it handles encrypted tunnels between two endpoints. Tailscale is a management layer built on top of WireGuard that automates key distribution, NAT traversal (via DERP relay servers), and mesh topology. Think of WireGuard as the engine and Tailscale as the car. You can drive the engine directly (manual config), or let a management layer handle the operational complexity. Tailscale is SaaS-hosted by default; self-hosted alternatives include Headscale and Netmaker.

### Does WireGuard support IPv6?

WireGuard fully supports IPv6. You can assign both IPv4 and IPv6 addresses to the WireGuard interface and include IPv6 ranges in `AllowedIPs`. This is useful for dual-stack deployments where internal services need to be reachable over both protocols. The configuration syntax is identical — just add an IPv6 address alongside the IPv4 one in your `Address` field.

### What does Temps use for WireGuard under the hood?

Temps uses `defguard_wireguard_rs` 0.9 (which wraps boringtun, Cloudflare's open-source Rust WireGuard implementation) for the in-process tunnel, and `x25519-dalek` 2.0 for Curve25519 key generation. This eliminates any dependency on the `wireguard-tools` system package or kernel module. The `temps-wireguard` crate is the only consumer of this stack — the join command in `temps-cli` calls it directly during node registration.

---

## Start Building Your Mesh

WireGuard gives you encrypted private networking between any servers, anywhere. For 2-3 nodes, the manual setup takes 15 minutes and works perfectly. Beyond that, the N-squared configuration problem will push you toward automation.

The core concept stays the same regardless of tooling: Curve25519 key pairs, UDP tunnels, cryptokey routing. Understanding the manual process makes you better at debugging any WireGuard-based tool, whether that's Tailscale, Netmaker, or an embedded solution.

Once your servers share a private network, such as the mesh from this guide, Temps can schedule containers across them: one `temps join` command in direct mode adds each worker to the cluster. Install Temps on the control plane with:

```bash
curl -fsSL https://temps.sh/deploy.sh | bash
```

If you'd rather keep a managed mesh, [securing a VPS with Tailscale](https://temps.sh/blog/secure-vps-with-tailscale) covers that route, and [building a multi-node Docker cluster without Kubernetes](https://temps.sh/blog/how-to-build-multi-node-docker-cluster-without-kubernetes) shows what runs on top.

For the full WireGuard protocol specification, see the [WireGuard whitepaper](https://www.wireguard.com/papers/wireguard.pdf). For Temps multi-node setup, check the [multi-node documentation](https://temps.sh/docs/multi-node) and [networking](https://temps.sh/docs/networking).

---

Source: https://temps.sh/blog/how-to-set-up-wireguard-mesh-between-servers · Index of all pages: https://temps.sh/llms.txt
