How to Build a WireGuard Tunnel Between Two VPS Servers on Ubuntu
In this article13 sections
A WireGuard tunnel gives two servers a private, encrypted network between them, over the public internet. A database server can then listen only on its private address, an application server can reach it there, and nothing else on the internet can. The setup is small, the code is short, and the tunnel is fast. This guide builds a tunnel between two Ubuntu 24.04 VPS servers, one acting as a hub and one as a client, then adds the firewall and routing rules that make it safe, and shows how to read the status when the connection will not come up.
The pattern covers the common jobs: a web server that reaches a database on a second server, a monitoring server that reaches private exporters, or an admin laptop that reaches servers with no public SSH port. The same steps scale to more peers by adding one peer block per server on the hub.
What you need
- Two Ubuntu 24.04 servers, each with a public IP address, and sudo access on both.
- A plan for the private addresses. This guide uses
10.8.0.0/24, with the hub at10.8.0.1and the client at10.8.0.2. Do not use an address range that either network already uses. - UDP port
51820reachable from the client to the hub. Check the provider’s firewall, not only UFW, because many clouds have their own.
Names used below. hub is the server that others connect to, with public address 203.0.113.10. client is the second server, with its own public address. Replace the addresses with yours, and keep the private addresses as they are.
Step 1: Install WireGuard on both servers
sudo apt update
sudo apt install -y wireguard
The package provides the wg command and wg-quick, which brings the interface up from a configuration file. Check the version:
wg --version
Expected: a version line such as wireguard-tools v1.0.x.
Step 2: Generate a key pair on each server
Each server needs its own private key, which never leaves that server, and a public key, which you share. Set the file mode first, so the private key is never readable by other users, even for a moment:
sudo sh -c 'umask 077 && wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey'
sudo ls -l /etc/wireguard/
Expected: privatekey and publickey, with the private key showing mode -rw-------. Print the public key on each server, and keep the two values on hand:
sudo cat /etc/wireguard/publickey
Run the same commands on the client. You now have a public key from each server, and each server has its own private key.
Step 3: Configure the hub
On the hub, create the interface configuration. The [Interface] section names this server’s address and the port to listen on. The [Peer] section lists the client, and the one address it may use:
sudo nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = HUB_PRIVATE_KEY
[Peer]
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 10.8.0.2/32
Replace HUB_PRIVATE_KEY with the contents of the hub’s /etc/wireguard/privatekey file, and CLIENT_PUBLIC_KEY with the client’s public key. The file holds a private key, so keep it at mode 600:
sudo chmod 600 /etc/wireguard/wg0.conf
AllowedIPs does two jobs in WireGuard. It says which addresses this peer may send from, which is a security check, and which traffic is routed to that peer. Keep it as narrow as the peer needs. On the hub, 10.8.0.2/32 means exactly that client.
Step 4: Configure the client
On the client, the peer is the hub. The Endpoint is the hub’s public address and port, and PersistentKeepalive keeps the connection open through NAT and firewalls:
sudo nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.8.0.2/24
PrivateKey = CLIENT_PRIVATE_KEY
[Peer]
PublicKey = HUB_PUBLIC_KEY
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.8.0.0/24
PersistentKeepalive = 25
Here AllowedIPs = 10.8.0.0/24 sends the whole tunnel network through the hub, so the client can reach the hub and any other peer. The keepalive of 25 seconds is the interval the WireGuard documentation suggests for connections behind NAT. The client initiates the connection, so the hub does not need the client’s address in advance.
sudo chmod 600 /etc/wireguard/wg0.conf
Step 5: Open the firewall on the hub
The hub must accept WireGuard’s UDP port from the internet. Allow that one port, and nothing else on the tunnel:
sudo ufw allow 51820/udp
sudo ufw status verbose
Expected: 51820/udp listed as ALLOW. Do not open the SSH port or other services on the tunnel interface unless you need them. Services bound to the tunnel address are reachable only from peers, which is the point of the tunnel.
Step 6: Bring the tunnel up on both servers
Start the interface with wg-quick, then check it. Start the hub first, then the client:
sudo wg-quick up wg0
sudo wg show
Expected: a block for the interface with its listening port, and a block for the peer. The peer block shows an endpoint, and a line latest handshake with a time within the last minute once the client has sent traffic. Make the tunnel start at boot on both servers:
sudo systemctl enable --now wg-quick@wg0
systemctl status wg-quick@wg0 --no-pager | head -5
Expected: the service active. A tunnel that comes up with wg-quick by hand but does not start at boot is a common cause of a broken service after a reboot, so check this step.
Step 7: Test the tunnel
From the client, ping the hub’s tunnel address, then from the hub, ping the client:
ping -c 3 10.8.0.1
ssh [email protected] hostname
Expected: replies from 10.8.0.1. The first ping may take a moment, while the handshake completes. Then check the counters in wg show: the transfer line should show data sent and received in both directions.
Step 8: Let peers reach each other through the hub
With a hub and two clients, a client reaches another client through the hub. The hub has to forward packets between peers. Enable forwarding and allow routing on the tunnel interface. Apply the setting permanently, then reload:
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
sudo ufw route allow in on wg0 out on wg0
Expected: net.ipv4.ip_forward = 1 printed by sysctl --system, and the UFW rule added. If you only need the two servers to reach each other directly, skip this step: a point-to-point tunnel does not need forwarding.
Step 9: Keep the keys safe and rotate them
- Never copy a private key to another machine. Each server generates its own. If a key is exposed, generate a new pair on that server, update its configuration and the public key on the other server, then restart the tunnel.
- Rotate keys on a schedule. A yearly rotation is a reasonable baseline for a small tunnel. Rotate immediately after a staff change or a server compromise.
- Keep the configuration private. The file holds the private key, so the mode 600 check in Step 3 and Step 4 matters.
When the handshake does not complete
If wg show shows latest handshake as never, or the peer block has no transfer, check these in order:
- The keys are paired correctly. Each side’s
PublicKeymust be the other side’s public key. A common mistake is pasting a server’s own public key into its own peer block. - The endpoint and port. The client’s
Endpointmust be the hub’s public address andListenPort. The hub must accept UDP 51820 at the provider firewall and in UFW. Check the hub’s log for incoming packets withsudo tcpdump -ni any udp port 51820 -c 5, then start the client. - AllowedIPs on each side. The hub’s peer entry needs the client’s tunnel address. A mismatch makes the hub drop the client’s packets.
- The clocks and the restart. After a key change, restart the interface on both sides with
sudo wg-quick down wg0 && sudo wg-quick up wg0.
Common mistakes
- Overlapping address ranges. If the tunnel network matches a local network, routes break in confusing ways. Pick a range that neither server uses.
- Opening every port on the tunnel. Allowing all traffic on
wg0gives every peer access to every service. Open only the ports each peer needs. - Forgetting
PersistentKeepalivebehind NAT. A client behind NAT can stop receiving packets until it sends one. - Setting
AllowedIPs = 0.0.0.0/0on a server. That sends all of the server’s traffic through the tunnel, which is a full VPN and not a site-to-site link. Use it only when you mean to route all traffic. - Skipping the boot test. A tunnel that works until the next reboot is still broken.
Frequently asked questions
Do both servers need a public IP address?
The hub needs one, because clients connect to it. The client needs one only if the hub has to connect back to it first. With a keepalive on the client, a client behind NAT works.
Is WireGuard faster than OpenVPN?
It is simpler and usually lighter on CPU, which is why it is common on small servers. Measure your own throughput before you rely on a figure, because it depends on the path and the hardware.
Can I use WireGuard instead of a database over the public internet?
Yes, and it is a good use. Bind the database to the tunnel address, not to 0.0.0.0, and keep the database port closed to the public interface.
Does the tunnel encrypt the traffic between the servers?
Yes. WireGuard encrypts every packet on the tunnel with the keys you generated. It does not replace TLS for public web traffic, which you still need.
A WireGuard tunnel is a few lines of configuration, with one private key per server and one public key per peer. Generate the keys with a strict umask, give each peer only the addresses it needs, open the one UDP port on the hub, enable the service at boot, and test both directions. Then bind your private services to the tunnel address, so only the servers you trust can reach them.