Reported symptoms
SITE-A’s router lost a power supply and was replaced with an identical
spare during a planned window. The archived configuration was loaded onto
the new chassis, the operator read compare, saw no differences from the
pre-failure configuration, and handed back.
The site runs two WireGuard tunnels:
SITE-A 198.51.100.10 wg0 port 51820 -> SITE-B 198.51.100.20 tunnel 10.10.10.0/30
SITE-A 198.51.100.10 wg1 port 51821 -> SITE-C 203.0.113.20 tunnel 10.10.20.0/30
Fourteen hours later a nightly batch job at SITE-B failed and the incident opened. Both tunnels had been down since the swap.
What made it hard is what was working:
- The eBGP session to ISP-A came up on the replacement chassis and has been Established since. Whatever else is wrong, the WAN is not down and the router is reachable from the internet.
- The LAN forwards. Management SSH works. Nothing about the router looks freshly broken.
- SITE-B and SITE-C report nothing. Their peer configuration is untouched, their firewall counters show the encrypted UDP arriving and being accepted, and their logs are as empty as they always are.
- Both tunnels failed at the same moment, which reads as a router-wide or circuit-wide event rather than anything per-peer.
- The tunnel monitor — an ICMP probe of 10.10.10.2 — did go red at the swap. Its alert was suppressed for the maintenance window and the suppression was never lifted.
Evidence provided
vyos@SITE-A:~$ show interfaces wireguard wg0 summaryinterface: wg0
public key: ZF1DeQIGCVfpPnBLyVcmPh+KbxtaUShqXr9pwFHJ4j4=
private key: (hidden)
listening port: 51820
peer: cVn4T2sM8xQ6yB1hJ0dR7kL3pW9zA5eG2uY8iO4nX1c=
endpoint: 198.51.100.20:51820
allowed ips: 10.10.10.0/30, 192.168.20.0/24Illustrative output
There is no latest handshake line and no transfer line. Those are
printed only once a handshake has happened and once a byte has moved, so
this is an absence to read rather than a “never” to report.
vyos@SITE-A:~$ sudo tcpdump -ni eth0 'udp port 51820' -c 4 -vvIP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148
IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148
IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148
IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148Illustrative output
vyos@SITE-B:~$ sudo tcpdump -ni eth0 'udp port 51820' -c 4 -vvIP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148
IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148
IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148
IP 198.51.100.10.51820 > 198.51.100.20.51820: UDP, length 148Illustrative output
vyos@SITE-B:~$ show firewall ipv4 input filteripv4 filter input
rule 5 accept state established,related 412984 packets
rule 10 accept udp dport 51820 iif eth0 1174 packets
default drop 26 packetsIllustrative output
vyos@SITE-A:~$ show interfaces wireguard wg0 public-keyZF1DeQIGCVfpPnBLyVcmPh+KbxtaUShqXr9pwFHJ4j4=Illustrative output
vyos@SITE-B:~$ show interfaces wireguard wg0 peersrSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE=Illustrative output
Work the evidence before reading on
The capture pair is doing most of the work here, so read it carefully before reaching for an explanation.
- The encrypted UDP leaves SITE-A and arrives at SITE-B. Which hypotheses does that eliminate outright?
- SITE-B’s input filter counter is advancing on the WireGuard rule. Which further hypothesis does that eliminate?
- SITE-B receives well-formed packets on an open port and sends nothing back. What are the reasons a WireGuard peer declines to answer, and what would each of them require to be true in the configuration?
- No
preshared-keynode exists on any of the three routers. Cross one of those reasons off. - Both tunnels failed at the same instant. What do wg0 and wg1 have in common that a per-peer setting does not?
Before continuing: the far ends were not touched during the window. Name the thing that changed at SITE-A which would break every peer at once without breaking BGP, the LAN, or SSH.
Root cause
1. The identity moved, and the configuration diff was clean
On VyOS 1.4 and later the WireGuard private key is a configuration value:
set interfaces wireguard wg0 private-key ..., carrying the base64 key
itself rather than naming a file. That is why loading the archived
configuration onto replacement hardware restores the tunnel identity along
with everything else, and why the operator’s compare was clean and
correct.
The provisioning script then ran. It is the standard script for standing up a VyOS router, and among the things it does for a new router is:
run generate pki wireguard key-pair install interface wg0
run generate pki wireguard key-pair install interface wg1
The install form stages set interfaces wireguard wgN private-key ...
into the current configuration session and prints the corresponding public
key for the operator to send to the far end. On a new router that is
exactly right. On a restore it overwrites the identity that was just
restored, and the commit that follows makes it real.
The review happened before that step, not after it. That is the whole
defect: a real, competent compare read against a candidate configuration
that had not finished changing.
2. Silence is the designed response
SITE-B receives an initiation it cannot authenticate — the key it holds for this peer no longer corresponds to the key SITE-A now signs with — and discards it without a reply. There is no ICMP rejection, no log line and no counter that names the peer, because answering would tell an unauthenticated sender that they found a live WireGuard endpoint.
That produces the exact signature in the evidence:
flowchart TD
A["latest handshake absent"] --> B{"Encrypted UDP<br/>leaves SITE-A?"}
B -- "yes" --> C{"Arrives at SITE-B?"}
C -- "yes" --> D{"SITE-B replies?"}
D -- "no" --> E["SITE-B will not authenticate it"]
E --> F{"preshared-key<br/>configured anywhere?"}
F -- "no" --> G["Public key mismatch"]
Every branch not taken is a hypothesis the capture pair eliminated for free, which is why the capture on both ends is the second command to run and not the tenth.
3. The blast radius pointed away from the cause
One identity per interface, two interfaces re-keyed by one script, two tunnels dead. The team read “everything VPN is down and everything else is up” as evidence of something router-wide or circuit-wide, and spent the first hour on the WAN handoff that had just been recabled.
The BGP session was the fact that should have ended that line of enquiry in a minute: an Established eBGP session over the same physical path is a working WAN. It was known and it was not used, because it was filed as “the parts that still work” rather than as evidence.
Resolution
- Confirm the mismatch in both directions before changing anything: the public key on SITE-A against the peer list on SITE-B, and the public key on SITE-B against the peer list on SITE-A. Do the same for wg1 and SITE-C. Two tunnels, four comparisons.
- Establish whether the failed chassis is still in custody. If its storage has left the building, the archived private key must be treated as exposed and the restore-the-old-key option is unavailable. This is a security question and it decides the rest of the procedure, so answer it before choosing.
- Where the old key is still trustworthy: put the archived private key back on SITE-A. One commit on one router, both tunnels return together, and no far end has to change. Read
compareimmediately before committing, and commit undercommit-confirm. - Where it is not: publish the new public key to SITE-B and SITE-C and update
peer SITE-A public-keyat each. This is one commit per far end, each in its own window with its own operator, and the site stays down until the last one lands. Sequence it and say so in the incident channel rather than letting sites come back in an unplanned order. - On any far-end commit, use
commit-confirm. The operator editing SITE-B may be reaching SITE-B through a path that this tunnel carries, and a wrong key committed unattended removes both the tunnel and the session that would fix it. - Bring wg1 and SITE-C back explicitly rather than assuming the same action covered it. The second tunnel is the one that gets closed with the incident and discovered a week later.
- Lift the monitoring suppression as a named step, and confirm the probe is green rather than assuming it followed the tunnel.
Verification
- Both ends show a recent handshake.
show interfaces wireguard wg0 summaryon SITE-A and on SITE-B both report alatest handshakeinside the last couple of minutes. One end alone is not a result. - The transfer counters move in both directions on both routers. Bytes sent with a flat received counter is the signature of a tunnel that is still broken and is the state this incident started from.
- The identity comparison passes in both directions, for both tunnels. The public key printed here appears in the peer list there, and vice versa — four comparisons, recorded in the ticket.
- The underlay is confirmed by a counter rather than by a capture. The input-filter WireGuard rule counter advances on both routers; a capture proves the same thing but does not stay proved after you close it.
- The inner path works end to end. Reach the far tunnel address, then a host behind it, from a host on the LAN rather than from the router — the router has sources and routes a host does not.
- The check can fail. On a lab pair, set a deliberately wrong peer public key and confirm the comparison reports a mismatch and the handshake stops. A comparison that has only ever agreed has not been tested.
- Monitoring reflects reality. The per-peer handshake-age signal is present and green, the suppression is lifted, and forcing a tunnel down in the lab raises the alert.
Prevention
- Split provisioning from restoration in the build automation. The two paths differ on one question — does this router need a new identity — and the wrong answer is silent. Where one script serves both, generating key material must sit behind an explicit flag.
- Read
compareimmediately before the commit, not earlier in a sequence that keeps writing. A review performed against a candidate configuration that is still being modified is a review of something that will not be committed. - Keep the inventory WireGuard does not keep for you: public key against router against site. The install command prints the new public key; a value that does not match the inventory means the identity moved, and that is the moment to notice rather than fourteen hours later.
- Alert on handshake age per peer, not on a ping of the tunnel address. A probe that pings the far tunnel address is a proxy that goes red for a dozen reasons and tells you which one for none of them.
- Give every maintenance suppression an expiry. A suppression that must be removed by hand will eventually not be.
- Treat key rotation as a paired change with a stated order, planned into one window. The accidental version of this rotation is exactly the deliberate one performed at one end only, which is why the fix and the fault have the same shape.