Skip to main content
RunBook Academy

VyOSXLI · WireGuardWireGuard

WireGuard keys — Curve25519 key pairs, preshared key, key management

Advanced⏱ ~18 mingenerate pki wireguard key-pairgenerate pki wireguard preshared-keyshow interfaces wireguardconfigurecomparecommitsaverollbackwg genkeywg pubkey

What you'll learn

  • Generate Curve25519 key pairs on VyOS with generate pki wireguard key-pair
  • Configure the optional per-peer preshared key
  • Explain where the private key lives on VyOS 1.4 and later, and what that changes about backups
  • Recognise the production failure modes of key handling

Prerequisites

Verified against VyOS 1.5.x LTS (circinus) · VyOS 1.4.x (sagitta) — legacy · FRRouting 10.x (VyOS 1.5) · Linux kernel 6.6 LTS (VyOS 1.5 base) · strongSwan 5.9.x (IPsec) · WireGuard 1.0.x (kernel module + userspace tooling) · 2026-08-19

Not yet marked complete on this device.

WireGuard uses Curve25519 for its asymmetric cryptography. Each peer has a private key (32 bytes, kept secret) and a public key (32 bytes, shared with peers). The private key never leaves the router; the public key is configured on the peer. The math guarantees that the private key cannot be derived from the public key (within practical computational limits), and the handshake protocol uses the keys to derive ephemeral session keys for each connection.

This lesson covers Curve25519 key pair generation on VyOS 1.5, the private/public key model, what the preshared key adds, where the private key actually lives once it is configured, and the production discipline of key rotation.

The Curve25519 key pair

Each WireGuard peer has two keys:

  • Private key — a 32-byte secret used to derive the peer’s identity. On VyOS 1.4 and later it is a configuration value: set interfaces wireguard wg0 private-key '...'.
  • Public key — a 32-byte value derived from the private key by Curve25519 scalar multiplication. It is shared with the far end, which configures it as peer NAME public-key '...'.

The relationship is one-way: the public key is derived from the private key, but the private key cannot be recovered from the public key within practical time. That asymmetry is what makes it safe to publish the public half.

An encoded key is 44 characters of base64 — 32 bytes plus padding:

AI9Y7cJ0kSg1r1tLZ4kX0aM2q7uB6nD8pE3fH5jK9wU=

private-key, peer NAME public-key and peer NAME preshared-key all carry the same value constraint on 1.5 — a plain base64 validator — so the CLI refuses a value that is not base64 the moment you type the set, with Key is not base64-encoded.

Be clear about what that constraint does not check on this release: it validates the encoding, not the length. A 44-character base64 string that decodes to the wrong number of bytes still passes the CLI and is handed to wg set, which is where it finally fails. Contrast both of these with the rules about which nodes must be present together — those live in the commit-time verify function and only fire when you commit.

Generating keys on VyOS 1.5

Key generation lives under generate pki, alongside the certificate commands in Part XLVII. There are two shapes: print the pair, or install it straight onto an interface.

# Print a key pair without touching the configuration
vyos@R1:~$ generate pki wireguard key-pair
Private key: AI9Y7cJ0kSg1r1tLZ4kX0aM2q7uB6nD8pE3fH5jK9wU=
Public key: rSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE=

The install form is the one to use in practice, because it puts the private half where it belongs without it ever passing through your clipboard:

vyos@R1:~$ configure
vyos@R1# run generate pki wireguard key-pair install interface wg0
1 value(s) installed. Use "compare" to see the pending changes, and "commit" to apply.
Corresponding public-key to use on peer system is: 'rSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE='

vyos@R1# compare
[edit interfaces wireguard wg0]
+private-key AI9Y7cJ0kSg1r1tLZ4kX0aM2q7uB6nD8pE3fH5jK9wU=

The command stages set interfaces wireguard wg0 private-key ... in your configuration session and prints the public half for you to send to the far end. Run it outside configuration mode and it cannot install anything, so it prints the command for you to paste instead:

vyos@R1:~$ generate pki wireguard key-pair install interface wg0
You are not in configure mode, commands to install manually from configure mode:
set interfaces wireguard wg0 private-key AI9Y7cJ0kSg1r1tLZ4kX0aM2q7uB6nD8pE3fH5jK9wU=

