WireGuardLab
Flat isometric illustration of two pink laptops with padlock screens joined by glowing links through a mesh of small pink nodes on a dark backdrop.
deployment-guides

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.

By WireGuardLab Editorial · · 6 min read

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:

  • %i expands to the interface name, so the same block works for wg1 without editing.
  • Replace eth0 with the real egress interface. Find it with ip route show default. On cloud images it is frequently ens3, enp1s0, or eth0 depending 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.
  • AllowedIPs on the server is a /32 per 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 Endpoint on 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, ::/0 sends 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.
  • DNS is a wg-quick feature, not a protocol feature. It rewrites the system resolver while the tunnel is up, using resolvconf or systemd-resolved. On a minimal server image neither may be installed, and wg-quick up will fail with a confusing error until openresolv or systemd-resolved is present. Omit the line if the client does not need pushed DNS.
  • PersistentKeepalive = 25 belongs 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 handshake timestamp 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.
  • transfer counters rising in both directions means data is flowing. Sent but not received usually means the return path or AllowedIPs is wrong.
  • An endpoint line 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:

  1. 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.
  2. The tunnel subnet overlapping a real LAN. Symptoms look random because they depend on which network the client is sitting on.
  3. Forwarding enabled but no NAT rule, so packets reach the server and leave with a 10.7.0.x source address that nothing on the internet can route back.
  4. A /24 in a client’s AllowedIPs on the server. That prefix now belongs to one peer; adding the same entry to a second peer silently moves it off the first.
  5. 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

  1. WireGuard Quick Start
  2. WireGuard Installation
  3. wg(8) manual page
  4. wg-quick(8) manual page

Related