NoctisNode
← GuidesNetworking2026-10-239 min

Build a Private VPS Cluster with a WireGuard Mesh

Connect VPS nodes into a private cluster with a WireGuard full mesh: one address plan, one [Peer] per node, /32 AllowedIPs, a strict firewall and services bound to wg0.

Short answer: to turn several VPS into a private cluster, give each VPS node a WireGuard interface with an address from one private subnet, add every other node as a `[Peer]` with its public `Endpoint` and a `/32` in `AllowedIPs`, allow UDP 51820 only from your own nodes, and bind databases and internal services to the `wg0` address instead of the public IP. Up to roughly ten nodes this is easy to maintain by hand or with a template; beyond that, use tooling that generates and distributes the configs.

This guide assumes you already know the basics from the WireGuard VPN guide and have hardened SSH on every node as in the SSH hardening guide; to reach nodes through a bastion without agent forwarding, see the StrictModes and agent forwarding guide.

Addressing plan for the VPN nodes

Pick one private range that does not collide with anything else you use (home LAN, Docker, Kubernetes pod ranges). A `/24` is enough for a small cluster and keeps addresses readable. Assign each node a fixed address and keep the plan in version control, because every config is derived from it.

NodeRolePublic IPwg0 addressListenPort
node1app203.0.113.1110.88.0.1/2451820
node2app203.0.113.1210.88.0.2/2451820
node3database203.0.113.1310.88.0.3/2451820
node4monitoring203.0.113.1410.88.0.4/2451820

Per-peer config on each VPS node

In a full mesh every node talks directly to every other node, so each config has one `[Interface]` and N-1 `[Peer]` sections. Four nodes means six tunnels in total; ten nodes means 45. Every node has a public IP, so `PersistentKeepalive` is usually not needed; the details are in the keepalive and PresharedKey guide.

  1. 01.On each node run `apt install wireguard`, then `umask 077; wg genkey | tee /etc/wireguard/wg0.key | wg pubkey > /etc/wireguard/wg0.pub`.
  2. 02.Collect all public keys and the address plan in one place.
  3. 03.Optionally generate one `wg genpsk` key per node pair for a post-quantum hedge.
  4. 04.Write `/etc/wireguard/wg0.conf` on each node as shown below.
  5. 05.Run `systemctl enable --now wg-quick@wg0` and check `wg show` for a recent handshake with every peer.
# /etc/wireguard/wg0.conf on node1
[Interface]
Address = 10.88.0.1/24
ListenPort = 51820
PrivateKey = <node1 private key>

[Peer]
# node2
PublicKey = <node2 public key>
Endpoint = 203.0.113.12:51820
AllowedIPs = 10.88.0.2/32

[Peer]
# node3
PublicKey = <node3 public key>
Endpoint = 203.0.113.13:51820
AllowedIPs = 10.88.0.3/32

[Peer]
# node4
PublicKey = <node4 public key>
Endpoint = 203.0.113.14:51820
AllowedIPs = 10.88.0.4/32

How AllowedIPs works in a mesh

`AllowedIPs` does two jobs at once. Outbound, it is a routing table: packets for `10.88.0.3` go to the peer that lists `10.88.0.3/32`. Inbound, it is an access list: a packet arriving from that peer is accepted only if its source address is inside that peer's `AllowedIPs`. That is why each peer gets exactly its own `/32`, not the whole `/24`.

The `/24` on the `Address` line only creates the kernel route for the subnet on `wg0`; WireGuard then picks the right peer per destination.

  • ▹Two peers must never share an overlapping `AllowedIPs` range on the same interface; the last one wins.
  • ▹If a node also routes a private subnet (for example Docker on node3), add that subnet to node3's `AllowedIPs` on the other nodes.
  • ▹Do not put `0.0.0.0/0` in a mesh peer unless you really want all traffic to leave through that node.

Firewall: only your nodes reach the tunnel

WireGuard does not answer unauthenticated packets, but limiting UDP 51820 to known node IPs still reduces noise. Inside the tunnel, open only the ports each service needs. This nftables example for Debian 12/13 and Ubuntu 24.04 goes in `/etc/nftables.conf`; load it with `nft -f /etc/nftables.conf` and enable `nftables.service`. Keep an open SSH session while testing.

table inet filter {
  set mesh_peers {
    type ipv4_addr
    elements = { 203.0.113.11, 203.0.113.12, 203.0.113.13, 203.0.113.14 }
  }
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif lo accept
    meta l4proto { icmp, ipv6-icmp } accept
    tcp dport 22 accept
    udp dport 51820 ip saddr @mesh_peers accept
    iifname "wg0" ip saddr 10.88.0.0/24 tcp dport { 5432, 6379, 9100 } accept
  }
}

Bind services to the wg interface

A firewall is one layer; the second is not listening on the public address at all. Bind internal services to the node's `wg0` address so a firewall mistake does not expose them. Because `wg0` must exist before the service binds, order the units with a systemd drop-in, or allow binding to addresses that are not up yet with `net.ipv4.ip_nonlocal_bind = 1`.

# PostgreSQL on node3: postgresql.conf
listen_addresses = '10.88.0.3'
# pg_hba.conf
host  all  all  10.88.0.0/24  scram-sha-256

# Redis: /etc/redis/redis.conf
bind 10.88.0.3

# systemctl edit postgresql  (repeat for other services)
[Unit]
[email protected]
[email protected]

Note · Check the result with `ss -tlnp`: internal services should show `10.88.0.x`, never `0.0.0.0` or the public IP.

Scaling: when to switch to tooling

A hand-written mesh is fine while the node count is small and changes are rare. Each new node means editing every existing config, so the work grows quadratically. Once that becomes a chore, generate the configs instead of writing them.

On NoctisNode, all new VPS nodes are deployed in FI-1 (Finland), each with a dedicated IPv4 and unmetered 1 Gbps port, and the hourly billing plus the agent API (with an auto-destroy TTL) make it cheap to add and remove test nodes. Plans start at the Wisp tier; see VPS plans.

Cluster sizeApproach
2–5 nodesHand-written configs, addressing plan in a text file
5–15 nodesAnsible or another config tool templating wg0.conf from one inventory
15+ nodes or frequent churnA mesh manager that distributes keys and peers automatically, or a hub-and-spoke design

FAQ

QHow do I connect multiple VPS into a private network?

AInstall WireGuard on each VPS, give each node an address from one private subnet, and list every other node as a peer with its public endpoint and a /32 AllowedIPs. Then bind internal services to the WireGuard address.

QDo VPN nodes in a WireGuard mesh need PersistentKeepalive?

AUsually not, because every VPS node has a public IP and can be reached directly. Add `PersistentKeepalive = 25` only for a node that sits behind NAT.

QWhat should AllowedIPs be for each VPS node?

AEach peer gets only its own WireGuard address as a /32, plus any subnet that node routes. Using the whole cluster subnet for every peer causes overlapping routes and breaks the mesh.

QHow many nodes can a WireGuard mesh have?

AWireGuard itself handles many peers, but a hand-maintained full mesh becomes tedious after about ten nodes because every node needs every other node's config. Beyond that, generate configs with automation or use a mesh manager.

Build a Private VPS Cluster with a WireGuard Mesh · NoctisNode