Read that message as an instruction rather than an error. The install variant only works from inside a configuration session because that is where a set can be staged.

The upstream tools

The reference implementation’s own tools still exist and still work; VyOS simply wraps them. Knowing them is useful when you are checking a key by hand or working on a Linux host that is the far end of the tunnel:

# Generate a private key
$ wg genkey
AI9Y7cJ0kSg1r1tLZ4kX0aM2q7uB6nD8pE3fH5jK9wU=

# Derive the public key from a private key (reads stdin)
$ echo "AI9Y7cJ0kSg1r1tLZ4kX0aM2q7uB6nD8pE3fH5jK9wU=" | wg pubkey
rSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE=

wg pubkey is the check that settles an argument: give it the private key from the configuration and it prints the public key that the far end must have configured. If that output does not match what the far end shows, you have found your problem without touching anything else.

Reading the public key back on VyOS does not require the private key at all:

vyos@R1:~$ show interfaces wireguard wg0 public-key
rSuuvkAuSoyqvogoA5OJjYBbJKKaYS13pWs4K69PTOE=

The preshared key

WireGuard supports an optional preshared key: a 32-byte symmetric value, configured identically on both ends of one peering, that is mixed into the handshake’s key derivation. It is a per-peer node, not an interface-wide one.

What it buys:

  • Resistance to a store-now-decrypt-later attack. An adversary who records the traffic today and later acquires the ability to break Curve25519 still cannot derive the session keys without the preshared secret, because it is symmetric and never crosses the wire.
  • Defence in depth against a stolen static key. The static private key alone is not enough to complete a handshake with a peer that also expects a preshared key.
# Print one
vyos@R1:~$ generate pki wireguard preshared-key
Pre-shared key: cIz1AsTjGdx58OMW6e7yW5KWSmSKGMGSajsKrpTkgmI=

# Or install it directly onto one peer
vyos@R1# run generate pki wireguard preshared-key install interface wg0 peer SITE-B

# Which is the same as writing it out by hand
set interfaces wireguard wg0 peer SITE-B preshared-key 'cIz1AsTjGdx58OMW6e7yW5KWSmSKGMGSajsKrpTkgmI='

Generate it once, on one side only, and carry it to the other. Both ends must hold the identical value. A peer with no preshared key configured behaves as though it had one of all zeroes, so “configured on one side only” is a mismatch, not a partial win: the handshake simply never completes, and neither router logs anything that names the cause.

Where the private key actually lives

On 1.4 and later the private key is a configuration value, which relocates the entire storage problem. There is no key file to chmod; there is a configuration that now contains a secret.

That means:

  1. show configuration prints it. So does show configuration commands. Use show configuration commands | strip-private for anything that leaves the router — a ticket, a diff for review, a paste to a vendor.
  2. Every configuration backup contains it, from the moment it is committed. That includes save to a file, the nightly config export, and any commit archive shipped off-box. Decide where those go and who can read them before you generate the key.
  3. The local commit archive keeps the old ones. The revisions listed by show system commit and restorable with rollback include the configurations from before a rotation, so a rotated-out key survives locally until those revisions age out. That is a feature for recovery and a liability for a key you rotated because it leaked.
  4. /config/config.boot is the file the whole thing lands in. Whoever can read that file has the router’s WireGuard identity, regardless of what the key file permissions used to be.
# What a reviewer should see instead of the key
vyos@R1:~$ show configuration commands | strip-private

# And the check that tells you whether a key is in there at all
vyos@R1:~$ show configuration commands | match private-key

Key rotation

Rotating a WireGuard key is a two-router change by construction, because the far end authenticates you by the public half. There is no revocation list and no grace period.

The in-place rotation, accepting a short outage:

# Step 1 - on R1, generate and stage the new private key
vyos@R1:~$ configure
vyos@R1# run generate pki wireguard key-pair install interface wg0
1 value(s) installed. Use "compare" to see the pending changes, and "commit" to apply.
Corresponding public-key to use on peer system is: 'kyxTG3RVQnG8mgi3oXIwBZBh45yNjHtYmojy4t1qr84='

