VPN ๐Ÿ“– 14 min read

WireGuard Cloud Deployment

This guide sets up a WireGuard VPN server by hand on a cloud VM running Ubuntu, using the standard wg and wg-quick tools. You'll generate keys, write the server and client configs, enable forwarding and NAT, connect phones and laptops, and learn how to read wg show and fix the problems you are most likely to hit.

Prerequisites

  • Hardware: A cloud VM (or any server) with a public IPv4 address, and at least one client device: a phone, laptop or another Linux machine
  • Software: Ubuntu Server 22.04, 24.04 or 26.04 LTS with sudo access over SSH, and access to your cloud provider's firewall or security group settings. Debian 12 and 13 use the same package name and commands.
  • Time: About an hour for the server and a first client

Versions: the commands target Ubuntu 22.04, 24.04 and 26.04 LTS, where WireGuard's install page lists sudo apt install wireguard. Ubuntu's package archive has the wireguard package for all three. 26.04 ships wireguard-tools 1.0.20250521 where 22.04 and 24.04 have 1.0.20210914, and the subcommands this guide uses (genkey, pubkey, show, syncconf, strip and up) are in both. The configuration format and tools are documented in the wg(8) and wg-quick(8) man pages, and the upstream Quick Start covers the same building blocks without wg-quick. If you need a VM first, Launch Your First EC2 Instance walks through one provider; any provider works.

If you'd rather manage peers from a web page than by editing files, WireGuard-Easy Deployment wraps the same tools in a Docker container with an admin panel. If your server can't accept inbound UDP at all (for example a home connection behind carrier-grade NAT), look at Tailscale Mesh VPN instead.

How the Pieces Fit Together

The server gets a WireGuard interface, wg0, with a private tunnel address (10.0.0.1/24 in this guide) and listens on UDP port 51820. Every client gets its own key pair and one tunnel address (10.0.0.2, 10.0.0.3 and so on). Each side lists the other as a [Peer] with its public key and its AllowedIPs.

Diagram: a cloud server with wg0 at 10.0.0.1/24 listening on UDP 51820, and two clients at 10.0.0.2 and 10.0.0.3 connecting to it.
One server and one key pair per client. The server listens on UDP 51820, each client gets one tunnel address, and each side lists the other as a [Peer].

AllowedIPs does two jobs, according to wg(8): it's the list of source addresses accepted from that peer, and the list of destinations sent to that peer. On the server, a client's AllowedIPs is just its own tunnel address. On a client, AllowedIPs decides what goes into the tunnel: 0.0.0.0/0 sends everything, a list of subnets sends only those. wg-quick turns the client's AllowedIPs into routes automatically.

For clients to reach the internet through the server, the server must forward packets between wg0 and its public interface and masquerade them (NAT) so replies come back. If you're shaky on subnets, routing or NAT, Networking Fundamentals covers them.

Server-Side Setup

Terminal window with the commands from the section Server-Side Setup
Generate the server key pair under umask 077, so the private key is never readable by others.

Step 1: Install WireGuard

sudo apt update
sudo apt install wireguard

The package comes from Ubuntu's standard repositories, so there's no extra repository or key to add.

Step 2: Generate Server Keys

sudo -i
cd /etc/wireguard
umask 077
wg genkey | tee server_private.key | wg pubkey > server_public.key
exit

This is the one-liner from wg(8). umask 077 makes the new files readable only by root, which matters because anyone with the private key can impersonate the server. The public key is not secret; it goes into every client config.

Step 3: Find the Public Network Interface

The NAT rule needs the name of the interface that carries the server's default route. It varies by provider and image, so don't assume eth0:

ip route show default

The word after dev in that line is the interface name. Use it wherever this guide writes eth0.

Step 4: Create the Server Config

sudo nano /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = <contents of server_private.key>

# Forward and masquerade client traffic. Replace eth0 with your interface from Step 3.
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

Address, PostUp and PostDown are wg-quick extensions; wg-quick(8) expands %i to the interface name and runs the snippets with bash when the tunnel goes up or down. Pick a tunnel subnet that doesn't overlap any network your clients sit on, or routing gets ambiguous. If the interface name in the MASQUERADE rule is wrong, the tunnel comes up and handshakes succeed, but client traffic never gets back, and nothing reports an error.

On current Debian and Ubuntu releases the iptables command is usually the iptables-nft compatibility layer on top of nftables (the Debian nftables wiki describes this default). iptables --version shows nf_tables or legacy. Either way the rules work; with the nft backend they also appear in sudo nft list ruleset.

Step 5: Enable IP Forwarding

echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
cat /proc/sys/net/ipv4/ip_forward

Linux doesn't forward packets between interfaces unless this is on. The last command should print 1. A drop-in file under /etc/sysctl.d/ survives reboots and doesn't depend on /etc/sysctl.conf existing.

