SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Add a WireGuard peer without restarting the VPN

Add a new WireGuard client with wg syncconf and wg-quick strip so connected peers stay up, plus wg set, wg-quick save, AllowedIPs collisions and PostUp rules.

What you are doing

To add a WireGuard peer without restarting the VPN, append a [Peer] block to /etc/wireguard/wg0.conf and load the difference into the running interface with one line:

sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'

Every client that was connected stays connected. Nothing is torn down and no session key is lost. The rest of this guide walks through the second-client workflow from key generation to the first handshake, explains why that one line works, shows the one-off alternative with wg set, and covers the AllowedIPs mistake that quietly breaks a client you did not touch.

The commands were written for Ubuntu 24.04, which ships wireguard-tools 1.0.20210914 as of September 2026 (wg --version prints the version). The server is the one from the WireGuard VPN install guide: interface wg0 on 10.8.0.1/24, listening on UDP 51820, with one laptop already connected as 10.8.0.2/32. The new client is a phone, and it gets 10.8.0.3/32.

Why wg-quick down and up kicks every client

wg-quick down wg0 deletes the interface. Deleting the interface deletes every peer, and deleting a peer throws away its session keys. wg-quick up wg0 then creates a fresh interface holding fresh peers with no session at all.

The connected clients do not know this happened. A client keeps encrypting traffic with a session key the server no longer holds. The server cannot authenticate those packets, so it drops them without replying, which is what WireGuard does with every packet it cannot authenticate. The client only starts a new handshake after it has sent data and waited about 15 seconds with no reply, which is the protocol's keepalive timeout plus its rekey timeout. Until that handshake completes, everything through the tunnel stalls. PersistentKeepalive does not help here. A keepalive is not counted as data, so it does not start that 15-second timer, and an idle client reconnects only when it next sends real traffic.

So every active client stalls for roughly 15 seconds and then reconnects. PostDown and PostUp also run in between, so the firewall rules are deleted and added back. On a VPN with one user that is an annoyance. On a VPN carrying an SSH session or a call, it is an outage you caused in order to add a phone.

Generate the second client's keys

Generate the key pair where the private key will live. For a laptop, run this on the laptop. For a phone, generate on the server, put the key into a config, show it as a QR code, and delete the file afterwards.

umask 077
wg genkey | tee client2.key | wg pubkey > client2.pub
wg genpsk > client2.psk
cat client2.pub

umask 077 before tee makes the files readable by you alone. wg genkey prints a 32-byte private key as 44 base64 characters, and wg pubkey derives the matching public key from standard input. The pre-shared key is optional. It adds a symmetric secret to the handshake, and both sides must hold the same one.

The server only ever needs client2.pub and, if you use one, client2.psk. The private key never goes into the server config. Paste it into PublicKey by mistake and the block parses fine, because a private key is also 32 bytes, but the server is then waiting for a client that does not exist, and the real client's handshake is dropped in silence.

Pick a free tunnel address

Every peer needs its own /32 inside 10.8.0.0/24. Ask the running interface which addresses are taken:

sudo wg show wg0 allowed-ips

It prints one line per peer: the public key, a tab, then that peer's allowed addresses. With one laptop connected you see a single line ending in 10.8.0.2/32. 10.8.0.1 is the server itself. So the next free address is 10.8.0.3.

Grep the file as well, because a peer that is in wg0.conf but has not been loaded yet does not appear in wg show:

sudo grep AllowedIPs /etc/wireguard/wg0.conf

Append the Peer block to wg0.conf

sudo tee -a /etc/wireguard/wg0.conf > /dev/null <<'EOF'

[Peer]
# phone, added 2026-09-20
PublicKey = <contents of client2.pub>
PresharedKey = <contents of client2.psk>
AllowedIPs = 10.8.0.3/32
EOF
sudo tail -n 6 /etc/wireguard/wg0.conf

tee -a appends. Plain tee without -a overwrites the file, and you lose the [Interface] section and every other peer. The comment line is safe: wg-quick strip passes comments through and wg ignores them. tee -a keeps the file's existing mode, so it stays 600. Confirm with ls -l /etc/wireguard/wg0.conf if you are not sure.

