Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Security Difficulty: Intermediate

Setting up a WireGuard VPN: secure access to your own server

A lean WireGuard VPN natively on the netcup server: keys, server and client config, firewall, phone QR – plus the Docker pitfall with the full tunnel.

· 12 min read ·Duration: approx. 40 minutes
Table of contents

So far, everything on your server is publicly reachable behind Traefik. But some things should run only for you – an admin dashboard, a database, an internal service. With WireGuard you build yourself an encrypted tunnel straight into the server, without opening any further ports to the internet.

What are we building?

A WireGuard VPN natively on the server (no container – WireGuard is in the Linux kernel, which is the leanest and most stable variant). Your devices – laptop, phone – get an encrypted tunnel to the server and, through it, a private IP in the 10.8.0.0/24 network. Two operating modes that you switch via a single client line:

  • Split tunnel (default): Only the path to the server and its internal services runs through the VPN. This lets you reach things that shouldn’t be public (a dashboard, a database, later your own DNS filter), without publishing a port for it.
  • Full tunnel (optional): All of your device’s internet traffic runs through the server – handy on open café Wi-Fi, because then everything goes out encrypted via your own IP.

Tested with wireguard-tools 1.0.20210914 on Debian 13 (kernel 6.12, WireGuard is built in there).

Why WireGuard and not OpenVPN or an open port? WireGuard runs in the kernel, is tiny with only a few thousand lines of code (easy to audit, small attack surface), needs only a single UDP port and gets by with a handful of config lines. The handshake is so lean that a mobile connection is practically back up immediately after waking. So instead of putting every internal service on the net with its own authentication, you lay one secure tunnel – and everything behind it stays private.

Prerequisites

  • A server with an active UFW firewall and root or sudo access.
  • If you also use the netcup firewall: open the WireGuard port there later (ready-made template in the firewall tutorial).
  • A client device with the official WireGuard app (Windows, macOS, Linux, iOS, Android).
🍳 Recommendation Ad

VPS 1000 G12

4 vCores · 8 GB RAM · 256 GB NVMe

from €10.36/month

A WireGuard VPN needs hardly any resources – the smallest VPS is plenty.

Go to netcup →

💶 5 € voucher for new netcup customers:36nc17844976032 (new customers only, no domains)

Step by step

Step 1: Install WireGuard

The kernel already ships WireGuard; you only need the userspace tools (and qrencode for the phone QR code):

Terminal
sudo apt update
sudo apt install -y wireguard wireguard-tools qrencode

Check that the kernel module can be loaded:

Terminal
sudo modprobe wireguard && lsmod | grep wireguard
Ausgabe
wireguard             118784  0

Step 2: Generate key pairs

WireGuard authenticates purely via key pairs – no password, no certificates. Each side (server and every client) has a private and a public key. The private one must never leave the machine. First set a strict file mask so the keys aren’t readable by others:

Terminal
umask 077
wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub

The first command generates the private key (server.key), pipes it through wg pubkey and stores the public one (server.pub). Generate a pair for your first client the same way:

Terminal
wg genkey | tee client.key | wg pubkey > client.pub

You now have four keys. Remember the assignment: the server config gets the private server key and the public client key; the client config the other way around. This is the most common source of error – never enter the private key of the wrong side.

Step 3: Create the server configuration

Create /etc/wireguard/wg0.conf. Enter your private server key and the public client key:

INI
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = YOUR_SERVER_PRIVATE_KEY

[Peer]
PublicKey = YOUR_CLIENT_PUBLIC_KEY
AllowedIPs = 10.8.0.2/32

What the lines mean:

  • Address – the VPN-internal IP of the server (10.8.0.1); the /24 spans the VPN subnet 10.8.0.0/24.
  • ListenPort – the UDP port WireGuard listens on. 51820 is the default.
  • [Peer] + AllowedIPs = 10.8.0.2/32 – this client may only appear under the VPN IP 10.8.0.2. Important: AllowedIPs has a different meaning on each side – on the server it’s the client’s fixed tunnel IP, on the client (step 5) it defines what is routed through the tunnel.

For every further client you later simply add another [Peer] block with the next IP (10.8.0.3/32 …).

Step 4: Open the firewall and start the tunnel

WireGuard speaks over UDP – open the port. In UFW:

Terminal
sudo ufw allow 51820/udp

If the netcup firewall also sits in front, you need an inbound rule there UDP · destination port 51820 – the ready-made template is in the netcup firewall tutorial. Now start the tunnel and set up autostart:

Terminal
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0

Check that the interface is up:

