WireGuard Multi-Peer Configuration and Zero-Downtime Key Rotation

devops-networking

When to Use This

WireGuard multi-peer operations and secure key illustration

WireGuard multi-peer configuration and zero-downtime key rotation become necessary the moment a simple point-to-point tunnel no longer fits your infrastructure. Consider a few concrete scenarios: you’re connecting three or more VPC-isolated environments where each node must reach the others directly; you’re running a hybrid cloud setup where on-premise servers communicate with cloud workloads through a central WireGuard gateway; or your security policy mandates periodic key rotation without service interruption — a common requirement in SOC 2 and ISO 27001 environments.

The single-peer WireGuard setup that works fine for a developer VPN breaks down quickly in a mesh topology. AllowedIPs routing becomes ambiguous, preshared keys per peer add coordination overhead, and any rotation approach that restarts the interface drops all active tunnels simultaneously. These are solvable problems, but they require deliberate configuration rather than copy-pasting a basic wg0.conf.

You need this approach when:

  • Three or more peers must exchange traffic through a central server or in a full mesh.
  • Each peer-to-peer relationship requires its own PresharedKey for layered symmetric encryption.
  • Key rotation must happen without dropping other active peer sessions.
  • NAT traversal is involved and PersistentKeepalive must be set per peer.

If you’re working with a single peer and have no rotation requirements, a standard wg-quick setup is sufficient. Everything beyond that benefits from the patterns described here. For related infrastructure automation approaches, the DevOps_DayS archive covers complementary tooling across Ansible, Terraform, and Kubernetes networking.

Setup

Start with a clean Debian or Ubuntu host designated as the WireGuard server. The server will hold the central wg0 interface and maintain [Peer] entries for every node in the mesh. Peer nodes each get their own interface configuration pointing back to the server’s public endpoint.

Install the WireGuard kernel module and userspace tools:

apt update && apt install -y wireguard wireguard-tools

Verify the kernel module loads correctly before generating any keys:

modprobe wireguard && lsmod | grep wireguard

Generate the server keypair. The private key stays on the server and never leaves; the public key is distributed to peers:

cd /etc/wireguard
wg genkey | tee server.key | wg pubkey > server.pub
chmod 600 server.key

For each peer, generate an independent keypair and a preshared key. The preshared key is symmetric — both sides of a peer relationship must use the same value, and it provides an additional layer of post-quantum resistance on top of the asymmetric Curve25519 handshake:

wg genkey | tee peer1.key | wg pubkey > peer1.pub
wg genpsk > peer1.psk
chmod 600 peer1.key peer1.psk

Repeat this for each peer. Keep the key files in /etc/wireguard/ and ensure the directory itself is not world-readable:

chmod 700 /etc/wireguard

Enable IP forwarding on the server node, which is required for multi-peer traffic routing between peers that don’t share a direct tunnel:

echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.conf
sysctl -p

Configuration

The configuration file at /etc/wireguard/wg0.conf must be owned by root and readable only by root. Any other permission allows unprivileged users to read private key material. The structure consists of one [Interface] block defining the server’s identity and one [Peer] block per connected node.

The PostUp and PostDown hooks handle iptables NAT rules that allow peers to forward traffic through the server to external networks. Without these, peers can reach each other on the WireGuard subnet but cannot route beyond it.

Below is the full automation script that generates server and peer keys, writes the complete wg0.conf, and includes a key rotation function that operates without restarting the interface. Read through it carefully — the rotation logic and the use of wg syncconf are the operationally significant parts:

#!/usr/bin/env bash
# WireGuard multi-peer setup + key rotation helper
# Target: /etc/wireguard/wg0.conf on server node

set -euo pipefail

WG_IFACE="wg0"
WG_DIR="/etc/wireguard"
SERVER_PORT=51820
SERVER_SUBNET="10.0.0.1/24"

