Originally published on kuryzhev.cloud
WireGuard key rotation usually gets planned the day after a laptop disappears from a taxi, when nobody can say which of the 40 peers on the hub belongs to it. The tunnel itself is simple. The operational picture around it (who holds which key, which address belongs to whom, what breaks if a key changes) is the part that rarely gets written down.
This checklist is for hub-and-spoke or partial-mesh deployments with enough peers that rotating by hand and from memory becomes risky. It assumes Linux hosts using wg and wg-quick, though the logic applies to other implementations.
Why this WireGuard key rotation checklist exists
WireGuard negotiates fresh session keys every couple of minutes, but the static keypair identifying each peer never changes on its own. There is no certificate expiry, no revocation list, and no built-in rotation. A peer's public key is its identity, so rotation is an operational process you have to design yourself.
Two properties make multi-peer rotation tricky. First, a given AllowedIPs range maps to exactly one peer on an interface, so you cannot keep the old and new key of the same peer active with the same address. Second, the hub's key is configured on every spoke, so rotating it touches the whole fleet at once.
A typical failure is a rotation that succeeds on the hub and is never finished on the spoke. The tunnel silently stops, and the only symptom is a handshake timestamp that never advances. The list below is meant to prevent that: plan, stage, verify, and keep a way back. Confirm details against the official WireGuard documentation and the wg(8) man page for your tool version.
The checklist
-
Build an inventory first. For every peer, record public key, assigned
AllowedIPs, owner, creation date, and whether a preshared key is used. A spreadsheet or a Git-tracked file works. Without this, you cannot tell which keys are stale. - Set a rotation trigger. Decide on a schedule (for example, every 6 to 12 months) plus event triggers: device loss, staff departure, suspected disclosure. Write both down.
- Generate keys on the peer itself. The private key should never travel. Only the public key (and, if used, a preshared key delivered over a separate secure channel) moves between machines.
-
Back up the current config. Copy the hub's config and the output of
wg showbefore touching anything. This is your rollback. - Pick a cutover pattern per peer. Because one address maps to one key, either swap atomically on the hub (remove old, add new in one command) or give the new key a temporary address and move traffic over.
- Rotate the spoke, then the hub entry. Apply the new private key (and preshared key, if used) locally, then apply the swap on the hub immediately after. Keep the gap between the two steps to seconds. If you manage the spoke over the tunnel itself, have out-of-band access ready, because the tunnel drops until both sides match.
-
Verify the handshake. Generate some traffic, since handshakes only happen when packets flow. Then check
wg show wg0 latest-handshakesfor the peer and test real application traffic, not just ping to the tunnel address. -
Persist the change. Runtime changes made with
wg setare lost on restart unless the config file is updated too. - Remove the old public key everywhere. Check the hub, any secondary hubs, and any spokes that peer directly with it.
- Update the inventory and archive the old private key securely, or destroy it. Record the rotation date.
The next block shows steps 3, 5 and 6 for a single spoke. The first part runs on the peer, the second on the hub.
# On the peer: umask keeps new files unreadable by other users
umask 077
wg genkey | tee /etc/wireguard/wg0.key.new | wg pubkey > /etc/wireguard/wg0.pub.new
wg genpsk > /etc/wireguard/wg0.psk.new # optional per-peer preshared key
# On the peer, at cutover: switch to the new private key and set the
# preshared key for the hub peer (omit the second line if no PSK is used)
wg set wg0 private-key /etc/wireguard/wg0.key.new \
peer HUB_PUBLIC_KEY preshared-key /etc/wireguard/wg0.psk.new
# On the hub: one command removes the old peer and adds the new one,
# so the AllowedIPs move without a window where neither key is configured.
# peer-b.psk must contain the same value as the peer's wg0.psk.new,
# copied over a separate secure channel.
wg set wg0 \
peer OLD_PUBLIC_KEY remove \
peer NEW_PUBLIC_KEY preshared-key /etc/wireguard/peer-b.psk allowed-ips 10.8.0.12/32
Treat the hub's own key as a separate project. One workable pattern is to bring up a second interface with the new key on a different ListenPort and its own tunnel subnet. Overlapping AllowedIPs on two interfaces produce conflicting routes. Then migrate spokes in waves, updating their Endpoint port and addresses, and retire the old interface when the last handshake moves over.
Commonly missed items
Watch out for a stale Endpoint or PersistentKeepalive assumption. A peer behind NAT that was reachable because of keepalives from the old config may go quiet if the new config omits them. The hub cannot initiate to it until the spoke sends a packet first.
Watch out for SaveConfig = true. With it enabled, wg-quick rewrites the config file from runtime state on shutdown. A hand-edited file can be overwritten by the old in-memory state. Pick one source of truth, and verify the behavior in the wg-quick man page for your version.
Other items that tend to slip through:
- Preshared keys. They are symmetric and per peer pair. Rotating the keypair but leaving a years-old preshared key in place defeats part of the purpose.
-
Private key file permissions. Config files containing
PrivateKeyshould be mode 600, owned by root. Backups and config-management copies often end up world-readable. - Keys in shell history, tickets, and chat. Public keys are fine to share. A private key pasted into a ticket must be treated as compromised.
- Firewall rules and DNS entries tied to the old key or address. If you used a temporary address during cutover, remove it afterward.
- Monitoring that keys off the public key. Dashboards and alerts labeled by old key will show a "missing" peer and a new unnamed one.
- Offline peers. A laptop that is off for a month will return with the old key. Decide in advance whether it gets re-enrolled or stays blocked.
Automation ideas
Rotation by hand is manageable for a handful of peers and becomes error-prone as the peer count grows. The goal of automation is not to hide the process but to make the inventory, the swap, and the verification repeatable. Start with the pieces that cause the most mistakes.
Staleness reporting. A scheduled job can list peers with no recent handshake, which catches both dead devices and half-finished rotations. For example:
# Report peers with no handshake in the last 10 minutes (timestamp 0 = never)
now=$(date +%s)
wg show wg0 latest-handshakes | while read -r key ts; do
if [ "$ts" -eq 0 ] || [ $((now - ts)) -gt 600 ]; then
echo "stale: $key" # feed this into your alerting or ticketing
fi
done
Note that an idle but healthy peer without keepalives can also show an old handshake, so tune the threshold to your traffic pattern.
Config generation. Keep peer definitions as data (YAML or similar) in Git and render wg0.conf from it with Ansible, Terraform, or a small script. Reviews of the rendered diff catch duplicate AllowedIPs before they reach the hub. Apply changes with wg syncconf wg0 <(wg-quick strip wg0) so existing sessions are not torn down; verify this against your installed version.
Secret handling. Store private keys in a secrets manager or generate them on the host and never export them. A timer-driven job on each spoke can generate a new keypair, submit only the public key to an enrollment API, and wait for confirmation before switching.
Staged rollout. Rotate a canary peer first, check the handshake, then proceed in batches with automatic halt on failed verification. For more infrastructure operations material, see kuryzhev.cloud.
Whatever you automate, keep the manual rollback path documented and tested. The backup from step 4 is only useful if someone has restored it before.
Top comments (0)