Terminal
sudo wg show wg0
Ausgabe
interface: wg0
  public key: nZT4kozgZjtHVzGfPRL+niI4llEQCUqGpYG0ni+fAT4=
  private key: (hidden)
  listening port: 51820

peer: j5h2jl87JApAbWrJikrth3H/6fNuf6X2TgBgDSJVwwE=
  allowed ips: 10.8.0.2/32

The peer is registered, but there’s no latest handshake yet – logical, the client is still missing.

Step 5: Set up the client

The client config is the mirror image. Create a file client.conf (on the server to generate the QR code; you then transfer the content to the client device):

INI
[Interface]
Address = 10.8.0.2/24
PrivateKey = YOUR_CLIENT_PRIVATE_KEY
# DNS = 10.8.0.1   # only enable if a DNS resolver really runs on 10.8.0.1 – otherwise e.g. 1.1.1.1

[Peer]
PublicKey = YOUR_SERVER_PUBLIC_KEY
Endpoint = YOUR_SERVER_IP:51820
AllowedIPs = 10.8.0.0/24
PersistentKeepalive = 25

The decisive lines:

  • Endpoint – the public IP of your server plus port. The client builds the tunnel to here.
  • AllowedIPs = 10.8.0.0/24 – this is the split tunnel: only traffic into the VPN subnet runs through the tunnel, your normal internet traffic stays direct. For the full tunnel (everything via the server) you set 0.0.0.0/0 here – see step 6.
  • DNS = 10.8.0.1 – deliberately commented out. This line routes the entire system DNS of your device to 10.8.0.1 – but by default nothing listens there on port 53, so with the tunnel active, name resolution breaks. Only set it if a dedicated DNS resolver/filter really runs on the server; if you just need working DNS in the tunnel, enter a real resolver like 1.1.1.1 instead.
  • PersistentKeepalive = 25 – keeps the connection open when the client sits behind a NAT/CGNAT (phone on the mobile network).

For the phone, generate a QR code from the file right in the terminal – in the WireGuard app “Add tunnel → Scan QR code”:

Terminal
qrencode -t ansiutf8 < client.conf

On the desktop you simply import the client.conf in the WireGuard app. Afterwards delete the client.conf and client.key from the server (they belong on the end device, not on the server).

Step 6: Check the connection (and enable the full tunnel)

Activate the tunnel in the client app and check on the server:

Terminal
sudo wg show wg0
Ausgabe
peer: j5h2jl87JApAbWrJikrth3H/6fNuf6X2TgBgDSJVwwE=
  endpoint: 203.0.113.24:34582
  allowed ips: 10.8.0.2/32
  latest handshake: 45 seconds ago
  transfer: 1.58 KiB received, 1.39 KiB sent

A latest handshake plus a growing transfer means the tunnel is up. From the client you can now reach the server at its VPN IP:

Terminal
ping 10.8.0.1

If you want the full tunnel (all traffic via the VPN), two changes are needed. On the client, set AllowedIPs = 0.0.0.0/0. Also set a DNS = line on the client now (e.g. DNS = 1.1.1.1 or your own resolver): once all traffic runs through the tunnel, the DNS leak past the tunnel is in fact the normal case, not the exception – without this line your device keeps querying the DNS servers of your local network (details in the DNS leak entry in the troubleshooting part). On the server, enable routing plus NAT – first turn on IP forwarding permanently:

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

Then add the forwarding and NAT rules to the [Interface] section of wg0.conf (replace eth0 with your real external interface, see ip route get 1.1.1.1):

INI
PostUp = iptables -I FORWARD 1 -i wg0 -j ACCEPT; iptables -I FORWARD 1 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

Docker on the same server? Then the rules must go to the top

If Docker also runs on the server (e.g. your Traefik), Docker sets the FORWARD policy to DROP and pushes its own chains in front. If you append the WireGuard rules with -A (append), the return path of your reply packets is dropped by Docker – the handshake works, but no internet arrives. That’s why above we use -I FORWARD 1 (insert at the very front) and the RELATED,ESTABLISHED rule for the return path. The same applies with UFW active, since it too sets a FORWARD default policy of DROP – the -I FORWARD 1 overrides it, so the rules take effect in both cases. Tested exactly like this: after that, the client goes onto the net with the server’s IP.

After sudo wg-quick down wg0 && sudo wg-quick up wg0 you check the full tunnel by querying your public IP on the client – it should now be the server IP:

Terminal
curl -s ifconfig.me

Step 7: Make a service reachable only via the VPN

