Security

Self-Host a WireGuard VPN Server on Ubuntu or Debian

Intermediate45 min to complete12 min readJuly 7, 2026

Quick answer

Run your own VPN with nothing but a $5 VPS and about 100 lines of config. WireGuard is the rare piece of security software that's both stronger and simpler than what it replaced — this tutorial builds a full server from scratch: keys, configs, NAT, a laptop client, and a phone connected by QR code, with every line explained in plain language.

intermediate · 45 min

Before you begin

  • An Ubuntu (20.04+) or Debian (11+) server with a public IP and sudo access
  • A laptop and/or phone to connect as clients
  • Basic comfort with SSH and editing files on a server
  • Ideally UFW already set up — see the firewall tutorial
WireGuard
VPN
Networking
Security
Linux
Ubuntu
Debian
Self-Hosted

A VPN sounds complicated, but the idea is simple: an encrypted tunnel between your device and a server you trust. Whatever network you're on — hotel Wi-Fi, airport, mobile data — your traffic travels through that tunnel first, unreadable to anyone in between, and exits to the internet from your server. Commercial VPN products sell you that tunnel to their servers; self-hosting means the far end is your machine, and nobody's business model sits between you and your traffic.

WireGuard is the tool to do it with. Where OpenVPN and IPsec are hundreds of thousands of lines of code with decades of accumulated options, WireGuard is a few thousand lines, lives in the Linux kernel, and has exactly one way to do things — modern cryptography, no choices to get wrong. The entire mental model fits in one sentence: each device has a key pair, and devices that know each other's public keys can build a tunnel. No usernames, no passwords, no certificates.

This is the full-control, full-responsibility counterpart to the Tailscale tutorial — Tailscale automates everything you're about to do by hand (I compare the two approaches in Tailscale vs. WireGuard). Doing it by hand once is worth it: you'll understand exactly what a VPN is, and you'll own every piece of it.

What You'll Build

  • A WireGuard server on wg0 with its own private subnet (10.8.0.0/24)
  • NAT and IP forwarding, so clients can reach the whole internet through the tunnel
  • A laptop client with a full-tunnel config (all traffic through the VPN)
  • A phone client provisioned by scanning a QR code in your terminal
  • The routine for adding and revoking devices without restarting anything

Step 1: Install WireGuard

On the server:

bash
sudo apt update
sudo apt install -y wireguard qrencode

wireguard brings the kernel module and the wg / wg-quick tools; qrencode is for the phone step later. That's the entire install — WireGuard has no daemon of its own, no service to configure. It's a network interface, like eth0, that happens to encrypt.

Step 2: Generate the Server's Keys

Every WireGuard participant has a key pair. In padlock terms: the public key is an open padlock you can hand out freely — anyone can use it to lock a message that only you can open. The private key is the only key that opens it, and it never leaves the machine it was born on.

bash
umask 077                          # files created now are readable by root only
wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub

That one pipeline writes the private key to server.key and derives the public key into server.pub (printing it to your terminal too). The umask 077 line matters: wg-quick will refuse to run — rightly — if the private key is world-readable.

Step 3: Write the Server Config

Create /etc/wireguard/wg0.conf:

ini
1[Interface]
2Address = 10.8.0.1/24
3ListenPort = 51820
4PrivateKey = <contents of /etc/wireguard/server.key>
5
6PostUp = ufw route allow in on wg0 out on eth0
7PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
8PostDown = ufw route delete allow in on wg0 out on eth0
9PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

Line by line, in plain terms:

  • Address — the server's IP inside the tunnel. You're inventing a tiny private neighborhood (10.8.0.0/24 — 254 usable addresses; the CIDR calculator breaks that notation down) and giving the server house number 1. Each client will get its own number: .2, .3, and so on.
  • ListenPort — the UDP port WireGuard listens on. 51820 is the conventional default.
  • PrivateKey — paste the actual key string (sudo cat /etc/wireguard/server.key). Yes, inline — this is why wg0.conf must stay root-only.
  • PostUp / PostDown — firewall commands run when the tunnel comes up and down. The ufw route lines permit forwarded traffic; the MASQUERADE line is NAT: when a client's packet heads for the internet, the server rewrites it to look like the server sent it, so replies come back to the server, which un-rewrites them into the tunnel. Without this line clients can reach the server but nothing beyond it — it's the single most-forgotten step in WireGuard setups.

One thing to check: eth0 must be your server's actual internet-facing interface. Find it with ip route | grep default — on modern clouds it's often ens5, enp1s0, or similar. Replace eth0 in all four lines if so.

Step 4: Enable Forwarding and Open the Firewall

