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.
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
51820/udp inbound on every peer in your
own firewall. A handshake that never completes is usually this:
Harden a VM.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.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 gets a new one, so name
peers by DNS. wg-quick resolves the endpoint at start; re-run it after a
DNS change.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.
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.