Now the real payoff. A service only you need doesn’t have to sit publicly behind Traefik – you bind it to the VPN IP of the server (10.8.0.1), then it’s reachable exclusively through the tunnel, without a single port on the internet.

For a Docker service that means: publish the port not to 0.0.0.0, but specifically to the VPN IP. So instead of - "8080:80":

YAML
    ports:
      - "10.8.0.1:8080:80"

The service thus listens only on the WireGuard interface – it’s invisible from the internet, and via the VPN you reach it at http://10.8.0.1:8080. (Important: wg0 must already be up when the container starts – the systemctl enable from step 4 ensures that.)

By the same principle you can also allow SSH only via the VPN: in UFW, remove the public SSH access and instead allow it only from the VPN subnet –

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

– then delete the public SSH rule. Be sure to test a second connection via the VPN beforehand, or you’ll lock yourself out (in an emergency the VNC console in the netcup SCP helps).

When things go wrong

A latest handshake never appears in wg show. The UDP port isn’t reachable or the keys don’t match. WireGuard is silent with wrong keys – there’s no error message, just no handshake. Check: sudo ufw status (port 51820/udp open?), the netcup firewall (inbound UDP 51820), the Endpoint in the client (correct public IP?) and that the private and public keys aren’t swapped.

Handshake is there, but no internet arrives in the full tunnel. Almost always the routing/NAT. Is net.ipv4.ip_forward=1 set (sysctl net.ipv4.ip_forward)? If Docker is running – or UFW is active – the FORWARD DROP trap from the warning box above applies; both set the forward policy to DROP. The rules must sit before the existing chains with -I FORWARD 1. Is the external interface in the MASQUERADE correct (eth0 vs. something else)?

Full tunnel is up, but DNS queries still run past the tunnel (DNS leak). Without a DNS = line, your device keeps querying the DNS servers of your local network – on open Wi-Fi the operator still sees which domains you visit. On Linux, check with resolvectl status which DNS server is assigned to the wg0 interface, or run a leak test (e.g. on dnsleaktest.com): if foreign resolvers appear there, enter a real resolver in the client config (DNS = 1.1.1.1) – or the server itself (10.8.0.1) if a dedicated DNS resolver runs there.

The tunnel is up, but large transfers (SSH, HTTPS, downloads) hang or break off. An MTU problem. wg-quick sets the MTU to 1420; in some networks (DS-Lite, certain mobile networks) that’s still too high. Try MTU = 1412 or 1280 in the [Interface] section of the client.

wg-quick up reports resolvconf: command not found. The DNS = line needs resolvconf. Either install sudo apt install openresolv or remove the DNS line if you don’t need the VPN DNS.

The connection falls asleep as soon as the phone briefly sends nothing. The client sits behind NAT/CGNAT. PersistentKeepalive = 25 in the client config keeps the connection open.

Maintenance & backups

  • Add more devices. One key pair per device and another [Peer] block in wg0.conf with the next free IP (10.8.0.3/32 …). Never use the same key on two devices. So that existing tunnels don’t drop in the process, reload the changed config live instead of doing down/up:

    Terminal
    sudo wg syncconf wg0 <(wg-quick strip wg0)
  • Back up the keys – they are the heart of the VPN. Include /etc/wireguard/ in your encrypted Restic backup. Whoever has the private keys gets into the VPN – treat them like passwords.

  • Keep it current. wireguard-tools gets its updates via automatic security updates; the kernel module comes with the kernel updates.

  • Bind internal services to the VPN. The real payoff: services no one from the internet should reach, you let listen only on 10.8.0.1 (instead of 0.0.0.0) – then they’re reachable exclusively through the tunnel, without any public port.

  • On a server migration, carry the new public IP over into the Endpoint of every client – otherwise the client no longer finds the server.

  • Honest about the effort: once set up, WireGuard runs practically maintenance-free. The only recurring task is creating a peer per new device.

Many devices? Then a mesh manager pays off

As long as you connect a handful of devices to one server, plain WireGuard here is exactly right – full control, nothing runs via third parties. But as soon as you want to network many devices with each other (laptop ↔ phone ↔ several servers), the manual peer maintenance gets tedious. Then it’s worth a look at Tailscale or the self-hosted Headscale: both build on WireGuard and only take over the key exchange and the networking automatically. You’ll still understand the foundation from this tutorial – it’s the same underneath.

What's next?

You might also like

Restic backups: encrypted and off-site
Security Intermediate

Restic backups: encrypted and off-site

Off-site backups with Restic: set up encrypted, restore snapshots, prune with retention and automate via a systemd …

· 12 min read