# --- Generate server keys (skip if already exist) ---
if [[ ! -f "${WG_DIR}/server.key" ]]; then
  wg genkey | tee "${WG_DIR}/server.key" | wg pubkey > "${WG_DIR}/server.pub"
  chmod 600 "${WG_DIR}/server.key"
  echo "[+] Server keypair generated"
fi

SERVER_PRIV=$(cat "${WG_DIR}/server.key")

# --- Write base server config ---
cat > "${WG_DIR}/${WG_IFACE}.conf" <<EOF
[Interface]
Address    = ${SERVER_SUBNET}
ListenPort = ${SERVER_PORT}
PrivateKey = ${SERVER_PRIV}
PostUp     = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown   = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
EOF

# --- Add multiple peers ---
PEERS=("peer1" "peer2" "peer3")
PEER_IPS=("10.0.0.2" "10.0.0.3" "10.0.0.4")

for i in "${!PEERS[@]}"; do
  PEER="${PEERS[$i]}"
  PEER_IP="${PEER_IPS[$i]}"

  if [[ ! -f "${WG_DIR}/${PEER}.key" ]]; then
    wg genkey | tee "${WG_DIR}/${PEER}.key" | wg pubkey > "${WG_DIR}/${PEER}.pub"
    wg genpsk > "${WG_DIR}/${PEER}.psk"
    chmod 600 "${WG_DIR}/${PEER}.key" "${WG_DIR}/${PEER}.psk"
  fi

  PEER_PUB=$(cat "${WG_DIR}/${PEER}.pub")
  PEER_PSK=$(cat "${WG_DIR}/${PEER}.psk")

  cat >> "${WG_DIR}/${WG_IFACE}.conf" <<EOF

[Peer]
# ${PEER}
PublicKey    = ${PEER_PUB}
PresharedKey = ${PEER_PSK}
AllowedIPs   = ${PEER_IP}/32
EOF
  echo "[+] Peer ${PEER} (${PEER_IP}) added to config"
done

# --- Key rotation for a specific peer (example: peer1) ---
rotate_peer_key() {
  local PEER="peer1"
  echo "[*] Rotating keys for ${PEER}..."
  wg genkey | tee "${WG_DIR}/${PEER}.key.new" | wg pubkey > "${WG_DIR}/${PEER}.pub.new"
  wg genpsk > "${WG_DIR}/${PEER}.psk.new"

  OLD_PUB=$(cat "${WG_DIR}/${PEER}.pub")
  NEW_PUB=$(cat "${WG_DIR}/${PEER}.pub.new")

  # Apply new peer inline without full restart
  wg set "${WG_IFACE}" peer "${NEW_PUB}" \
    preshared-key "${WG_DIR}/${PEER}.psk.new" \
    allowed-ips "10.0.0.2/32"
  wg set "${WG_IFACE}" peer "${OLD_PUB}" remove

  mv "${WG_DIR}/${PEER}.key.new" "${WG_DIR}/${PEER}.key"
  mv "${WG_DIR}/${PEER}.pub.new" "${WG_DIR}/${PEER}.pub"
  mv "${WG_DIR}/${PEER}.psk.new" "${WG_DIR}/${PEER}.psk"
  chmod 600 "${WG_DIR}/${PEER}.key" "${WG_DIR}/${PEER}.psk"

  # Persist rotated config without interface restart
  wg-quick strip "${WG_IFACE}" | wg syncconf "${WG_IFACE}" /dev/stdin
  echo "[+] Key rotation complete for ${PEER}"
}

rotate_peer_key

The config file must be readable only by root. Set this immediately after writing it:

chmod 600 /etc/wireguard/wg0.conf

Bring up the interface and enable it at boot:

wg-quick up wg0
systemctl enable wg-quick@wg0

Refer to the official WireGuard quickstart documentation for platform-specific kernel module notes, particularly for older Ubuntu LTS releases where the DKMS package differs.

Examples

With the interface running, these are the runtime operations you’ll perform most frequently: adding a peer without restarting, verifying handshake state, and executing a key rotation.

