# Build a private mesh

There is no private network, no VPC and no SDN here. Every VM has one public
IPv4 and reaches the Internet and nothing of ours. If your machines need to
talk to each other over private addresses — a database that should not listen
on a public port, an NFS export, a cluster that assumes a flat internal
network — you build that yourself, and WireGuard is the short way.

Nothing of ours filters it. The one enforced rule is anti-spoof — a VM may
emit from its own IP and MAC — and a WireGuard peer does exactly that, sending
UDP from its own address with your traffic inside.

## The shape

Each VM gets a WireGuard interface and a private address on it; peers are
reached at their public addresses. Two or three machines are a full mesh
(every peer lists every other); past that, put one machine in the middle as a
hub and give it `AllowedIPs` covering the whole private range.

```
# on each VM
umask 077
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
```

`/etc/wireguard/wg0.conf`, one per machine:

```
[Interface]
Address    = 10.10.0.1/24        # unique per VM
ListenPort = 51820
PrivateKey = <this VM's private key>

[Peer]
PublicKey  = <the other VM's public key>
AllowedIPs = 10.10.0.2/32        # what this peer answers for
Endpoint   = peer2.example.com:51820
PersistentKeepalive = 25
```

```
systemctl enable --now wg-quick@wg0
wg show                                  # handshakes, per peer
ping 10.10.0.2
```

## What goes wrong

- **The UDP port is closed.** Open `51820/udp` inbound on every peer in your
  own firewall. A handshake that never completes is usually this:
  [Harden a VM](harden-your-vm.md).
- **`AllowedIPs` is a routing table, not an allow-list.** Whatever a peer
  lists is what it sends there. Two peers claiming the same range is a
  silently misrouted mesh.
- **MTU.** The tunnel costs overhead; leave `wg-quick` to set the MTU, and if
  you see large transfers stall while pings work, set `MTU = 1420` explicitly
  in `[Interface]`.
- **`Endpoint` was written as an IP address.** The address is bound to its VM
  for that VM's life and a [recreate](recreate-vm.md) gets a new one, so name
  peers by DNS. `wg-quick` resolves the endpoint at start; re-run it after a
  DNS change.

## Then close the public ports

Bind Postgres, Redis, NFS and anything else internal to the WireGuard address
alone, not `0.0.0.0`, and leave the public inbound policy denying everything
the Internet is not meant to reach. A mesh that does not close the public
ports has changed nothing.

## What it costs

Traffic between your VMs leaves each machine's own interface, so it is
metered as egress like anything else outbound; the tunnel does not change
that. Keep chatty internal traffic on the machine that generates it.