By default, Linux refuses to pass traffic through itself — it only handles its own. A VPN server is a middleman by definition, so flip the forwarding switch permanently:

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

Then let the world reach your WireGuard port. If you set up UFW:

bash
sudo ufw allow 51820/udp

Note it's UDP, not TCP. And unlike SSH, an open WireGuard port reveals nothing to scanners — WireGuard stays silent to any packet not signed by a known public key, so port scans see nothing there at all.

Step 5: Start the Server

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

wg-quick@wg0 reads /etc/wireguard/wg0.conf, creates the interface, and applies your PostUp rules; enable --now starts it and makes it survive reboots. Check it:

bash
sudo wg show

You'll see the interface, its public key, and the listening port. No peers yet — the server is a party where nobody's arrived. Let's invite the laptop.

Step 6: Connect Your Laptop

Install WireGuard on the laptop (macOS/Windows apps from wireguard.com/install, or apt install wireguard on Linux) and generate its keys the same way:

bash
umask 077
wg genkey | tee laptop.key | wg pubkey > laptop.pub

Create the client config — in the desktop app: Add empty tunnel (it pre-generates keys for you); on Linux: /etc/wireguard/wg0.conf on the laptop:

ini
1[Interface]
2Address = 10.8.0.2/32
3PrivateKey = <contents of laptop.key>
4DNS = 1.1.1.1
5
6[Peer]
7PublicKey = <contents of /etc/wireguard/server.pub>
8Endpoint = <your-server-public-ip>:51820
9AllowedIPs = 0.0.0.0/0
10PersistentKeepalive = 25

The new pieces, plainly:

  • DNS — with all traffic tunneled, DNS lookups should go through the tunnel too, or you leak "which sites am I visiting" to the local café network. 1.1.1.1 is Cloudflare; use any resolver you trust.
  • Endpoint — where in the real world the server lives. This is the only real-world address in the whole system; everything else is tunnel-internal.
  • AllowedIPs — the most misunderstood line in WireGuard, because it works in both directions. On a client it means "which destinations go into the tunnel": 0.0.0.0/0 = everything = full tunnel. Set it to 10.8.0.0/24 instead and you get a split tunnel — only VPN-subnet traffic tunnels, normal browsing goes out locally.
  • PersistentKeepalive — a tiny packet every 25 seconds that keeps the connection alive through home routers and phone carriers' NAT, which drop mappings for silent connections.

Now tell the server this laptop exists — append to the server's /etc/wireguard/wg0.conf:

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

Here's AllowedIPs in its server-side meaning: "packets claiming to be from 10.8.0.2 are only accepted from this peer." It's routing and spoof-protection in one line. Apply without dropping existing connections:

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

Activate the tunnel on the laptop (toggle in the app, or sudo wg-quick up wg0), then verify — sudo wg show on the server should show the peer with a recent latest handshake, and on the laptop:

bash
ping 10.8.0.1            # can I reach the server inside the tunnel?
curl https://ifconfig.me # whole internet: should print the SERVER's public IP

If that curl shows your server's IP, congratulations — you're running your own VPN.

Step 7: Connect Your Phone With a QR Code

Typing keys on a phone keyboard is misery, so don't. On the server, build the phone's config as a file, then render it as a QR code in your terminal:

bash
1umask 077
2wg genkey | tee phone.key | wg pubkey > phone.pub
3
4cat > phone.conf <<EOF
5[Interface]
6Address = 10.8.0.3/32
7PrivateKey = $(cat phone.key)
8DNS = 1.1.1.1
9
10[Peer]
11PublicKey = $(sudo cat /etc/wireguard/server.pub)
12Endpoint = <your-server-public-ip>:51820
13AllowedIPs = 0.0.0.0/0
14PersistentKeepalive = 25
15EOF
16
17qrencode -t ansiutf8 < phone.conf

Add the phone as a peer on the server (same as Step 6, with 10.8.0.3/32 and phone.pub), wg syncconf again, then in the WireGuard app: Add tunnel → Scan from QR code, point the camera at your terminal, toggle on. Done.

Two notes: the QR code is the phone's private key, so clear your terminal afterward and rm phone.key phone.conf once the phone is set up. And each device gets its own key pair and IP — never copy one config to two devices, or their traffic will fight over the same tunnel slot.

Step 8: The Add / Revoke Routine

