Setting Up WireGuard VPN on a Linux Server

By Eddie Power Nov 26, 2025 in Networking 6 min read
bash ssh wireguard

WireGuard is the VPN I reach for whenever I need to get into a home lab, a NAS or a small VPS from outside. The whole protocol fits in a few thousand lines of code, it's built into the Linux kernel (since 5.6), and a working config is about ten lines long. Compare that with an OpenVPN setup and its certificate authority, and it's easy to see why it has become the default choice for self-hosters.

This guide builds a simple hub setup: one Linux server with a public IP (or a port-forward on your router) and one or more clients (a laptop and a phone) that connect to it. Commands are for Debian and Ubuntu, but the configs are the same on any distro.

How WireGuard thinks about peers

There's no "server" or "client" in WireGuard itself. Every machine is a peer with a private key, a public key and a list of AllowedIPs. Two settings do most of the work:

  • AllowedIPs acts as both a routing table and an access list. Traffic to those addresses goes into the tunnel, and packets coming *from* a peer are only accepted if their source address is in that peer's list.
  • Endpoint is where to send packets. Only the side that initiates the connection needs it, which is why the "server" config usually has no endpoints for its peers.

Keep that model in mind and most WireGuard problems become easy to reason about.

Install and generate keys

sudo apt update
sudo apt install wireguard

Generate a key pair for the server. The umask makes sure the private key is only readable by root:

sudo -i
cd /etc/wireguard
umask 077
wg genkey | tee server.key | wg pubkey > server.pub
wg genkey | tee laptop.key | wg pubkey > laptop.pub
wg genpsk > laptop.psk

The optional pre-shared key (genpsk) adds a symmetric secret on top of the normal key exchange. It costs nothing and is a sensible extra layer. Ideally, generate client keys on the client and copy only the public key to the server. Generating everything in one place is fine for a first test.

Server configuration

Create /etc/wireguard/wg0.conf. Here 10.8.0.0/24 is the VPN subnet; pick any private range that doesn't clash with your LAN.

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <contents of server.key>

# NAT VPN traffic out of the server's main interface (change eth0 if needed)
PostUp   = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# laptop
PublicKey = <contents of laptop.pub>
PresharedKey = <contents of laptop.psk>
AllowedIPs = 10.8.0.2/32

Each client gets its own [Peer] block with a unique /32 address. Check the real name of your outbound interface with ip route show default. On cloud VMs it's often ens3 or enp1s0, not eth0.

Enable IP forwarding

The NAT rules are only needed if clients should reach the internet or your LAN *through* the server. If you just want to reach the server itself, skip this. Otherwise:

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

Open the firewall and start the tunnel

sudo ufw allow 51820/udp
sudo systemctl enable --now wg-quick@wg0
sudo wg show

wg-quick@wg0 is a systemd template unit that reads /etc/wireguard/wg0.conf, so the tunnel comes back after a reboot. (If you want to understand what that @ means, I cover template units in Systemd Units Explained.) If the server sits behind a home router, forward UDP 51820 to it as well.

Client configuration

On the laptop, create /etc/wireguard/wg0.conf (or import it into the WireGuard app on Windows, macOS, iOS or Android):

[Interface]
Address = 10.8.0.2/32
PrivateKey = <contents of laptop.key>
DNS = 10.8.0.1

[Peer]
PublicKey = <contents of server.pub>
PresharedKey = <contents of laptop.psk>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.8.0.0/24, 192.168.1.0/24
PersistentKeepalive = 25

A few notes on the choices here:

  • AllowedIPs on the client decides what goes through the tunnel. The example is a *split tunnel*: only the VPN subnet and the home LAN (192.168.1.0/24) are routed over WireGuard, and everything else uses the local connection. Use 0.0.0.0/0, ::/0 to send all traffic through the server, which is what you want on untrusted Wi-Fi.
  • DNS only makes sense if something on the server actually answers DNS on 10.8.0.1 (Pi-hole, Unbound, dnsmasq). Remove the line otherwise.
  • PersistentKeepalive = 25 sends a small packet every 25 seconds so NAT routers keep the UDP mapping open. Clients behind home or mobile NAT need it; the server doesn't.

For phones, qrencode saves typing the keys by hand:

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

Bring the client up with sudo wg-quick up wg0, then test with ping 10.8.0.1.

Checking that it works

wg show is the first thing to look at on both ends:

sudo wg show wg0

The line that matters is latest handshake. If it shows a recent time and the transfer counters go up on both sides, the tunnel is fine and any remaining problem is routing or firewall. If there's no handshake at all, the packets aren't arriving. Work through this list:

  1. Is UDP 51820 open on the server firewall *and* forwarded on the router? Check with sudo ss -ulpn | grep 51820 on the server.
  2. Do the keys match? The server's peer PublicKey must be the client's public key, and the client's peer PublicKey must be the server's. Swapping a private key in by mistake is the classic error.
  3. Is the Endpoint reachable? Dynamic home IPs change. A dynamic DNS name helps, and WireGuard only re-resolves it when the tunnel restarts.

If there's a handshake but you can't reach the LAN, check ip_forward, the NAT rule (sudo iptables -t nat -L POSTROUTING -v) and that the client's AllowedIPs includes the LAN range.

Locking down other services behind the VPN

Once WireGuard is running, the next step is to stop exposing everything else. A common pattern is to make SSH listen only on the VPN address, or allow it only from the VPN subnet:

sudo ufw allow from 10.8.0.0/24 to any port 22 proto tcp
sudo ufw delete allow 22/tcp

Do this from a session that's already connected over the VPN, and keep a second session open until you've confirmed you can still log in. It pairs well with the steps in Hardening Your SSH Server: key-only login plus no public SSH port removes most of the noise in your auth logs.

The same idea works for admin panels, databases and dashboards. If you're running services in containers, bind them to 10.8.0.1 instead of 0.0.0.0. My notes on networking in Docker in Production and Podman Rootless Containers cover how to publish ports on a specific address.

Wrapping up

A WireGuard server is a small amount of config for a big security win: one UDP port open to the internet, keys instead of passwords, and private access to everything else. Keep one [Peer] per device so you can revoke a lost phone by deleting a single block and running sudo systemctl restart wg-quick@wg0.

If you'd rather have someone set up the VPN, server or website for your business, that's the kind of work I do. See what I offer here.

Share this article

E
Eddie Power admin

Linux enthusiast and open source advocate.

Comments (1)

A
Eddie EP Power

This is a test comment, I thought this article was super helpfull and i also got more info from site xyz.123 super helpfull is not what most comments say.

Leave a Comment

Comments are moderated and will appear after approval.