Add a new peer at runtime without touching the config file or restarting the interface:

wg set wg0 peer <NEW_PEER_PUBKEY> \
  preshared-key /etc/wireguard/peer4.psk \
  allowed-ips 10.0.0.5/32

Verify the peer registered and check when its last handshake occurred. A handshake timestamp within the last 3 minutes indicates an active tunnel:

wg show wg0

The output shows each peer’s public key, endpoint, allowed IPs, latest handshake time, and transfer statistics. If a peer shows no handshake, the issue is almost always a firewall blocking UDP port 51820 or a mismatched public key.

Remove a peer at runtime — for example, during key rotation before adding the replacement:

wg set wg0 peer <OLD_PEER_PUBKEY> remove

After any runtime change, persist the current kernel state back to the config file using syncconf. This is the correct pattern — it does not restart the interface and does not disrupt other active peers:

wg-quick strip wg0 | wg syncconf wg0 /dev/stdin

The wg-quick strip command removes wg-quick-specific directives (like PostUp/PostDown) that the lower-level wg syncconf command cannot process. Piping through stdin avoids writing an intermediate temp file. This combination is the standard zero-downtime sync pattern.

For a peer behind NAT that cannot receive incoming connections, add PersistentKeepalive to its [Peer] block on the server side. This instructs the server to send keepalive packets every 25 seconds, maintaining the NAT mapping:

[Peer]
# peer2-behind-nat
PublicKey           = <PEER2_PUBKEY>
PresharedKey        = <PEER2_PSK>
AllowedIPs          = 10.0.0.3/32
PersistentKeepalive = 25

Common Mistakes

These are the failure modes that appear consistently when WireGuard multi-peer deployments break in production.

Setting AllowedIPs = 0.0.0.0/0 on a server-side peer entry. This is correct on the peer’s own client config when you want to route all traffic through the tunnel. On the server’s wg0.conf, it creates a routing loop: the server tries to route all outbound traffic back through the WireGuard interface, which immediately breaks internet connectivity for the server itself and for all other peers. Server-side peer entries should always use the peer’s specific /32 address.

PresharedKey mismatches after rotation. The preshared key is symmetric — both the server’s [Peer] block and the peer’s own [Peer] block pointing back to the server must use the identical value. During rotation, if you update the server’s entry but the peer hasn’t yet received its new PSK, the handshake fails silently. WireGuard does not log “wrong PSK” — it simply doesn’t complete the handshake. The symptom is a peer that shows no handshake in wg show despite correct public keys. Always coordinate PSK updates atomically: update both sides before removing the old peer entry.

Rotation race conditions. The script above adds the new peer entry before removing the old one. This is intentional. If you remove the old entry first and then fail partway through adding the new one, the peer loses connectivity entirely. The brief window where both old and new public keys exist for the same AllowedIPs is harmless — WireGuard matches on public key, not IP, for the handshake. Remove the old entry only after confirming the new handshake is established.

Forgetting to run wg syncconf after runtime changes. wg set modifies the live kernel state but does not write to wg0.conf. After a server reboot, all runtime-added peers disappear. Always follow any wg set operation with a syncconf call and verify the config file reflects the current state.

Incorrect file permissions on wg0.conf. If wg0.conf is world-readable (permissions 644), any local user can read the server’s PrivateKey and all peer PSKs. The file must be 600 and owned by root. Some distributions’ package installers do not enforce this automatically — verify it explicitly after initial setup and after any automated config regeneration.

Not accounting for the PostUp interface name. The %i placeholder in PostUp/PostDown expands to the interface name (wg0). If you rename the interface or use multiple WireGuard interfaces, verify the iptables rules reference the correct outbound interface alongside %i. A common error is hardcoding eth0 in the MASQUERADE rule on a host where the default route is on ens3 or a bond interface — NAT silently fails and peers can reach the WireGuard subnet but not external addresses.

Leave a Reply

Your email address will not be published. Required fields are marked *

Support us · 💳 Monobank