This is the whole "control plane" you now own, and it's three moves:

  • Add a device: generate a key pair, pick the next free IP (10.8.0.4/32, ...), write the client config, append a [Peer] block on the server, sudo wg syncconf wg0 <(sudo wg-quick strip wg0).
  • Revoke a device (sold the laptop, lost the phone): delete its [Peer] block from wg0.conf, run the same syncconf — access ends instantly. This is why per-device keys matter: revoking one device never disturbs the others.
  • See who's connected: sudo wg show — each peer's last handshake and traffic counters. A handshake within the last ~2 minutes means actively connected.

Common Issues

  • No handshake at all (wg show never shows latest handshake for the peer). Work outward: keys — the client config needs the server's public key and vice versa; swapped or truncated keys are the #1 cause. Endpoint — right public IP and port? Firewall — sudo ufw status shows 51820/udp ALLOW, and your cloud provider's security group (AWS/GCP/Azure) also allows UDP 51820 — the cloud firewall is a separate gate people forget.
  • Handshake works, can ping 10.8.0.1, but no internet. That's the forwarding/NAT layer: confirm sysctl net.ipv4.ip_forward prints 1, and that the interface name in your MASQUERADE and ufw route lines matches ip route | grep default. A MASQUERADE rule pointing at a nonexistent eth0 fails silently.
  • wg-quick up fails with resolvconf: command not found (Linux clients). The DNS = line needs a resolver hook: sudo apt install openresolv (or resolvconf), or delete the DNS line and manage DNS yourself.
  • Connection dies after idle time behind home/mobile networks. The NAT mapping timed out — make sure the client's [Peer] block has PersistentKeepalive = 25.
  • Everything breaks after reboot. You started the tunnel with wg-quick up but never enabled the unit: sudo systemctl enable wg-quick@wg0. Same for forwarding if you used sysctl -w instead of a file in /etc/sysctl.d/.

Frequently Asked Questions

Why is there no username or password?

The key pair is the identity — possession of the private key is the login, exactly like SSH keys. It's a stronger model than passwords (256-bit keys can't be guessed or phished) with one sharp edge: anyone who copies a device's config file gets in as that device. Protect configs like passwords, and revoke a device's peer block the moment its hardware is lost.

Is WireGuard really more secure than OpenVPN?

Its cryptography isn't magically stronger — both are secure when configured well. The difference is that WireGuard is much harder to configure badly: one fixed, modern cipher suite instead of a menu of legacy options, and a codebase small enough to actually audit. Smaller attack surface, fewer decisions, fewer mistakes. It's also markedly faster, since it runs inside the Linux kernel.

Should I use a full tunnel or a split tunnel?

Full tunnel (AllowedIPs = 0.0.0.0/0) sends everything through the server — the right choice on untrusted Wi-Fi and for phones generally. Split tunnel (AllowedIPs = 10.8.0.0/24) tunnels only VPN-network traffic — right when you just need to reach your server privately and don't want your Netflix stream making a detour through a VPS. It's one line in the client config; you can even keep two tunnel profiles and toggle.

Can my devices talk to each other through the VPN?

Yes — the server can relay laptop ↔ phone traffic, but you need two things beyond Step 4. First, the firewall must allow forwarding between tunnel clients, which the wg0 → eth0 rule from Step 3 doesn't cover — add PostUp = ufw route allow in on wg0 out on wg0 (and the matching PostDown) to the server config. Second, each client's AllowedIPs must include the tunnel subnet (AllowedIPs = 0.0.0.0/0 already covers it; a split-tunnel client needs 10.8.0.0/24). Note the topology is hub-and-spoke: everything relays through the server. If you want devices connecting directly to each other, that's the mesh problem Tailscale solves.

Does the WireGuard port make my server easier to attack?

Barely — and this is one of WireGuard's nicest properties. The protocol doesn't respond at all to packets that aren't authenticated with a known public key, so to a port scanner UDP 51820 looks identical to a closed port. Compare that with SSH, which happily announces its version banner to anyone who connects. There's no login prompt to brute-force because there's no login.

Tear Down

Stop and disable the tunnel, remove the firewall rule, forwarding, and the package:

bash
sudo systemctl disable --now wg-quick@wg0
sudo ufw delete allow 51820/udp
sudo rm /etc/sysctl.d/99-wireguard.conf
sudo apt remove --purge wireguard
sudo rm -rf /etc/wireguard

That last line deletes all keys and configs — every client is permanently disconnected. On clients, remove the tunnel in the app or sudo wg-quick down wg0 && sudo rm /etc/wireguard/wg0.conf.

Official References

If the manual key-and-peer bookkeeping feels like a chore, that's the exact chore Tailscale automates — and the comparison post will help you decide which trade-off fits. Either way, finish the job on this server with the UFW firewall and SSH hardening tutorials.

We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.

Struggling with this in production?

We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.