Managing a remote team, running services in the cloud, or simply wanting a secure way to access your home lab from anywhere — these are exactly the situations where a VPN earns its place. But not all VPNs are created equal. OpenVPN has been the standard for years, but its setup complexity and performance overhead push many engineers toward something newer: WireGuard.
In this tutorial, you will set up a fully functional WireGuard VPN server on Ubuntu. By the end, you will have a server that accepts connections from peers (clients), routes their traffic through a private tunnel, and optionally forwards all internet traffic through your server.
What is WireGuard?
WireGuard is a modern, open-source VPN protocol built directly into the Linux kernel (since 5.6). Compared to OpenVPN or IPsec, it is:
- Faster — a lean codebase (~4,000 lines vs OpenVPN’s ~70,000) means less overhead
- Simpler to configure — no certificates or complex PKI; everything is based on public/private key pairs
- More secure — uses state-of-the-art cryptography (Curve25519, ChaCha20, Poly1305) with sensible defaults; no algorithm negotiation means fewer attack surfaces
- Always on — roaming between networks (Wi-Fi to 4G) reconnects instantly because the session is stateless
WireGuard works by creating a virtual network interface (wg0 by default). Each participant in the VPN — server or client — is called a peer. Each peer has a private key and shares its public key with the others. Traffic between peers is encrypted using those keys, and routing rules determine what traffic flows through the tunnel.
Prerequisites
Before starting, make sure you have:
- Two Ubuntu machines — one acts as the VPN server (ideally a VPS with a public IP), and one is your client (laptop, desktop, or another server). Both should run Ubuntu 20.04 or 22.04.
- Root or sudo access on both machines.
- A public IP address on the server, or at least a port forwarded through your router if it is behind NAT.
- Basic Linux familiarity — editing files with
nanoorvim, running commands withsudo.
In the examples below:
- Server public IP:
203.0.113.1 - VPN subnet:
10.0.0.0/24 - Server VPN IP:
10.0.0.1 - Client VPN IP:
10.0.0.2 - WireGuard port:
51820(UDP)
Step 1 — Install WireGuard on the Server
SSH into your server and install WireGuard. On Ubuntu 20.04 and 22.04, it is available in the default repositories.
sudo apt update
sudo apt install -y wireguard
Verify the installation:
wg --version
You should see output like wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/.
Step 2 — Generate the Server Key Pair
WireGuard uses asymmetric key pairs. You generate a private key, then derive the public key from it. The private key never leaves the machine; you share only the public key with peers.
wg genkey | sudo tee /etc/wireguard/server_private.key | wg pubkey | sudo tee /etc/wireguard/server_public.key
Lock down the private key so only root can read it:
sudo chmod 600 /etc/wireguard/server_private.key
Read both keys — you will need them in the next step:
sudo cat /etc/wireguard/server_private.key
sudo cat /etc/wireguard/server_public.key
Step 3 — Configure the WireGuard Interface on the Server
Create the main configuration file at /etc/wireguard/wg0.conf. Replace <SERVER_PRIVATE_KEY> with the content of server_private.key.
sudo nano /etc/wireguard/wg0.conf
Paste the following:
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <SERVER_PRIVATE_KEY>
# Enable IP forwarding and NAT for internet traffic routing
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
A few notes on the fields:
Address— the VPN IP address this interface will use, with the subnet mask for the VPN network.ListenPort— the UDP port WireGuard listens on. Ensure this port is open in your firewall.PostUp/PostDown— iptables rules that run when the interface comes up or goes down. TheMASQUERADErule is what makes internet traffic forwarding work: it rewrites the source IP of outbound packets to the server’s public IP so replies know where to come back to.
Note on
eth0: Replaceeth0with your actual outbound network interface name. Check it withip route get 8.8.8.8 | awk '{print $5; exit}'.
Lock down the config file:
sudo chmod 600 /etc/wireguard/wg0.conf
Step 4 — Enable IP Forwarding on the Server
By default, Linux does not forward packets between interfaces. You need to enable this so the server can route traffic from the VPN tunnel to the internet.
sudo nano /etc/sysctl.conf
Find the line #net.ipv4.ip_forward=1 and uncomment it (remove the #):
net.ipv4.ip_forward=1
Apply the change immediately without rebooting:
sudo sysctl -p
Confirm it is active:
sysctl net.ipv4.ip_forward
# Expected output: net.ipv4.ip_forward = 1
Step 5 — Open the Firewall Port
Allow WireGuard’s UDP port through UFW. If you are not using UFW, apply the equivalent rule in your cloud provider’s security group or iptables.
sudo ufw allow 51820/udp
sudo ufw status
Step 6 — Start the WireGuard Interface
Bring up the interface and enable it to start on boot:
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
Check that it is running:
sudo wg show
Output should show the wg0 interface with its public key and listen port.
Step 7 — Set Up the Client
Now switch to your client machine (laptop or second server). Install WireGuard the same way:
sudo apt update
sudo apt install -y wireguard
Generate a key pair for the client:
wg genkey | sudo tee /etc/wireguard/client_private.key | wg pubkey | sudo tee /etc/wireguard/client_public.key
sudo chmod 600 /etc/wireguard/client_private.key
Read the client’s public key — you will need it on the server side:
sudo cat /etc/wireguard/client_public.key
Step 8 — Add the Client as a Peer on the Server
Go back to your server and open /etc/wireguard/wg0.conf. Append a [Peer] section for the client. Replace <CLIENT_PUBLIC_KEY> with the key you just generated.
sudo nano /etc/wireguard/wg0.conf
Add to the bottom of the file:
[Peer]
PublicKey = <CLIENT_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32
AllowedIPs = 10.0.0.2/32— tells the server that packets destined for10.0.0.2should go to this peer, and that packets received from this peer must claim to come from10.0.0.2. The/32means this is a single host, not a network.
Reload the WireGuard configuration without dropping existing connections:
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
Or simply restart the service:
sudo systemctl restart wg-quick@wg0
Step 9 — Configure the Client Interface
Back on the client, create /etc/wireguard/wg0.conf. Replace the placeholders:
<CLIENT_PRIVATE_KEY>— content of/etc/wireguard/client_private.key<SERVER_PUBLIC_KEY>— content of/etc/wireguard/server_public.key203.0.113.1— your server’s real public IP
sudo nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.2/24
PrivateKey = <CLIENT_PRIVATE_KEY>
DNS = 1.1.1.1
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = 203.0.113.1:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
Key fields explained:
DNS = 1.1.1.1— DNS server to use while connected. Without this, DNS queries might bypass the tunnel.Endpoint— the server’s public IP and port. This is how the client knows where to send the initial handshake.AllowedIPs = 0.0.0.0/0— routes all traffic through the VPN tunnel. If you only want to route VPN subnet traffic (split tunneling), use10.0.0.0/24instead.PersistentKeepalive = 25— sends a keepalive packet every 25 seconds. This keeps the connection alive through NAT and firewalls that would otherwise drop the UDP session.
Set correct permissions:
sudo chmod 600 /etc/wireguard/wg0.conf
Step 10 — Connect and Test
Bring up the VPN on the client:
sudo wg-quick up wg0
Verify the handshake happened:
sudo wg show
You should see a [Peer] entry on the client with latest handshake showing a recent timestamp and bytes transferred. If you see (none) under latest handshake, the peers have not yet exchanged traffic — double-check the endpoint IP and port.
Test connectivity to the server’s VPN IP:
ping 10.0.0.1
If you configured AllowedIPs = 0.0.0.0/0, all your internet traffic now flows through the server. Verify your public IP has changed:
curl ifconfig.me
You should see 203.0.113.1 (the server’s IP), not your client’s original IP.
To enable the client VPN on boot:
sudo systemctl enable wg-quick@wg0
To disconnect:
sudo wg-quick down wg0
Common Mistakes and Troubleshooting
Handshake never completes
The most common cause is a firewall blocking UDP port 51820 on the server. Check:
sudo ufw status
# or check cloud provider security group rules
Also verify the server is actually listening:
sudo ss -ulnp | grep 51820
RTNETLINK answers: Operation not permitted when bringing up wg0
This often means the kernel module failed to load. Check:
sudo modprobe wireguard
dmesg | tail -20
On some minimal cloud images, you may need to install linux-modules-extra-$(uname -r).
Traffic is routed through the tunnel but DNS leaks
If you set AllowedIPs = 0.0.0.0/0 but did not set a DNS in the client config, your DNS queries may still go through your ISP. Add DNS = 1.1.1.1 to the [Interface] section of your client config and restart the tunnel.
Client can reach the server’s VPN IP but not the internet
This means IP forwarding or the MASQUERADE rule is not active on the server. Confirm:
sysctl net.ipv4.ip_forward
sudo iptables -t nat -L POSTROUTING -n -v
If the MASQUERADE rule is missing, the PostUp in your server config may reference the wrong interface name. Check with:
ip route get 8.8.8.8
And update eth0 in wg0.conf to match the real interface (often ens3, enp0s3, or eth0).
wg syncconf says “interface not found”
The interface is not up. Run sudo wg-quick up wg0 first, or use sudo systemctl start wg-quick@wg0.
Best Practices
Keep private keys private. Never paste a private key into a chat, ticket, or version control system. If a key is compromised, regenerate the pair and update the peer entry immediately.
Use unique IPs for each peer. Assign each client a distinct /32 address in the AllowedIPs field on the server side. Overlapping AllowedIPs across peers will cause unpredictable routing.
Restrict AllowedIPs for internal-only peers. If a peer (e.g., a backend server) only needs access to the VPN subnet and not the internet, set AllowedIPs = 10.0.0.0/24 in its client config instead of 0.0.0.0/0. This is split tunneling — only VPN-bound traffic goes through the tunnel.
Automate peer management for scale. For many peers, managing flat config files becomes painful. Tools like wg-easy or netmaker provide a web UI and API on top of WireGuard.
Monitor the tunnel health. The wg show command gives you per-peer transfer bytes and last handshake time. If latest handshake is older than 3 minutes, the peer has likely disconnected. You can wrap this in a cron job or monitoring script to alert on stale tunnels.
Use separate keys per device. If a user has a laptop and a phone, give each device its own key pair and [Peer] entry. This way, if one device is lost, you can revoke just that peer without affecting others — simply remove its [Peer] block from the server config and run sudo wg syncconf wg0 <(sudo wg-quick strip wg0).
Conclusion
You now have a working WireGuard VPN server on Ubuntu. Here is what you accomplished:
- Installed WireGuard and generated key pairs on both server and client
- Configured the server interface with IP forwarding and NAT for full traffic routing
- Added a client peer and connected the tunnel
- Verified the connection with a handshake check and public IP test
From here, you can expand the setup by adding more peers (repeat Steps 7–9 for each new client), exploring split tunneling to route only specific traffic through the VPN, or deploying WireGuard on mobile clients using the official WireGuard iOS/Android app — the peer configuration is identical, and both apps support scanning a QR code generated from the client config file with qrencode.
For production deployments, consider pairing WireGuard with a configuration management tool like Ansible to automate peer provisioning across a fleet of machines.