# Step 2 - read the diff before you commit anything
vyos@R1# compare
vyos@R1# commit
# The tunnel drops here: R2 still expects the old public key.

# Step 3 - on R2, update the peer with R1's new public key
vyos@R2# set interfaces wireguard wg0 peer R1 public-key 'kyxTG3RVQnG8mgi3oXIwBZBh45yNjHtYmojy4t1qr84='
vyos@R2# commit
vyos@R2# save

# Step 4 - confirm, then save on R1
vyos@R1:~$ show interfaces wireguard wg0 summary
# latest handshake within the last couple of minutes, and transfer
# counters moving in BOTH directions
vyos@R1:~$ configure
vyos@R1# save

The outage lasts from the commit on R1 until the commit on R2 — seconds if both are prepared, minutes if the second operator has to be woken up. Do not commit step 2 until step 3 is typed and waiting.

Do not skip the compare in step 2. The install command reports 1 value(s) installed whether or not the node it wrote is the one you expected, and compare is the only thing between you and a rotation that quietly did not happen. What you want to see is a single line pair: the old private-key value leaving and the new one arriving, on wg0 and nothing else.

Configuration and validation

configure

# R1: identity and listening port
run generate pki wireguard key-pair install interface wg0
set interfaces wireguard wg0 address '10.10.10.1/30'
set interfaces wireguard wg0 port '51820'
set interfaces wireguard wg0 description 'tunnel to SITE-B'

# R1: the far end, as a named peer
set interfaces wireguard wg0 peer SITE-B public-key 'cVn4T2sM8xQ6yB1hJ0dR7kL3pW9zA5eG2uY8iO4nX1c='
set interfaces wireguard wg0 peer SITE-B address '198.51.100.20'
set interfaces wireguard wg0 peer SITE-B port '51820'
set interfaces wireguard wg0 peer SITE-B allowed-ips '10.10.10.0/30'
set interfaces wireguard wg0 peer SITE-B allowed-ips '192.168.20.0/24'
set interfaces wireguard wg0 peer SITE-B preshared-key 'cIz1AsTjGdx58OMW6e7yW5KWSmSKGMGSajsKrpTkgmI='

compare
commit
save

The peer name is a label you choose (SITE-B here). It is what compare shows a reviewer and what you type at 03:00 to delete the peer, so name it for the site rather than for its key.

Validation:

# The local public key - the value the far end must hold
vyos@R1:~$ show interfaces wireguard wg0 public-key

# The peers this interface knows, listed by public key
vyos@R1:~$ show interfaces wireguard wg0 peers

# The state that decides whether it works
vyos@R1:~$ show interfaces wireguard wg0 summary
# the peer (by public key), latest handshake a moment ago,
# transfer non-zero both ways

# The same data from the underlying tool, if you prefer it
vyos@R1:~$ sudo wg show wg0

A clean validation: show interfaces wireguard wg0 summary lists the peer with a recent latest handshake and transfer counters that are non-zero in both directions. Bytes out with zero bytes in means the far end is receiving and discarding — a key, a preshared key, or an allowed-ips problem on the other router.

None of those commands knows the peer names. On 1.5 every one of them is sudo wg show wg0 with a subcommand appended, and wg identifies peers only by their 44-character base64 public key, because that is the only identifier the protocol carries. SITE-B exists in the configuration and nowhere else. That is exactly what you want when you are matching keys across two routers — the output is already in the units of the comparison — but it means going from a key in the output back to a site takes one more step:

vyos@R1:~$ show configuration commands | match wireguard

Keep that in mind when you write the key inventory: on this release the inventory is not a convenience, it is the only mapping from what the router prints to what the site is called.

Production failure modes

The private key left with a configuration backup

The key is in config.boot, so it is in every backup, every config export, and every configuration pasted into a ticket. This is now the most likely way for a WireGuard key to leak, and it does not look like a key handling mistake at the time — it looks like normal change management.

Diagnostic: search your ticket system and config archive for private-key. Anything that matches is a key that must be treated as exposed.

Fix: rotate (procedure above), and make strip-private the default for anything that leaves the router.

Preshared key on one side only

