Set up a WireGuard VPN on your own server
From an empty server to a working WireGuard tunnel: key pairs, NAT with nftables, wg-quick as a service, a QR code for your phone and the three failure modes that really cost you time.
What is the same on all four distributions, and what is not
WireGuard has been part of the Linux kernel since kernel 5.6. On Debian 13, Debian 12, Ubuntu 24.04 and Ubuntu 22.04 you therefore no longer have to build a DKMS module, add a backport repository or import a third-party key. All four also ship the same version of the userland tools (upstream version 1.0.20210914), which means the commands wg and wg-quick behave identically everywhere.
The differences are all in the surrounding pieces, and that is exactly where most guides fall apart:
- Packet filter:
wireguard-toolsrecommends nftables or iptables. Since apt pulls in the first available alternative, a slim Debian installation ends up with nftables, not iptables. Theiptables -t nat -A POSTROUTINGlines that everyone copies from everyone else do nothing there unless you install the package yourself. - DNS handover: for the
DNS =line,wg-quickstubbornly calls a program namedresolvconf. On Ubuntu, thesystemd-resolvedpackage ships a compatibility wrapper for it, while a minimal Debian installation often has noresolvconfat all. If you need to install it, the matching package on Debian and on Ubuntu 22.04 isopenresolv, and on Ubuntu 24.04 that package no longer exists. This only affects Linux clients, not phones. - Firewall frontend: Ubuntu images often come with an active ufw, Debian usually does not. ufw blocks forwarding by default, even when
net.ipv4.ip_forwardis set to 1.
Check what you are dealing with first:
apt-get update
apt-get install -y wireguard wireguard-tools nftables qrencode
wg --version
apt-cache policy wireguard-tools
If wg --version prints a version number, the userland is in place. Whether the kernel plays along only becomes clear on the first wg-quick up. If that leaves you with RTNETLINK answers: Operation not supported or Unable to access interface: Protocol not supported, you are running a kernel without WireGuard support, typically a very old one or a heavily stripped-down container kernel.
Generate the key pairs without building in trouble
WireGuard has no user names and no certificates. Each participant has exactly one key pair, plus an optional shared preshared key as an additional symmetric layer. Create both with umask set, otherwise the private keys sit on disk readable by everyone:
umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
wg genkey | tee /etc/wireguard/PHONE.key | wg pubkey > /etc/wireguard/PHONE.pub
wg genpsk > /etc/wireguard/PHONE.psk
ls -l /etc/wireguard
You do not have to create the /etc/wireguard directory yourself, the wireguard-tools package already ships it with mode 0700. More important is a quirk of umask: the value only applies to the shell session in which you set it. If you generate the keys in two rounds, for example because the connection dropped in between and you logged in again, you are back to the default mask afterwards, and server.key as well as PHONE.psk end up on disk with 0644. So set the permissions explicitly once more at the end:
chmod 600 /etc/wireguard/*.key /etc/wireguard/*.psk
Three mistakes keep coming up here:
- Public and private key swapped. Both are 44-character Base64 strings and look identical. If a public key ends up in
[Interface] PrivateKeyby accident, the tunnel still starts, but a handshake never happens. You can derive the answer at any time:wg pubkey < /etc/wireguard/server.keyhas to produce exactly the contents ofserver.pub. - Line break copied along. Keys copied out of a terminal with the mouse like to bring whitespace with them.
wganswers that withKey is not the correct length or format. - File permissions. If you forget
umask 077,wg-quickwarns at startup withWarning: `/etc/wireguard/wg0.conf' is world accessible. That is not cosmetic, this file holds the private key in plain text.
The server configuration
First find out the name of your internet-facing interface. On virtual machines it is called eth0, ens3 or enp1s0, depending on the image:
ip route show default
Then create /etc/wireguard/wg0.conf. Pick a tunnel network that you are unlikely to run into on a hotel Wi-Fi while travelling, so preferably not 192.168.0.0/24 or 192.168.1.0/24:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = CONTENTS_OF_SERVER_KEY
PostUp = nft add table ip wgnat
PostUp = nft add chain ip wgnat postrouting '{ type nat hook postrouting priority srcnat; policy accept; }'
PostUp = nft add rule ip wgnat postrouting ip saddr 10.8.0.0/24 oifname "eth0" masquerade
PostDown = nft delete table ip wgnat
[Peer]
# PHONE
PublicKey = CONTENTS_OF_PHONE_PUB
PresharedKey = CONTENTS_OF_PHONE_PSK
AllowedIPs = 10.8.0.2/32
Replace eth0 with your actual interface. If you prefer to stay with iptables, install iptables and use this instead:
PostUp = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
Two details that are rarely mentioned. First: every PostUp line is executed through a shell, and if one of them fails, wg-quick rolls back the entire start. A typo in the nft rule therefore does not give you a half-working tunnel, it gives you no tunnel at all. Second: AllowedIPs means something completely different on the server side than on the client side. Here it is a mapping table that says which source address belongs to which peer. If you enter the same address for two peers, the one loaded last wins and the other stays silent. Every client gets exactly one /32.
Set the permissions and check the syntax before you start:
chmod 600 /etc/wireguard/wg0.conf
wg-quick strip wg0
wg-quick strip prints the configuration without the wg-quick specific lines. If the command runs through, the file is formally correct. If it reports wg-quick: Line unrecognized, you have usually put an option into the wrong section, for example DNS under [Peer].
Enable IP forwarding permanently
Without forwarding, every packet ends at the server. Turn it on immediately and permanently:
echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-wireguard.conf
sysctl --system
sysctl net.ipv4.ip_forward
The last command has to print net.ipv4.ip_forward = 1. If you want to send IPv6 through the tunnel, net.ipv6.conf.all.forwarding=1 belongs in the same file.
If ufw is running, that is not enough yet. ufw sets its own forwarding policy that applies independently of the kernel switch. Open the port and allow the traffic through explicitly:
ufw allow 51820/udp
ufw route allow in on wg0 out on eth0
The typical symptom of forgotten forwarding is particularly nasty: the handshake works, the ping to 10.8.0.1 works, but nothing behind it is reachable. If you only look at the handshake, you will spend hours searching in the wrong place.
Set up wg-quick as a service
The package ships a template unit, and the name after the @ is the name of the configuration file without the extension:
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
systemctl status wg-quick@wg0
journalctl -u wg-quick@wg0 -n 50 --no-pager
One trap that many people only notice weeks later: SaveConfig = true. This option writes the running state back into the file when the service stops. In the process you lose all comments, all PostUp lines in their original order and every bit of structure you added by hand. For a server whose configuration you maintain by hand, leave the option out.
After starting, check the state at the interface rather than at the exit code:
wg show
ip -brief address show wg0
ss -ulpn
ss -ulpn has to show a listener on UDP port 51820. wg show lists the peers, at this point still without a handshake.
Client configuration and a QR code for the phone
The client file is best generated on the server, because all the keys are there anyway. Careful: on the client side, AllowedIPs means something else, namely which destinations should go through the tunnel. 0.0.0.0/0, ::/0 means everything.
mkdir -p /etc/wireguard/clients
Contents of /etc/wireguard/clients/PHONE.conf:
[Interface]
PrivateKey = CONTENTS_OF_PHONE_KEY
Address = 10.8.0.2/32
DNS = 9.9.9.9, 149.112.112.112
[Peer]
PublicKey = CONTENTS_OF_SERVER_PUB
PresharedKey = CONTENTS_OF_PHONE_PSK
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = your.server.address:51820
PersistentKeepalive = 25
PersistentKeepalive = 25 is not a luxury on phones. Mobile network NAT often forgets UDP mappings after 30 to 60 seconds, and without a keepalive the server can no longer reach an established connection on its own initiative.
You generate the QR code directly in the terminal:
qrencode -t ansiutf8 < /etc/wireguard/clients/PHONE.conf
In the WireGuard app, tap the plus, choose the import via QR code and point the camera at the terminal. Two practical notes: shrink the terminal font before you generate the code, otherwise it will not fit on the screen. And do not delete the file straight afterwards, you will need it again when you switch devices. If you do delete it, you have to generate a new key pair, because the private key cannot be recomputed from the public one.
How to tell that it really works
The fact that systemctl start returns without output only means that the interface exists. The actual test has three stages, and you should go through them in this order:
wg show wg0 latest-handshakes
wg show wg0 transfer
Stage one, the handshake. latest-handshakes prints one Unix timestamp per peer. If it says 0, a connection has never been established. In the verbose output of wg show you read latest handshake: 42 seconds ago instead.
Stage two, the data flow. transfer shows received and sent bytes. Sent bytes without received bytes means your packets go out and nothing comes back. That is almost always a firewall or a wrong endpoint, never a key problem.
Stage three, the actual path. On the client, check whether the traffic is routed into the tunnel at all:
ip route get 1.1.1.1
If it says dev wg0, the routing is correct. Only after that does it make sense to look at a page that shows your public IP address. If it shows the address of your server, you are done.
Troubleshooting: no handshake
The most common symptom of all. Start by switching on the logging of the kernel module, it usually delivers the answer within ten seconds:
echo 'module wireguard +p' > /sys/kernel/debug/dynamic_debug/control
dmesg -w
Now start a connection attempt on the client and read along. The three messages that matter:
wireguard: wg0: Handshake for peer 1 (...) did not complete after 5 seconds, retrying (try 2)and nothing else. Not a single packet arrives at the server. Cross-check withtcpdump -n -i eth0 udp port 51820. If you see nothing there, the cause is the firewall in front of the server, the port, or the client sitting in a network that filters outgoing UDP. A test from a mobile network instead of the office Wi-Fi separates these cases cleanly.wireguard: wg0: Invalid handshake initiation from .... Packets arrive but do not match. Almost always the public key of the server in the client file is wrong, or the preshared key is configured on one side only. A preshared key has to be identical on both sides or absent on both sides.- No message at all, even though
tcpdumpshows packets. In that case WireGuard is listening on a different port or on a different address. Check withss -ulpn.
A rarely documented special case is the clock. WireGuard puts a timestamp into the first handshake message, and the server remembers the highest value it has ever seen per peer. It discards older timestamps, which is the protection against replay. If a device had its clock set far into the future and connected once, it will no longer get through after the clock is corrected. The server keeps that state in memory, so the fix is: set the clock correctly, then run wg-quick down wg0 and wg-quick up wg0 on the server. After the rebuild the block is gone.
Switch the logging off again afterwards, it is chatty:
echo 'module wireguard -p' > /sys/kernel/debug/dynamic_debug/control
Troubleshooting: DNS does not work
Symptom: the tunnel is up, ping 1.1.1.1 works, but no name resolves. There are exactly three causes.
First: the resolver is not reachable. If you enter DNS = 10.8.0.1, a name server really has to be listening on that address on the server. A bare Debian or Ubuntu has none. Either you enter a public resolver that is reached through the tunnel, or you set one up yourself. For the second option, dnsmasq with a small file under /etc/dnsmasq.d/wireguard.conf is enough:
interface=wg0
bind-dynamic
no-resolv
server=9.9.9.9
server=1.1.1.1
cache-size=1000
bind-dynamic matters because wg0 may not exist yet when dnsmasq starts. no-resolv is mandatory on Ubuntu: without that line, dnsmasq reads /etc/resolv.conf, finds the systemd-resolved stub address 127.0.0.53 there and builds a loop. And the point that bites most often on Ubuntu: dnsmasq occupies port 53. On a system with systemd-resolved active, that port is already taken, the two services collide, and dnsmasq does not start. The lines interface=wg0 and bind-dynamic are therefore not cosmetic, they keep dnsmasq from binding to every address. Afterwards, use ss -ulpn to confirm that dnsmasq and systemd-resolved are not fighting over port 53.
Second: the resolver is outside the AllowedIPs. If you send only selected networks through the tunnel instead of 0.0.0.0/0 and configure a DNS server whose address is not in that list, the queries go past the tunnel into the local network.
Third, on Linux clients only: resolvconf is missing. The DNS = line is passed on by wg-quick to a program called resolvconf. If it is missing, the start aborts with /usr/bin/wg-quick: line 32: resolvconf: command not found. Check first whether the program exists at all:
command -v resolvconf
When it comes to installing it, the distributions diverge, and that is exactly where copied guides fail on Ubuntu 24.04. There the openresolv package exists neither in main nor in universe, and the call ends with E: Package 'openresolv' has no installation candidate:
| System | Matching package |
|---|---|
| Debian 11, Debian 12, Debian 13 | apt-get install -y openresolv |
| Ubuntu 22.04 | apt-get install -y openresolv |
| Ubuntu 24.04 | apt-get install -y resolvconf |
On Ubuntu 24.04, resolvconf is a virtual package that resolves unambiguously to systemd-resolved and creates /usr/sbin/resolvconf in the process, which is exactly the binary that wg-quick calls for the DNS line. You can just as well install systemd-resolved directly there. If you want one line that works on all of the systems named above, use this one:
apt-get install -y openresolv || apt-get install -y resolvconf
On Ubuntu 24.04 there is a variant of this that shows up after an upgrade from 22.04: Failed to resolve interface "tun.wg0": No such device. The cause is a leftover old resolvconf package from 22.04 together with /etc/resolvconf/interface-order, so not the virtual package of the same name from 24.04. Based on that file, wg-quick prefixes the interface name with tun., which the compatibility wrapper of systemd-resolved cannot do anything with. The fix is to remove the old package so that only the wrapper from systemd-resolved remains.
Troubleshooting: MTU problems
The most unpleasant symptom, because everything appears to work. The handshake is up, ping runs, SSH runs, but web pages load halfway and then stall, large downloads break off, and HTTPS of all things is affected. The reason: small packets fit, large ones do not.
WireGuard adds 60 bytes around every packet when the tunnel runs over IPv4 (20 bytes IP, 8 bytes UDP, 32 bytes WireGuard), and 80 bytes over IPv6. wg-quick therefore subtracts a flat 80 bytes from the detected path MTU and ends up at 1420 on a normal 1500 byte path. That is deliberately conservative and correct in most cases.
It is not correct when the path is narrower than 1500, for example with DSL over PPPoE (1492), behind another tunnel or in some mobile networks. Measure the actual path MTU from the client to the public address of the server, with the Don't Fragment bit set and without the tunnel:
ping -M do -s 1472 -c 3 TARGET_ADDRESS
1472 plus 28 bytes of headers makes 1500. If ping: local error: message too long, mtu=... or Frag needed and DF set comes back, lower the value step by step until it gets through: 1464, 1444, 1414, 1372. Add 28 to the value you found and subtract 80. For 1464 that would be a path MTU of 1492 and a tunnel MTU of 1412.
This goes into the [Interface] section, on the side that has the problem:
MTU = 1412
The quick counter-test before you start calculating: set MTU = 1280 as an experiment. That is the smallest MTU IPv6 guarantees, and it works practically everywhere. If your pages load cleanly with it, the MTU was the culprit, and you can work your way towards the optimal value at your leisure. If the problem stays, it is somewhere else.
On the server itself a wrong MTU is rarer, but possible: if the value there is higher than the path allows, you only see the effect with certain destinations. A look at ip -brief address show wg0 and ip link show wg0 shows the value currently set.
Change peers in production without kicking everyone out
The reflex to type systemctl restart wg-quick@wg0 after every change drops all existing connections and rebuilds the NAT rules. On a server with several users that is unnecessarily blunt. WireGuard can reconcile the configuration while it runs:
wg syncconf wg0 <(wg-quick strip wg0)
The command compares the file with the running state and changes only the differences. Existing peers keep their session. Note that the process substitution with <(...) requires bash or zsh, in a plain sh it fails. You can also add a single peer directly:
wg set wg0 peer PUBLIC_KEY allowed-ips 10.8.0.3/32
This change only lives in memory. Write it into the configuration file as well, otherwise the peer is gone after the next restart. That, by the way, is the most common reason behind the sentence "it still worked yesterday".
If you want to run a WireGuard endpoint permanently and with a stable address, your own server is the obvious basis. At KernelHost, KVM root servers and dedicated servers run in the maincubes datacenter in Frankfurt am Main (TÜV TIER3+) on our own network, PrePaid and without a minimum term. Related reading: our articles on hardening SSH and on setting up ufw.
Frequently asked questions
Which WireGuard version is in Debian 13, Debian 12, Ubuntu 24.04 and Ubuntu 22.04?
Why do the iptables lines from many guides not work on Debian?
The handshake works, but I cannot reach the internet. What is wrong?
How do I recognize an MTU problem?
Do I have to run my own DNS server for DNS to work inside the tunnel?
Why does a device stop getting through after the clock is corrected?
2026 KernelHost GmbH. All rights reserved. This guide is protected by copyright. Republishing it on other websites, in whole, in part or in edited form, is not permitted without our written consent. Quoting with a source credit and a link is expressly welcome.

