WireGuard is a lightweight VPN built into the Linux kernel. It uses modern cryptography, a tiny configuration format and a single UDP port, which makes it a good fit for joining two networks over the internet. In this tutorial you will connect two sites, each with a Linux gateway running Ubuntu 24.04, so that hosts on one private LAN can reach hosts on the other as if they were on the same network.

Prerequisites

To follow this tutorial you need:

  • Two servers running Ubuntu 24.04 LTS that act as the VPN gateway of each site, for example two CubePath VPS or a VPS plus an on-premises router box.
  • A non-root user with sudo privileges on both gateways.
  • A public IP address on at least one gateway. If both have a public IP, either side can start the tunnel.
  • UDP port 51820 reachable on the gateways from the internet.
  • Two private networks whose address ranges do not overlap.

This guide uses the following example values. Replace them with your own everywhere they appear:

ValueSite ASite B
Public IP of the gateway203.0.113.10198.51.100.20
LAN behind the gateway192.168.1.0/24192.168.2.0/24
Gateway LAN interfaceeth1eth1
Tunnel address (wg0)10.100.0.1/2410.100.0.2/24

Check your interface names with ip -br addr before you start. On many VPS the public interface is eth0 and the private one eth1, but it may be called ens18, enp1s0 or similar.

Step 1 - Installing WireGuard

The WireGuard kernel module ships with the Ubuntu 24.04 kernel, so you only need the userspace tools (wg and wg-quick). Run this on both gateways:

sudo apt update
sudo apt install wireguard

Confirm that the tools are installed:

wg --version
wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/

Step 2 - Enabling IP forwarding

A site-to-site gateway routes packets between its LAN interface and the tunnel, so the kernel must be allowed to forward IPv4 traffic. Create a dedicated sysctl file on both gateways instead of editing /etc/sysctl.conf:

echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wireguard.conf

Load the new setting:

sudo sysctl --system

Verify that forwarding is active:

sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

Step 3 - Generating the key pairs

Each gateway needs its own private key, and each side needs the other's public key. Generate the keys on each gateway, then make the private key readable by root only:

wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key
sudo chmod 600 /etc/wireguard/private.key

The command prints the public key, for example:

fE2t8HkZ0Wx3JqP5r1u9Y7bN6mV4cL2sD8aQ0eR3tUo=

Show the private key when you need to paste it into the configuration file:

sudo cat /etc/wireguard/private.key

Write down the public key of Site A and the public key of Site B. Public keys are safe to copy between servers; private keys never leave the machine that generated them.

Step 4 - Configuring the Site A gateway

The key setting in a site-to-site tunnel is AllowedIPs. On each peer it lists every network that lives behind the other side: WireGuard uses it both to accept packets from that peer and to route packets towards it. wg-quick also adds kernel routes for these ranges automatically.

On Site A, create the configuration file:

sudo nano /etc/wireguard/wg0.conf

Add the following content, replacing the keys with the real values:

[Interface]
Address = 10.100.0.1/24
ListenPort = 51820
PrivateKey = SITE_A_PRIVATE_KEY

[Peer]
# Site B
PublicKey = SITE_B_PUBLIC_KEY
Endpoint = 198.51.100.20:51820
AllowedIPs = 10.100.0.2/32, 192.168.2.0/24
PersistentKeepalive = 25

PersistentKeepalive = 25 sends a small packet every 25 seconds, which keeps the tunnel alive when one of the gateways sits behind NAT or a stateful firewall. If both gateways have public IPs without NAT you can leave it out.

Protect the file, since it contains the private key:

sudo chmod 600 /etc/wireguard/wg0.conf

Step 5 - Configuring the Site B gateway

Site B gets the mirror image of the same configuration:

sudo nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.100.0.2/24
ListenPort = 51820
PrivateKey = SITE_B_PRIVATE_KEY

[Peer]
# Site A
PublicKey = SITE_A_PUBLIC_KEY
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.100.0.1/32, 192.168.1.0/24
PersistentKeepalive = 25
sudo chmod 600 /etc/wireguard/wg0.conf

If Site B has no public IP (for example it is an office router behind NAT), keep the Endpoint line here and remove it from Site A. Site B will then initiate the tunnel and Site A will learn its current address from the first handshake.

Step 6 - Opening the firewall

The gateways must accept the WireGuard UDP port and forward traffic between the tunnel and the LAN. UFW drops forwarded traffic by default, so you need explicit route rules. Run this on both gateways, adjusting eth1 to your LAN interface:

sudo ufw allow 51820/udp
sudo ufw route allow in on wg0 out on eth1
sudo ufw route allow in on eth1 out on wg0

If UFW is not yet enabled, allow SSH first so you do not lock yourself out, then enable it:

sudo ufw allow OpenSSH
sudo ufw enable

Check the result:

sudo ufw status verbose
To                         Action      From
--                         ------      ----
22/tcp (OpenSSH)           ALLOW IN    Anywhere
51820/udp                  ALLOW IN    Anywhere