One end has peer NAME preshared-key configured and the other does not. WireGuard substitutes an all-zero preshared key where none is set, so the two ends derive different handshake keys and the initiation is discarded without a reply.

Diagnostic: show interfaces wireguard wg0 summary on both ends shows the peer listed with its endpoint and allowed ips and no latest handshake line and no transfer line at all. wg show prints those two lines only once there has been a handshake and once a byte has moved, so there is no “never” to read — the absence is the signal. A capture on the WAN shows the initiation leaving one router and arriving at the other, with no response — which is what distinguishes this from a firewall drop, where nothing arrives at all.

Fix: configure the identical preshared key on both ends, or remove it from both. There is no third state.

Wrong public key configured for the peer

A truncated or transposed paste. The far end cannot authenticate the initiation and drops it silently; WireGuard deliberately does not answer packets it cannot authenticate.

Diagnostic: on each router run show interfaces wireguard wg0 public-key, and compare with the peer entry configured on the other. This is a two-router check and it cannot be done from one side.

Fix: correct the public key on the router that has it wrong.

Commit rejected: missing mandatory nodes

A peer is not usable until it has both a public key and an allowed-ips list, and the interface is not usable without a private key. The commit fails with, respectively, a missing mandatory public-key error naming the peer, a missing mandatory allowed-ips error naming the peer, and a private-key not defined error for the interface.

There is also a self-reference guard: configuring a peer whose public key belongs to this very interface is rejected with an error saying the peer has an identical public key to the interface. That one usually means the two routers’ key pairs were generated in the same place and got crossed.

Fix: read the error — each one names the node it wants.

Key not recoverable after a rebuild

The router is rebuilt from an image and the operator has no configuration backup. The old private key is gone; the far end still expects its public half.

Fix: generate a new key pair on the rebuilt router and update the far end, exactly as in a rotation. There is no way to recover the old identity without the configuration that contained it, which is the real reason the configuration backup matters here.

Rollback

# Take a restore point before touching keys
configure
save /config/pre-change-wg-keys-TICKET.conf

# ... make the change, read the diff, then commit ...
compare
commit-confirm 5
confirm
save

# If the tunnel does not come back, return to the previous revision
rollback 1
commit

# Or reload the file you saved
load /config/pre-change-wg-keys-TICKET.conf
commit
save

commit-confirm is the right tool for a key change made over the tunnel that the key protects: if the new key does not establish, the router reverts on its own and the session comes back without a console trip.

Production discipline

Cross-course references

  • Part XLI-01 (XLI-VyOS-WireGuard / WireGuard concept) covers the protocol and where the keys fit into the handshake.
  • Part XLI-03 (XLI-VyOS-WireGuard / peers) covers the named-peer tree that the public key is configured under.
  • Part XLVII-06 (XLVII-VyOS-MgmtPlane / PKI and cert rotation) covers generate pki for certificates and the wider rotation discipline.
  • Part LVI-05 (LVI-VyOS-Upgrade / upgrade rollback) covers the configuration backup discipline that now protects the key as well.

Quiz

Knowledge check · 4 questions

  1. Q1. On VyOS 1.5, which command generates a WireGuard key pair and puts the private half onto the interface?

  2. Q2. The preshared key in WireGuard is required for the tunnel to establish; if it is not configured, the handshake fails.

  3. Q3. An operator suspects R1's WireGuard private key has been exposed. What is the immediate response, and in what order must the two routers be committed?

    R1 runs wg0 with peers to R2 and R3. The key was generated on R1 and has never been copied anywhere, but a configuration backup from R1 was attached to a ticket several weeks ago and that ticket system is readable by contractors. Both tunnels are currently up. On 1.4 and later the private key is a configuration value, so the backup contains it.

  4. Q4. A junior operator is about to paste the output of `show configuration commands` from a WireGuard router into a public vendor support ticket. What is the exposure, and what should be pasted instead?

    The router runs VyOS 1.5 with wg0 configured. The operator wants help with an unrelated OSPF question and reaches for the full configuration because it is easier than extracting the relevant part. On this release the WireGuard private key and any preshared keys are configuration values, so they are in that output.

Passing score: 75%. Answers are checked in this browser.