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.
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
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.
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
Endpointmust be the server's public address, not its private VPC address.DNSis handled by wg-quick, which sets it throughresolvconf. 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/0is 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 example10.0.0.0/24, 192.168.1.0/24.- Add
PersistentKeepalive = 25under[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
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
- Create a key pair for the new device.
- Give it the next free tunnel address (
10.0.0.3/32,10.0.0.4/32and so on). Two peers with the same address break each other. - Add a
[Peer]block on the server and apply it withwg syncconf. - 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 verboseorsudo nft list ruleset. - Server not listening.
sudo ss -ulnp | grep 51820should show the port. If not, checksudo systemctl status wg-quick@wg0andsudo journalctl -u wg-quick@wg0 -b. - Wrong endpoint. The client's
Endpointmust be the public IP or a name that resolves to it. - Key mismatch. The client's
[Peer] PublicKeymust 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_forwardmust print1. 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 POSTROUTINGshould show the MASQUERADE rule with the interface from Step 3. - Forwarding blocked by a firewall. With UFW, check that the
ufw route allowrule exists (sudo ufw statusshows 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.1but 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.1works through the tunnel but names don't resolve, theDNSaddress 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 runningwg 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 server10.0.0.2to10.0.0.9: laptops and desktops10.0.0.10to10.0.0.29: phones and tablets10.0.0.30and 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.