AllowedIPs on the server side is one /32: the single tunnel address this client may use as a source, and the single address the server will send to this peer. The 0.0.0.0/0 you may have seen elsewhere belongs in the client's config, where it means "send everything through the tunnel".

Add the WireGuard peer without restarting wg0

sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'
sudo wg show wg0

Three things happen in that first line, and each one has a reason.

wg-quick strip wg0 reads /etc/wireguard/wg0.conf and prints it with the keys that only wg-quick understands removed from the [Interface] section: Address, DNS, MTU, Table, PreUp, PostUp, PreDown, PostDown and SaveConfig. Everything else, including comments and every [Peer] block, passes through unchanged. Run sudo wg-quick strip wg0 on its own once and read what comes out.

Those keys have to go because wg does not accept them. wg configures the kernel interface and nothing else. It knows PrivateKey, ListenPort and FwMark for the interface, and PublicKey, PresharedKey, AllowedIPs, Endpoint and PersistentKeepalive for a peer. Addresses, DNS servers, routes and hooks are the job of wg-quick, which is a shell script around wg. Feed wg the raw file and it stops at the first line it does not know:

Line unrecognized: `Address=10.8.0.1/24'
Configuration parsing error

Nothing has been applied at that point. wg parses the whole file before it touches the interface, so a parsing error never leaves the interface half changed.

<( ... ) is bash process substitution. It runs the command inside and hands wg a path such as /dev/fd/63 that reads the output. wg syncconf wants a filename, and this gives it one without writing the stripped config, private key included, to disk. It is also the reason for the sudo bash -c '...' wrapper. sudo wg syncconf wg0 <(wg-quick strip wg0) looks equivalent but fails with fopen: No such file or directory, because sudo closes the extra file descriptor behind /dev/fd/63 before it starts wg. Running the whole line inside one root shell keeps the descriptor and the command together. The wrapper must be bash, not sh. On Ubuntu and Debian sh is dash, which does not implement process substitution and answers Syntax error: "(" unexpected.

wg syncconf does the actual work. Its sibling wg setconf replaces the whole configuration: it removes every peer and adds back the ones in the file, which destroys every session. wg syncconf first reads the live configuration, finds the live peers that are missing from the file and marks only those for removal, and then applies the file without the replace-everything flag. Peers that already exist are updated in place and keep their session keys, because removing a peer is what clears its session. An unchanged private key is skipped by the kernel too, so the interface's own identity is not disturbed. That is the whole trick, and it is why the wg-quick manual documents exactly this line as the way to reload a config without disrupting active sessions.

Check that the peer arrived

sudo wg show wg0 now lists a second peer: block with allowed ips: 10.8.0.3/32 and, if you set one, preshared key: (hidden). It has no endpoint:, no latest handshake: and no transfer: line yet. Those appear after the client's first handshake. The laptop's latest handshake is unchanged, which is the proof that its session survived. A peer that is missing here means syncconf rejected the file, and it will have printed why on the line above.

Write the client config

[Interface]
PrivateKey = <contents of client2.key>
Address = 10.8.0.3/32
DNS = 10.8.0.1

[Peer]
PublicKey = <server public key, from /etc/wireguard/server.pub>
PresharedKey = <contents of client2.psk>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

Address must be the same /32 the server lists for this peer. If the client says 10.8.0.4 and the server says 10.8.0.3/32, every packet the client sends carries a source address the server does not allow for that peer, and the server drops it after a successful handshake. For a phone, sudo apt install -y qrencode and then qrencode -t ansiutf8 < client2.conf prints a QR code the WireGuard app can scan. Delete client2.conf and client2.key from the server once the phone has them.

Bring the client up and watch the server:

sudo wg show wg0
ping -c 3 10.8.0.3

The new peer now has an endpoint: line with the phone's public address, a latest handshake: a few seconds old, and rising transfer: counters. If the handshake is there but names do not resolve on the phone, the DNS line is where to look, and the DNS-over-WireGuard fixes cover each case. If you would rather click than edit a file for every new phone, wg-easy runs WireGuard in Docker with a web UI that creates the peer and its QR code, and it applies the change with the same syncconf line under the hood.

The one-off route: wg set, then wg-quick save

Sometimes you want to change one thing right now and decide later whether to keep it. wg set talks to the live interface directly:

sudo wg set wg0 peer "$(cat client2.pub)" preshared-key client2.psk allowed-ips 10.8.0.3/32
sudo wg show wg0 allowed-ips

That adds the peer immediately, with the same no-disruption property as syncconf, and it does not touch wg0.conf. The interface forgets it at the next wg-quick down or reboot. wg addconf wg0 phone.conf is the same idea with a file holding only the new [Peer] block, and it has the same persistence problem. Two details bite with wg set.

allowed-ips replaces the peer's list, it does not add to it. wg set wg0 peer <key> allowed-ips 192.168.10.0/24 on a peer that had 10.8.0.3/32 leaves it with only the LAN range, and the client's own tunnel address stops working. Write the full list every time. wireguard-tools 1.0.20250521 and later, the version Ubuntu 26.04 ships, accept a + or - prefix on an address for an incremental change on a recent kernel, but the 1.0.20210914 on Ubuntu 24.04 does not.

To keep a wg set change, write the live state back to the file:

sudo wg-quick save wg0
sudo cat /etc/wireguard/wg0.conf

save runs wg showconf wg0, puts the wg-quick keys from the old file back on top, and overwrites wg0.conf with the result. Read the file afterwards, because it is not the file you wrote. Comments are gone. Peers may come out in a different order. Every peer that has connected gains an Endpoint = line holding the address it last connected from, taken from the live state. SaveConfig = true in [Interface] makes wg-quick down do this on its own, which is why the install guide leaves it unset: with it on, a hand edit made while the interface is up is overwritten by the live state at the next down, and nothing tells you.

For a server you plan to keep, treat wg0.conf as the source of truth. Edit it and sync it. Keep wg set for experiments. Removing a client works the same way in reverse: sudo wg set wg0 peer <public key> remove cuts it off now, and deleting its block from the file keeps it off, because the next syncconf applies the file.

Why a duplicate AllowedIPs breaks a client you did not touch

Give the new peer an address another peer already has, and the other peer stops working. No error is printed anywhere. This is the most common way adding a second client breaks the first one.

The cause is cryptokey routing, the table inside the interface that maps each allowed address to exactly one peer. Adding 10.8.0.2/32 to a second peer moves that entry. The laptop's line in sudo wg show wg0 allowed-ips then reads (none). Outbound, packets for 10.8.0.2 now go to the phone. Inbound, packets from the laptop arrive with source 10.8.0.2, an address that is no longer in the laptop's list, so they are dropped. The laptop's handshake still completes, because the handshake is checked against its key and not against its address. So wg show reports a fresh latest handshake for a peer that cannot pass a single packet. That combination, handshake fine and no traffic, is the signature of a collision.

With syncconf the file order decides: peers are applied in the order they appear, so the peer written later in the file wins, and it wins again on every sync. With wg set, the command that ran last wins. The fix is to give one of them a different /32 and sync again, then confirm with wg show wg0 allowed-ips that every peer has exactly one line and no line says (none).

Routes: the one thing syncconf does not do

wg-quick up does two things per peer that wg syncconf never does. It adds a kernel route for each AllowedIPs range not already covered by the interface address, and it runs the hooks. For a client that owns a single /32 inside 10.8.0.0/24, no route is needed, because the server's own 10.8.0.1/24 connected route already covers it. That is why the workflow above needs nothing more.

It matters when a peer carries a network behind it. A home router peer with AllowedIPs = 10.8.0.5/32, 192.168.10.0/24 needs the server's kernel to know that 192.168.10.0/24 is reached through wg0. Cryptokey routing decides which peer gets a packet once it is inside wg0. The kernel routing table decides whether the packet enters wg0 at all. After a syncconf that adds such a peer, add the route by hand:

sudo ip route add 192.168.10.0/24 dev wg0
ip route show dev wg0

The next wg-quick up adds the same route itself from the file, so reboots stay correct. The full setup, including the return path on the home side, is in routing to your home LAN over WireGuard.

What PostUp and PostDown actually do

The hook lines appear in almost every WireGuard tutorial, and most readers copy them without knowing what they do. They matter here for one reason: they run at up and down only, and syncconf never runs them. That is fine when you add a peer, because the rules are about the interface, not about individual peers.

A typical server [Interface] carries this pair:

PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o enp1s0 -j MASQUERADE

wg-quick runs each hook through bash, PostUp after the interface is up and PostDown after it is gone, with %i replaced by the interface name. The two commands do this:

  • iptables -A FORWARD -i wg0 -j ACCEPT appends a rule to the FORWARD chain: a packet that arrived on wg0 and is addressed to some other host may pass through this server. On a box where the FORWARD policy is DROP, which Docker and ufw both set, every client packet bound for the internet is dropped without it. On a bare Ubuntu with the default ACCEPT policy this rule changes nothing.
  • iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE rewrites the source address of packets leaving through enp1s0 to the server's own public IP. Replies then come back to the server, which maps them to the client. Without it the client's packets leave with source 10.8.0.3, an address the internet cannot route back to, so ping 10.8.0.1 works and ping 1.1.1.1 times out.

PostDown deletes the same two rules with -D. The rule text after -D must match the -A line exactly, or iptables answers Bad rule (does a matching rule exist in that chain?) and the rule stays behind after the interface is gone. enp1s0 is the egress interface, taken from ip route show default. It is rarely eth0 on a current VPS image.

The same two rules as nftables hooks look like this. The table is created at up and deleted whole at down, so there is no rule text to keep in sync:

PostUp = nft add table inet wg; nft add chain inet wg forward '{ type filter hook forward priority filter; }'; nft add rule inet wg forward iifname "%i" accept; nft add chain inet wg postrouting '{ type nat hook postrouting priority srcnat; }'; nft add rule inet wg postrouting oifname "enp1s0" ip saddr 10.8.0.0/24 masquerade
PostDown = nft delete table inet wg

One nftables rule to know before you use that: every base chain on a hook is evaluated. An accept ends only the chain it is in, while a drop anywhere on the hook is final. So an accept in this wg table does not override a policy drop in the forward chain of another table, such as the one in /etc/nftables.conf from the install guide. On a box managed by one nftables file, put the forward and masquerade rules in that file and leave the hooks out. Hooks are the right tool when nothing else manages the firewall and you want the rules to live and die with the interface.

FAQ

Does wg syncconf disconnect the clients that are already connected?

No. wg syncconf reads the live configuration first, removes only the peers that are no longer in the file, and updates the rest in place. A peer's session keys are cleared when the peer is removed, so an existing client keeps its session and never notices. wg setconf and wg-quick down both remove every peer, which is why they stall every client for about 15 seconds while it re-handshakes.

Why does wg syncconf print "Line unrecognized: Address"?

Because you passed it wg0.conf directly instead of the output of wg-quick strip. wg only understands PrivateKey, ListenPort and FwMark in [Interface], and Address, DNS, MTU, Table, the hook lines and SaveConfig belong to wg-quick. strip removes them. Nothing was applied, since wg parses the whole file before it changes the interface. Use sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'.

Can I add a peer with wg set instead of editing wg0.conf?

Yes. sudo wg set wg0 peer <public key> allowed-ips 10.8.0.3/32 adds it to the running interface without disturbing anyone, but the file is not updated, so the peer is gone after the next wg-quick down or reboot. Run sudo wg-quick save wg0 to write the live state into the file, then read the file, because save drops comments and adds Endpoint = lines for peers that have connected. Editing the file and syncing is the cleaner habit.

Why did my first client stop working after I added a second one?

Almost always because both peers were given the same AllowedIPs. Each address maps to exactly one peer, so the second assignment silently takes the address from the first. sudo wg show wg0 allowed-ips shows the first peer as (none). Its handshake still completes, so latest handshake looks healthy while no packet gets through. Give the second peer its own /32, then sync again and check the list.

Does wg syncconf run PostUp and add routes?

No. wg knows nothing about the shell hooks or the kernel routing table. Both are handled by wg-quick at up and down. Adding a peer whose AllowedIPs sits inside the interface subnet needs no route, because the connected route already covers it. A peer that carries a separate network behind it, such as a home LAN, needs sudo ip route add <range> dev wg0 after the sync, and wg-quick up adds the same route itself on the next start.

#wireguard#wg-syncconf#wg-quick#vpn#peers#linux-networking