WireGuard Setup on Ubuntu: Server and Client
A config-first WireGuard install on Ubuntu: key generation, wg0.conf for server and client, forwarding, firewall rules, and adding peers without downtime.
A working WireGuard server is two files and about ten commands. Most of the length in a WireGuard tutorial is not the VPN at all: it is IP forwarding, NAT, and firewall rules, which are Linux networking questions that would exist with any tunnel. Separating the two makes the whole thing much easier to debug later, so this guide keeps them apart.
The target is Ubuntu 22.04 LTS or 24.04 LTS on a host with a public IP address and a UDP port you can reach, plus one or more clients. Nothing here is Ubuntu specific except the package manager and the firewall front end.
What is actually being installed
WireGuard has lived in the mainline Linux kernel since 5.6, so on any currently supported Ubuntu release there is no module to compile and no DKMS build to babysit. The wireguard package is a metapackage; what you use from it is wireguard-tools, which provides wg and wg-quick.
sudo apt update
sudo apt install wireguard
wg is the low level tool that talks to the kernel interface. wg-quick is a shell script wrapper that reads a config file, creates the interface, assigns addresses, installs routes, and runs your hooks. The distinction matters because the two accept different config keys, which is covered in the article on keys, AllowedIPs, and NAT traversal.
Generate keys with a restrictive umask
Every interface, server and client alike, gets one Curve25519 private key. The public key is derived from it. Set the umask first so the private key is never briefly world readable on disk:
sudo -i
umask 077
mkdir -p /etc/wireguard
cd /etc/wireguard
wg genkey | tee server.key | wg pubkey > server.pub
wg genkey | tee client1.key | wg pubkey > client1.pub
Optionally add a preshared key per peer pair. It is a 32 byte symmetric secret mixed into the handshake on top of public key authentication, documented as post-quantum mitigation: if Curve25519 is broken years from now by an adversary who recorded today’s traffic, sessions that also used a preshared key stay confidential.
wg genpsk > client1.psk
Plan the addressing before writing any config. A /24 out of RFC 1918 space that does not collide with any LAN either end might visit is the usual choice. This guide uses 10.7.0.0/24, with the server at .1 and clients from .2 upward. Avoid 192.168.0.0/24 and 192.168.1.0/24 for the tunnel, because a client on a hotel or home network in the same range will have a routing conflict.
The server config
/etc/wireguard/wg0.conf:
[Interface]
PrivateKey = <contents of server.key>
Address = 10.7.0.1/24
ListenPort = 51820
MTU = 1420
PostUp = sysctl -q -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i %i -j ACCEPT
PostUp = iptables -A FORWARD -o %i -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -s 10.7.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT
PostDown = iptables -D FORWARD -o %i -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.7.0.0/24 -o eth0 -j MASQUERADE
[Peer]
PublicKey = <contents of client1.pub>
PresharedKey = <contents of client1.psk>
AllowedIPs = 10.7.0.2/32
Points that matter more than they look:
%iexpands to the interface name, so the same block works forwg1without editing.- Replace
eth0with the real egress interface. Find it withip route show default. On cloud images it is frequentlyens3,enp1s0, oreth0depending on the hypervisor. - The masquerade rule is only needed if clients should reach the internet or a LAN through the server. A tunnel that only carries traffic between the peers themselves needs neither NAT nor forwarding, and leaving both off is the safer default.
AllowedIPson the server is a/32per client. It is simultaneously the routing entry for that peer and the inbound filter: a decrypted packet whose inner source address falls outside the range is discarded.- No
Endpointon the server side. The server learns each client’s address from the first authenticated packet and updates it as the client roams.
Prefer to keep forwarding enabled permanently rather than in a hook? Write it to a sysctl drop-in instead of the PostUp line:
printf 'net.ipv4.ip_forward=1\nnet.ipv6.conf.all.forwarding=1\n' \
| sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
The client config
/etc/wireguard/wg0.conf on the client, or an importable file for a phone:
[Interface]
PrivateKey = <contents of client1.key>
Address = 10.7.0.2/32
DNS = 10.7.0.1
MTU = 1420
[Peer]
PublicKey = <contents of server.pub>
PresharedKey = <contents of client1.psk>
Endpoint = vpn.example.net:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
Three decisions live in this file:
- Full tunnel or split tunnel.
AllowedIPs = 0.0.0.0/0, ::/0sends everything through the server. To route only the private networks and leave normal browsing on the local connection, list them instead:AllowedIPs = 10.7.0.0/24, 192.168.20.0/24. Splitting means DNS for those networks needs thought, because a client keeps using its local resolver. DNSis awg-quickfeature, not a protocol feature. It rewrites the system resolver while the tunnel is up, usingresolvconforsystemd-resolved. On a minimal server image neither may be installed, andwg-quick upwill fail with a confusing error untilopenresolvorsystemd-resolvedis present. Omit the line if the client does not need pushed DNS.PersistentKeepalive = 25belongs on the NAT’d side, which is the client. It sends an empty transport message after 25 seconds of silence, 60 bytes on the wire over IPv4, short enough to stay inside the idle UDP timeouts most consumer NAT tables apply. On the server it is mostly wasted traffic.
The MTU = 1420 in both files assumes a clean 1500 byte path. On PPPoE, mobile, or any path with another tunnel underneath it, that value is too high and produces the classic “connects fine, large transfers hang” failure. The MTU calculation and how to measure your own path covers the arithmetic; the mesh designer will compute it for the common transports.
Start it and check it
sudo systemctl enable --now wg-quick@wg0
sudo wg show
wg show is the diagnostic that answers most questions:
- A
latest handshaketimestamp means the peers authenticated each other. No timestamp at all means they never did, which is a key, endpoint, or UDP reachability problem, not a routing problem. transfercounters rising in both directions means data is flowing. Sent but not received usually means the return path orAllowedIPsis wrong.- An
endpointline on the server shows the address the client was last seen from, which is the roaming mechanism in action.
Confirm the socket is actually listening and reachable:
sudo ss -ulnp | grep 51820
Open the port in the firewall. On Ubuntu with UFW:
sudo ufw allow 51820/udp
sudo ufw reload
Cloud hosts usually have a second firewall in front of the instance. A security group or network ACL that only permits TCP will produce a tunnel that never handshakes while every command on the host looks correct.
Adding a client without restarting the tunnel
Restarting wg-quick drops every existing peer. Add a peer to the running interface instead, then write the same block into the config file so it survives a reboot:
sudo wg set wg0 peer "$(cat client2.pub)" \
preshared-key /etc/wireguard/client2.psk \
allowed-ips 10.7.0.3/32
sudo wg-quick save wg0
wg-quick save wg0 writes the live interface state back to the config. It is convenient and it has a sharp edge: the saved file loses comments and reorders keys. If your config is hand maintained and commented, add the [Peer] block by hand instead. The same warning applies to SaveConfig = true in the [Interface] block, which rewrites the file on every shutdown.
For a phone, render the client config as a QR code rather than copying keys through a chat app:
sudo apt install qrencode
qrencode -t ansiutf8 < client2.conf
Removing access is the mirror image, and it is the part people forget. WireGuard has no revocation list and keys never expire, so a removed client stays valid until its [Peer] block is deleted from every interface that trusted it:
sudo wg set wg0 peer "$(cat client2.pub)" remove
File permissions and other quiet mistakes
wg-quick prints a warning when a config file is world readable, and a private key in a world readable file is a full compromise of that interface. Fix it once:
sudo chmod 600 /etc/wireguard/*.conf /etc/wireguard/*.key /etc/wireguard/*.psk
Other recurring problems, in the order they usually appear:
- A private key pasted where a public key belongs. The handshake never completes and there is no error saying why. Keys are 44 base64 characters and look identical, so name the files carefully.
- The tunnel subnet overlapping a real LAN. Symptoms look random because they depend on which network the client is sitting on.
- Forwarding enabled but no NAT rule, so packets reach the server and leave with a
10.7.0.xsource address that nothing on the internet can route back. - A
/24in a client’sAllowedIPson the server. That prefix now belongs to one peer; adding the same entry to a second peer silently moves it off the first. - UDP blocked upstream. Some captive and corporate networks pass only TCP 443. WireGuard is UDP only, so no configuration change on your side fixes that path.
Where to go next
For several sites or a laptop that needs a direct path to more than one machine, the topology decision comes before the config: a full mesh of N peers needs N(N-1)/2 tunnels and every node’s public address, while hub and spoke needs one public endpoint and N-1 tunnels. The mesh designer generates both shapes and shows the tunnel count as you change the peer number.
If the plan involves many roaming devices behind carrier grade NAT, read the comparison between plain WireGuard and Tailscale before building a hub, because the NAT traversal work an overlay does is exactly what plain WireGuard leaves to you.
See also
Sources
Related
WireGuard vs Tailscale: Which One to Run
Tailscale runs on WireGuard, so the real choice is about the control plane: key distribution, NAT traversal, relays, identity, and what fails without them.
WireGuard MTU Calculator and Fragmentation Fix
Work out the right WireGuard MTU from your path MTU, understand the 60 and 80 byte overhead, measure the real ceiling, and clamp TCP MSS correctly.
WireGuard Explained: Keys, AllowedIPs, and NAT Traversal
How WireGuard really works: the static key handshake, what AllowedIPs controls, PersistentKeepalive and NAT, MTU math, and how it compares to OpenVPN.