Step 6: Open the Firewall

UDP 51820 has to be allowed in two separate places: the operating system's firewall and the provider's network firewall (an AWS security group, a cloud firewall, and so on). The provider's firewall is configured in its console or API, not on the VM.

# If UFW is enabled on the VM
sudo ufw allow 51820/udp
# UFW's default policy for routed traffic is to drop it; allow tunnel traffic out (replace eth0)
sudo ufw route allow in on wg0 out on eth0

The ufw route syntax and the separate routed policy are described in ufw(8). Make sure SSH stays allowed before you enable UFW on a remote VM; Locking Down SSH covers the safe order.

Step 7: Start WireGuard

sudo systemctl enable --now wg-quick@wg0
sudo wg show

The wg-quick@ unit runs wg-quick up wg0 at boot. At this point wg show lists only the interface: its public key, a hidden private key and the listening port. Peers appear once you add them.

Client Setup

Every device gets its own key pair. Never share one key between devices; if a device is lost, you remove just its peer.

Editor window showing Client config from this section
Endpoint is the server's public address, and AllowedIPs = 0.0.0.0/0 sends all IPv4 traffic through the tunnel.

Generate Client Keys

The cleanest approach is to generate the key pair on the client itself (the desktop and mobile apps can do this), so the private key never leaves the device, and send only the public key to the server. For phones it's often easier to generate everything on the server and hand over a QR code; in that case delete the client's private key file from the server afterwards.

umask 077
wg genkey | tee laptop_private.key | wg pubkey > laptop_public.key

Create the Client Config

[Interface]
PrivateKey = <client private key>
Address = 10.0.0.2/32
DNS = 1.1.1.1

[Peer]
PublicKey = <server public key>
Endpoint = <server public IP or DNS name>:51820
AllowedIPs = 0.0.0.0/0
  • Endpoint must be the server's public address, not its private VPC address.
  • DNS is handled by wg-quick, which sets it through resolvconf. If you run your own resolver such as Pi-hole, use its address here, as long as it's reachable through the tunnel.
  • AllowedIPs = 0.0.0.0/0 is an IPv4 full tunnel. It does not route IPv6: native IPv6 can bypass this VPN. Do not describe it as protecting all traffic. For split tunneling, list only the ranges you want through the VPN, for example 10.0.0.0/24, 192.168.1.0/24.
  • Add PersistentKeepalive = 25 under [Peer] only if the client sits behind NAT and must stay reachable while idle. The WireGuard Quick Start explains that keepalives keep NAT mappings open and are off by default because most users don't need them.

Before relying on the tunnel, compare curl -4 https://icanhazip.com and curl -6 https://icanhazip.com with and without it, and check the clientโ€™s DNS resolver. The IPv4 result should change to the server address. If IPv6 still works outside the tunnel, either configure and test end-to-end IPv6 routing and firewall rules or disable IPv6 on the client using its operating-system network settings. Merely adding ::/0 without server-side IPv6 routing is not a dual-stack setup.

Add the Client to the Server

Append a peer block to /etc/wireguard/wg0.conf:

[Peer]
PublicKey = <client public key>
AllowedIPs = 10.0.0.2/32

Apply it without disconnecting existing peers. wg-quick(8) gives this pattern; it's wrapped in bash -c because the config file is readable only by root and the <( ) syntax needs bash:

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

Restarting the unit with sudo systemctl restart wg-quick@wg0 also works but briefly drops every connected peer.

Connecting Clients

Terminal window with the commands from the section Connecting Clients
On the client: install WireGuard, install the config with mode 600, bring the tunnel up.

Linux

sudo apt install wireguard
sudo install -m 600 laptop.conf /etc/wireguard/wg0.conf
sudo wg-quick up wg0

If wg-quick up fails with a resolvconf error, the DNS line is the cause: install a resolvconf provider for your distribution, or remove the line and configure DNS another way.

Windows and macOS

Install the official client from wireguard.com/install (the Mac version comes from the App Store), then import the .conf file as a tunnel and activate it.

iOS and Android

Install the WireGuard app from the App Store or Play Store. You can import a file, type the config, or scan a QR code generated on the server:

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

The QR code contains the private key. Generate it in a terminal only you can see, and delete phone.conf once the phone is set up.

Verify It's Working

With the client connected, run sudo wg show on the server. For each peer it reports:

  • peer: the peer's public key. Match it against your notes to tell clients apart.
  • endpoint: the public IP and port the peer's packets last came from. It updates automatically when a laptop changes networks.
  • allowed ips: the AllowedIPs configured for that peer on this side.
  • latest handshake: how long ago the two sides last completed a handshake. If this line is missing, the peers have never talked; a recent time means keys and connectivity are fine.
  • transfer: bytes received from and sent to the peer. If received keeps growing but sent stays near zero, packets arrive but replies aren't routed back.