Anywhere on eth1           ALLOW FWD   Anywhere on wg0
Anywhere on wg0            ALLOW FWD   Anywhere on eth1

For a tighter setup, replace the first rule with sudo ufw allow from 198.51.100.20 to any port 51820 proto udp on Site A (and the equivalent on Site B) so that only the other gateway can reach the port.

Step 7 - Starting the tunnel

The wg-quick@ systemd template brings the interface up at boot. Enable and start it on both gateways:

sudo systemctl enable --now wg-quick@wg0

Check the service:

sudo systemctl status wg-quick@wg0
● [email protected] - WireGuard via wg-quick(8) for wg0
     Loaded: loaded (/usr/lib/systemd/system/[email protected]; enabled; preset: enabled)
     Active: active (exited) since Thu 2026-09-25 10:12:04 UTC; 6s ago

Now look at the tunnel state:

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

peer: k9Xp2Vt7QmL4sB1nR8cY3hZ6wE0uJ5aD2fG7iO9pTqM=
  endpoint: 198.51.100.20:51820
  allowed ips: 10.100.0.2/32, 192.168.2.0/24
  latest handshake: 4 seconds ago
  transfer: 1.02 KiB received, 1.18 KiB sent
  persistent keepalive: every 25 seconds

A latest handshake value means the two gateways authenticated each other. From Site A, ping the tunnel address of Site B:

ping -c 3 10.100.0.2

Also confirm that wg-quick installed the route to the remote LAN:

ip route show dev wg0
10.100.0.0/24 proto kernel scope link src 10.100.0.1
192.168.2.0/24 scope link

Step 8 - Routing traffic from the LAN hosts

The gateways now know how to reach each other's LAN, but the other machines on each LAN must also send traffic for the remote network to their local gateway.

  • If the WireGuard server is the default gateway of its LAN, nothing else is needed.
  • Otherwise, add a static route on the LAN's main router, or on each host. For a Linux host in Site A whose gateway has the LAN address 192.168.1.1:
sudo ip route add 192.168.2.0/24 via 192.168.1.1

This route is lost on reboot. To make it permanent on Ubuntu hosts, add it to the host's Netplan file under the LAN interface:

      routes:
        - to: 192.168.2.0/24
          via: 192.168.1.1

Then apply it with sudo netplan apply.

Test end to end from a host in Site A to a host in Site B:

ping -c 3 192.168.2.50
64 bytes from 192.168.2.50: icmp_seq=1 ttl=62 time=18.4 ms
64 bytes from 192.168.2.50: icmp_seq=2 ttl=62 time=18.1 ms
64 bytes from 192.168.2.50: icmp_seq=3 ttl=62 time=18.2 ms

The TTL drops by two because the packet crossed both gateways.

Adding a third site

WireGuard has no concept of a central server, so extra sites are just extra [Peer] sections. For a Site C with LAN 192.168.3.0/24 and tunnel address 10.100.0.3:

  1. Install WireGuard, enable forwarding and generate keys on the Site C gateway (steps 1 to 3).
  2. On Site A and Site B, add a [Peer] block with Site C's public key, its endpoint and AllowedIPs = 10.100.0.3/32, 192.168.3.0/24.
  3. On Site C, add one [Peer] block for Site A and one for Site B, each with their tunnel IP and LAN.
  4. Restart the tunnel on every gateway with sudo systemctl restart wg-quick@wg0.

Each AllowedIPs range can belong to only one peer on a given interface. If two peers claim the same range, the last one wins and traffic for the first peer silently stops.

Troubleshooting

latest handshake never appears. The UDP packets are not arriving. Check that port 51820/udp is open on both gateways and in any upstream firewall, that the Endpoint IP is correct, and that each side has the other's public key (not its own). sudo tcpdump -ni eth0 udp port 51820 shows whether packets reach the server.

The gateways can ping each other but LAN hosts cannot. Check that net.ipv4.ip_forward is 1, that the UFW route allow rules exist, that the remote LAN is listed in AllowedIPs, and that the LAN hosts have a route back through their gateway (step 8). A missing return route is the most common cause.

Large transfers hang while ping works. This is usually an MTU problem on a path with a smaller MTU, such as PPPoE. Add MTU = 1380 to the [Interface] section on both sides and restart the tunnel.

Changes to wg0.conf have no effect. wg-quick reads the file only when the interface starts. Run sudo systemctl restart wg-quick@wg0 after every edit.

To see why the service failed to start, check its log:

sudo journalctl -u wg-quick@wg0 -b

Conclusion

You connected two private networks through an encrypted WireGuard tunnel, with keys stored securely, routes installed by wg-quick, UFW forwarding only between the tunnel and the LAN, and the tunnel started automatically at boot. From here you can add more sites as extra peers, restrict the firewall rules to specific hosts or ports, or monitor tunnel health by alerting when wg show wg0 latest-handshakes reports a handshake older than a few minutes.