How to Set Up a WireGuard VPN Server on Ubuntu 24.04 LTS

Set up a WireGuard VPN server on Ubuntu 24.04 or 22.04: IP forwarding, NAT with iptables, ufw or nftables, client configs with QR codes, split or full tunnel.

WireGuard is a communication protocol and free and open-source software that implements encrypted virtual private networks, and was designed with the goals of ease of use, high speed performance, and low attack surface.

It is a lightweight Virtual Private Network (VPN) that supports IPv4 and IPv6 connections. A VPN allows you to traverse untrusted networks as if you were on a private network. It gives you the freedom to access the internet safely and securely from your smartphone or laptop when connected to an untrusted network, like the WiFi at a hotel or coffee shop.

Compared to other popular VPN solutions, such as IPsec and OpenVPN, WireGuard is faster, easier to configure, and has a smaller footprint. It is cross-platform and can run almost anywhere, including Linux, Windows, Android, iOS and macOS.

Wireguard is a peer-to-peer VPN; it does not use the client-server model. Depending on its configuration, a peer can act as a traditional server or client. It works by creating a network interface on each peer device that acts as a tunnel. Peers authenticate each other by exchanging and validating public keys, mimicking the SSH model. Public keys are mapped with a list of IP addresses that are allowed in the tunnel. The VPN traffic is encapsulated in UDP.

On Ubuntu versions: this guide was tested on Ubuntu 24.04 LTS with wireguard-tools 1.0.20210914. Every command works unchanged on Ubuntu 22.04 LTS, whose standard security maintenance runs until May 2027. WireGuard has been part of the Linux kernel since 5.6, so there is no module to build on either release.

This guide does it by hand with wg and wg-quick, which is the best way to understand what WireGuard is doing. If you would rather have a web UI for adding clients, or want to roll WireGuard out with configuration management, see these instead:

Ensure your Server packages are up to date

Before proceeding, ensure that your os packages are updated. Use this command:

sudo apt update && sudo apt upgrade -y

Optionally set the server hostname

sudo hostnamectl set-hostname vpn.example.com

Install wireguard vpn

Wireguard is available in default ubuntu repositories. Install it using this command:

sudo apt install wireguard

This installs wireguard-tools, which gives you the wg and wg-quick commands. The kernel part is already in the stock Ubuntu kernel.

Enable IP forwarding

If you are using WireGuard to connect a peer to the WireGuard Server in order to access services on the server only, then you do not need to complete this section. If you would like to route your WireGuard Peer’s Internet traffic through the WireGuard Server (a full tunnel), or reach other machines on the server’s network, then you need IP forwarding.

Put the settings in their own drop-in file rather than editing /etc/sysctl.conf, so they are easy to find and survive upgrades:

printf 'net.ipv4.ip_forward = 1\n' | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
net.ipv4.ip_forward = 1

This guide routes IPv4 only. IPv6 through the tunnel needs its own forwarding, firewall and NAT rules, and turning on net.ipv6.conf.all.forwarding makes the server ignore router advertisements, which drops its own IPv6 default route on SLAAC networks unless you also set accept_ra = 2 on the uplink.

Drop the IPv6 line if you are not using IPv6 in the tunnel.

You also need the name of the server’s public network interface for the NAT rules below. It is often eth0, ens5 or enp1s0:

ip -o -4 route show to default | awk '{print $5}'

The examples below use ens5; replace it with yours everywhere.

Configure WireGuard VPN Server

The wg and wg-quick command-line tools allow you to configure and manage the WireGuard interfaces.

Run the following command to create a public/private key pair. The files will be saved under /etc/wireguard/ directory.

sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key'

The keys are written by a single root shell (sudo sh -c '…') on purpose. If you instead run sudo wg genkey | … > server_public.key, the final redirect is performed by your shell, not by sudo, and it fails with Permission denied because /etc/wireguard is owned by root. The umask 077 inside that shell makes sure both key files are created readable only by root:

-rw------- 1 root root 45 Nov 10 08:26 server_private.key
-rw------- 1 root root 45 Nov 10 08:26 server_public.key

wg genkey prints a base64 private key, and wg pubkey derives the matching public key (server_public.key) from it. The public key is what you give to clients.

Remember, the private key (server_private.key) should never be shared with anyone and always be kept secure.

Configure Tunnel Device

We need to create a network interface for WireGuard. The network interface name will be wg0.

sudo vim /etc/wireguard/wg0.conf

Add this:

[Interface]
## Private IP address for the wg0 interface ##
Address = 10.70.0.1/24

## VPN server listening port ##
ListenPort = 51820

## VPN server private key ##
PrivateKey = <contents of /etc/wireguard/server_private.key>

## Firewall rules (see the next section) ##
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o ens5 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o ens5 -j MASQUERADE

Replace the PrivateKey with the content of the one you generated:

sudo cat /etc/wireguard/server_private.key

Save and close the wg0.conf file. Additionally, tighten the permissions so that only the root user can read the keys and the config:

sudo chmod 600 /etc/wireguard/*.key /etc/wireguard/wg0.conf

Do not use chmod -R 600 /etc/wireguard/. The -R applies 600 to the directory too, which strips its execute (traversal) bit — the directory needs 700, and only the files inside it should be 600.

The interface can be named anything, however it is recommended to use something like wg0 or wgvpn0. The settings in the interface section have the following meaning:

  • Address - A comma-separated list of v4 or v6 IP addresses for the wg0 interface. Use IPs from a range that is reserved for private networks (10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16) that does not clash with the networks your clients sit on.
  • ListenPort - The UDP port WireGuard listens on. 51820 is the convention.
  • PrivateKey - The server’s private key.
  • SaveConfig - When set to true, the current state of the interface is saved to the configuration file when shutdown. Leave it out (as we do here) if you want to hand-edit wg0.conf, because it will overwrite your file — including your comments — every time the interface goes down.
  • PostUp / PostDown - Commands wg-quick runs after bringing the interface up and after taking it down. %i expands to the interface name (wg0).

Firewall: forwarding and NAT

Clients on a full tunnel send all their traffic to the server, which must forward it out of ens5 and masquerade it behind the server’s public IP. Pick one of the three options below, depending on what manages the firewall on your server.

Option 1: iptables rules in wg0.conf

This is what the PostUp/PostDown lines above do: allow forwarding from wg0, allow the replies back in, and masquerade traffic leaving ens5. The rules exist only while the tunnel is up, and are removed when it goes down, so there is nothing to persist separately.

On Ubuntu 24.04 the iptables command is the nftables-backed iptables-nft, so these rules end up in the kernel’s nftables ruleset like everything else. With the tunnel up you can see them:

sudo iptables -S FORWARD
sudo iptables -t nat -S POSTROUTING
-P FORWARD ACCEPT
-A FORWARD -i wg0 -j ACCEPT
-A FORWARD -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-P POSTROUTING ACCEPT
-A POSTROUTING -o ens5 -j MASQUERADE

Option 2: ufw

If ufw is active, remove the PostUp and PostDown lines from wg0.conf and let ufw own the rules. ufw drops forwarded traffic by default (DEFAULT_FORWARD_POLICY="DROP" in /etc/default/ufw), so it needs a route rule for the tunnel, plus a NAT rule it does not have a command for.

Add the NAT rule at the very top of /etc/ufw/before.rules, before the existing *filter section:

*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.70.0.0/24 -o ens5 -j MASQUERADE
COMMIT

Then allow SSH (so you do not lock yourself out), the WireGuard port, and forwarding from the tunnel to the internet:

sudo ufw allow OpenSSH
sudo ufw allow 51820/udp
sudo ufw route allow in on wg0 out on ens5
sudo ufw enable
sudo ufw reload

sudo ufw status should now include:

51820/udp                  ALLOW IN    Anywhere
Anywhere on ens5           ALLOW FWD   Anywhere on wg0

Do not skip the route rule because ping works without it. ufw lets ICMP echo through the forward chain on its own, so a ping test passes while every TCP connection from the client times out.

Option 3: plain nftables

If you manage the firewall with nftables directly, put the NAT rule in its own table so wg-quick can add and remove it as one unit. Create /etc/wireguard/wg0-nat.nft:

table ip wireguard {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    iifname "wg0" oifname "ens5" masquerade
  }
}

and replace the PostUp/PostDown lines in wg0.conf with:

PostUp = nft -f /etc/wireguard/wg0-nat.nft
PostDown = nft delete table ip wireguard

If your own ruleset has a forward chain with policy drop, also accept traffic with iifname "wg0" and ct state established,related there.

Open the WireGuard port at your cloud provider

If your server runs at a cloud provider (AWS, GCP, Azure, Hetzner, DigitalOcean), you also have to allow inbound UDP 51820 in the security group or cloud firewall. That is a separate layer from ufw, and the most common reason a client never completes a handshake.

Enable and Start WireGuard VPN Service

Run the following command on the server to enable auto-start at system boot time and start WireGuard.

sudo systemctl enable --now wg-quick@wg0

Check its status with the following command. You should see active in the output:

sudo systemctl status wg-quick@wg0.service

Output:

● wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0
     Loaded: loaded (/usr/lib/systemd/system/wg-quick@.service; enabled; preset: enabled)
     Active: active (exited) since Sat 2025-01-18 06:02:04 UTC; 6s ago
       Docs: man:wg-quick(8)
             man:wg(8)
             https://www.wireguard.com/
             https://www.wireguard.com/quickstart/
             https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
             https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
    Process: 1362 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)
   Main PID: 1362 (code=exited, status=0/SUCCESS)
        CPU: 30ms

Jan 18 06:02:04 vpn.example.com systemd[1]: Starting wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0...
Jan 18 06:02:04 vpn.example.com wg-quick[1362]: [#] ip link add wg0 type wireguard
Jan 18 06:02:04 vpn.example.com wg-quick[1362]: [#] wg setconf wg0 /dev/fd/63
Jan 18 06:02:04 vpn.example.com wg-quick[1362]: [#] ip -4 address add 10.70.0.1/24 dev wg0
Jan 18 06:02:04 vpn.example.com wg-quick[1362]: [#] ip link set mtu 8921 up dev wg0
Jan 18 06:02:04 vpn.example.com systemd[1]: Finished wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0.

active (exited) is correct: wg-quick is a script that configures the kernel interface and exits, so there is no long-running process. The MTU wg-quick picks is the outgoing interface’s MTU minus 80 bytes of overhead, so it is 1420 on a normal 1500-byte network.

To check the interface state and configuration, enter:

sudo wg show wg0

Output:

interface: wg0
  public key: dk6Uv6NVV0DqjkzJEqDvxsM8RCr0H0SSxcJTCs28rnc=
  private key: (hidden)
  listening port: 51820

No peers are listed yet — we add the client in the next section.

Set up client

Install the WireGuard package on the client machine

sudo apt install wireguard

You’ll need to generate a public/private key pair on the peer using the exact steps you used on the server.

sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/client_private.key | wg pubkey > /etc/wireguard/client_public.key'

Open a new /etc/wireguard/wg0.conf file on the WireGuard client machine using your preferred editor:

sudo vim /etc/wireguard/wg0.conf

Add the following lines to the file:

[Interface]
## VPN client private IP address ##
Address = 10.70.0.2/24

## VPN client private key ##
PrivateKey = <contents of /etc/wireguard/client_private.key>

## Resolver to use while the tunnel is up ##
DNS = 1.1.1.1

[Peer]
## VPN server public key ##
PublicKey = <contents of server_public.key on the server>

## VPN server public IP address and port ##
Endpoint = 203.0.113.10:51820

## Route all the traffic through the VPN tunnel ##
AllowedIPs = 0.0.0.0/0

## Keep the connection alive through NAT ##
PersistentKeepalive = 25

Replace 203.0.113.10 with your server’s public IP address, and fill in the two keys. PersistentKeepalive makes the client send a packet every 25 seconds, which keeps the mapping open on home routers and mobile networks that would otherwise forget an idle UDP flow.

The DNS line matters whenever AllowedIPs = 0.0.0.0/0. Without it the client keeps using whatever resolver the local network handed it, so your DNS queries still go to the untrusted network you are trying to tunnel out of. On Linux clients wg-quick needs resolvconf (or openresolv) installed for DNS to take effect:

sudo apt install resolvconf

Finally, save the file and chmod 600 it.

Full tunnel or split tunnel

AllowedIPs on the client decides what goes through the VPN:

  • Full tunnel, AllowedIPs = 0.0.0.0/0 (add , ::/0 for IPv6): all traffic goes through the server. Use it on untrusted Wi-Fi. wg-quick sets up policy routing so the encrypted packets to the server itself still leave through the normal interface:

    [#] wg set wg0 fwmark 51820
    [#] ip -4 rule add not fwmark 51820 table 51820
    [#] ip -4 rule add table main suppress_prefixlength 0
    [#] ip -4 route add 0.0.0.0/0 dev wg0 table 51820
    
  • Split tunnel, AllowedIPs = 10.70.0.0/24 (plus any private networks behind the server, for example 10.70.0.0/24, 10.0.10.0/24): only those networks use the tunnel, and the rest of the client’s traffic goes out as usual. Use it to reach servers in a private network without routing your browsing through the VPN. You can check where a given address goes with ip route get:

    $ ip route get 10.70.0.1
    10.70.0.1 dev wg0 src 10.70.0.2 uid 0
    $ ip route get 172.31.0.3
    172.31.0.3 via 172.30.0.1 dev eth0 src 172.30.0.3 uid 0
    

A split tunnel does not need the NAT rules on the server unless the client reaches other machines behind it.

Add the client to the server

You need to configure the server-side VPN option to allow a connection between the client (Peer) computer and the server. So, go back to the server and edit the /etc/wireguard/wg0.conf file to add client information as follows:

sudo vim /etc/wireguard/wg0.conf
[Peer]
## Client public key ##
PublicKey = <contents of client_public.key on the client>

## Client IP address ##
AllowedIPs = 10.70.0.2/32

Note the /32. On the server side, AllowedIPs is a routing table: it tells WireGuard which source addresses to accept from this peer and which destinations to send down its tunnel. It must therefore be the single host address of that client. If you write 10.70.0.2/24 here, the server routes the whole 10.70.0.0/24 VPN subnet to this one peer, and every client you add afterwards will fail to reach the others. Give each peer its own /32 (10.70.0.3/32, 10.70.0.4/32, and so on), each in its own [Peer] block.

Apply the change without restarting the interface, so clients that are already connected stay connected:

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

wg-quick strip removes the Address, PostUp and other wg-quick-only lines that wg does not understand, and syncconf applies only the difference. The new peer shows up straight away:

sudo wg show wg0 allowed-ips
R1L6BMI1Q9i27QZsyi/9xketCsXLRDxrJ35T61b3xCo=	10.70.0.2/32
kdU/cZU/adVwWxTF8bnQqsIevyVZT0kXO+Fpc1epUGE=	10.70.0.3/32

sudo systemctl restart wg-quick@wg0 also works, but it briefly drops every client.

Phones and laptops: share the config as a QR code

The WireGuard apps for Android and iOS can import a tunnel by scanning a QR code. Generate the client’s keys and wg0.conf as above (you can do it on the server, in a directory only root can read), then render the config as a QR code in the terminal with qrencode:

sudo apt install qrencode
sudo qrencode -t ansiutf8 < client1.conf

Scan it from the app with Add tunnel > Create from QR code. Use -t png -o client1.png instead to get an image file. The QR code contains the client’s private key, so do not screenshot it into chat, and delete client1.conf from the server once the client is set up if you generated it there.

Connecting the WireGuard Client to the Server

Run the following command on the client machine to connect the VPN client to the VPN server:

sudo systemctl enable --now wg-quick@wg0

For a laptop where you only want the VPN sometimes, use sudo wg-quick up wg0 and sudo wg-quick down wg0 instead.

To test the connection, ping the VPN server (10.70.0.1) from the client (10.70.0.2):

ping -c 3 10.70.0.1
PING 10.70.0.1 (10.70.0.1) 56(84) bytes of data.
64 bytes from 10.70.0.1: icmp_seq=1 ttl=64 time=0.657 ms
64 bytes from 10.70.0.1: icmp_seq=2 ttl=64 time=0.526 ms
64 bytes from 10.70.0.1: icmp_seq=3 ttl=64 time=0.707 ms

--- 10.70.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2007ms
rtt min/avg/max/mdev = 0.526/0.630/0.707/0.076 ms

On a full tunnel, also check that internet traffic leaves through the server: curl -4 https://ifconfig.me on the client should print the server’s public IP.

Verify with wg show

On the server, wg show lists every peer:

sudo wg show wg0
interface: wg0
  public key: dk6Uv6NVV0DqjkzJEqDvxsM8RCr0H0SSxcJTCs28rnc=
  private key: (hidden)
  listening port: 51820

peer: R1L6BMI1Q9i27QZsyi/9xketCsXLRDxrJ35T61b3xCo=
  endpoint: 172.30.0.3:36620
  allowed ips: 10.70.0.2/32
  latest handshake: 7 seconds ago
  transfer: 820 B received, 732 B sent

The latest handshake line is the one to check. A peer that has connected shows a handshake within the last two minutes; a peer that never has shows no endpoint, latest handshake or transfer lines at all. For scripts, sudo wg show wg0 latest-handshakes prints a Unix timestamp per peer, and 0 means never.

Troubleshooting

No handshake at all. The client’s packets are not reaching the server, or the server is dropping them:

  • UDP 51820 is blocked: check sudo ufw status and the cloud security group.
  • Endpoint on the client has the wrong IP or port.
  • A key mismatch: the server’s [Peer] PublicKey must be the client’s public key, and the client’s [Peer] PublicKey must be the server’s public key. A private key pasted in the wrong place fails silently.

Handshake works, but no internet on a full tunnel. Forwarding or NAT is missing on the server: check sysctl net.ipv4.ip_forward prints 1, that the masquerade rule names the right interface (sudo iptables -t nat -S POSTROUTING or sudo nft list ruleset), and, with ufw, that the route allow rule exists.

Ping works, but websites do not load. On ufw that is the missing route rule (ICMP is allowed without it). Otherwise it is usually DNS: check the DNS line and that resolvconf is installed on the client. Large pages that stall while small ones load point at MTU; try MTU = 1380 in the client’s [Interface].

It stopped working after restarting the server. The client keeps using its old session until it notices the server no longer answers, which takes several seconds. Give it about 15 seconds, or run sudo wg-quick down wg0 && sudo wg-quick up wg0 on the client.

Conclusion

In this guide we managed to set up wireguard as a VPN peer gateway on Ubuntu: a server with forwarding and NAT through iptables, ufw or nftables, a full or split tunnel client, QR codes for phones, and wg show to confirm it all works. To manage more than a handful of clients, have a look at the wg-easy and SaltStack guides linked at the top.

comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy