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.
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).
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.
💶 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):
sudo apt update
sudo apt install -y wireguard wireguard-tools qrencodeCheck that the kernel module can be loaded:
sudo modprobe wireguard && lsmod | grep wireguardwireguard 118784 0Step 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:
umask 077
wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pubThe 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:
wg genkey | tee client.key | wg pubkey > client.pubYou 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:
[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/32What the lines mean:
Address– the VPN-internal IP of the server (10.8.0.1); the/24spans the VPN subnet10.8.0.0/24.ListenPort– the UDP port WireGuard listens on.51820is the default.[Peer]+AllowedIPs = 10.8.0.2/32– this client may only appear under the VPN IP10.8.0.2. Important:AllowedIPshas 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:
sudo ufw allow 51820/udpIf 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:
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0Check that the interface is up:
sudo wg show wg0interface: wg0
public key: nZT4kozgZjtHVzGfPRL+niI4llEQCUqGpYG0ni+fAT4=
private key: (hidden)
listening port: 51820
peer: j5h2jl87JApAbWrJikrth3H/6fNuf6X2TgBgDSJVwwE=
allowed ips: 10.8.0.2/32The 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):
[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 = 25The 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 set0.0.0.0/0here – see step 6.DNS = 10.8.0.1– deliberately commented out. This line routes the entire system DNS of your device to10.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 like1.1.1.1instead.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”:
qrencode -t ansiutf8 < client.confOn 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:
sudo wg show wg0peer: 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 sentA latest handshake plus a growing transfer means the tunnel is up. From the client
you can now reach the server at its VPN IP:
ping 10.8.0.1If 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:
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.confThen 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):
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 MASQUERADEDocker on the same server? Then the rules must go to the top
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:
curl -s ifconfig.meStep 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":
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 –
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 inwg0.confwith 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 doingdown/up:Terminalsudo 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-toolsgets 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 of0.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
Endpointof 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
Send feedback: feedback@serverkueche.de
What's next?

AdGuard Home: network-wide ad and tracking blocker
Set up AdGuard Home with Docker: your own DNS server that blocks ads, trackers and malicious sites for all devices – …
You might also like

CrowdSec: modern, collaborative intrusion prevention
Set up CrowdSec with the Traefik bouncer: detect attacks from the access logs, block attackers with a 403 and benefit …

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

Setting up Fail2ban: block brute-force attacks automatically
Fail2ban watches your logs and bans IPs after too many failed attempts – with a safe whitelist for your own address so …