On a full-tunnel client, check which public address the internet sees, for example with curl https://ifconfig.me. It should be the server's IP.

Adding More Clients

  1. Create a key pair for the new device.
  2. Give it the next free tunnel address (10.0.0.3/32, 10.0.0.4/32 and so on). Two peers with the same address break each other.
  3. Add a [Peer] block on the server and apply it with wg syncconf.
  4. Write down which public key and address belong to which device. A short text file is enough; never store private keys in a shared or public repository.

Troubleshooting

No handshake at all

Symptom: wg show on the server has no latest handshake for the peer, and the client shows data sent but nothing received.

  • UDP port blocked. Check the provider's firewall or security group for an inbound UDP 51820 rule, then the VM firewall with sudo ufw status verbose or sudo nft list ruleset.
  • Server not listening. sudo ss -ulnp | grep 51820 should show the port. If not, check sudo systemctl status wg-quick@wg0 and sudo journalctl -u wg-quick@wg0 -b.
  • Wrong endpoint. The client's Endpoint must be the public IP or a name that resolves to it.
  • Key mismatch. The client's [Peer] PublicKey must be the server's public key, and the server's peer block must hold the client's public key. Pasting a private key where a public key belongs is a common slip.

Handshake works, but no traffic

Symptom: latest handshake is recent, but websites or remote hosts don't load.

  • Forwarding off. cat /proc/sys/net/ipv4/ip_forward must print 1. If UFW is enabled, also check /etc/ufw/sysctl.conf, because ufw(8) warns that its sysctl settings can override yours.
  • NAT rule missing or on the wrong interface. sudo iptables -t nat -S POSTROUTING should show the MASQUERADE rule with the interface from Step 3.
  • Forwarding blocked by a firewall. With UFW, check that the ufw route allow rule exists (sudo ufw status shows it as ALLOW FWD).
  • AllowedIPs. On the server, the peer's AllowedIPs must contain the client's tunnel address exactly. On the client, AllowedIPs must cover the destinations you're trying to reach. If you can ping 10.0.0.1 but not the internet on a full tunnel, look at forwarding and NAT; if you can reach the internet but not a private subnet, the subnet is missing from AllowedIPs or the server can't route to it.
  • DNS only. If ping 1.1.1.1 works through the tunnel but names don't resolve, the DNS address in the client config is wrong or not reachable through the tunnel.

Some sites or large transfers hang (MTU)

Symptom: ping and small requests work, but some pages stall, SSH freezes when lots of output arrives, or uploads hang.

WireGuard adds its own headers to every packet, so the tunnel MTU is smaller than the underlying link's. wg-quick(8) picks an MTU automatically from the endpoint route, which is usually right, but some networks (PPPoE, mobile carriers, nested tunnels) have a smaller path MTU than it assumes. Set a lower value explicitly in the client's [Interface] section, for example MTU = 1380, reconnect, and step down further if the problem persists. On Linux you can probe the path with ping -M do -s SIZE <host>, lowering SIZE until packets get through without a "message too long" error.

The service won't start

Run sudo journalctl -u wg-quick@wg0 -b. Typical causes are a typo in wg0.conf, a key that isn't valid base64, the listen port already in use by another process, or a PostUp command that exits with an error, such as a mistyped iptables option. (A wrong interface name does not cause an error; iptables accepts any name, which is why Step 3 matters.)

Security Notes

  • Keep private keys readable only by root on the server and only by the owner on clients. Never commit them to Git.
  • One key pair per device, so you can revoke a single device by deleting its [Peer] block and running wg syncconf.
  • wg(8) supports an optional preshared key per peer (wg genpsk, PresharedKey =) as an extra symmetric layer that the man page describes as being for post-quantum resistance.
  • The VPN server is still a server: keep it patched and lock down SSH.

Example Address Plan

Keeping a simple plan from the start avoids duplicate addresses later. For example, with the server at vpn.example.com:51820:

  • 10.0.0.1: the server
  • 10.0.0.2 to 10.0.0.9: laptops and desktops
  • 10.0.0.10 to 10.0.0.29: phones and tablets
  • 10.0.0.30 and up: servers or site-to-site peers

Use full tunnels for devices that join untrusted Wi-Fi, and split tunnels for devices that only need to reach private services.

WireGuard or Something Else?

WireGuard is a good default when you control both ends and UDP is allowed: the configuration is short, key handling is simple and the code base is small. OpenVPN is still useful when you need TCP transport through restrictive networks or certificate-based user management; see OpenVPN Cloud Deployment. A coordinated mesh such as Tailscale suits devices that can't accept inbound connections.