You have a VPS. You have a home lab. You have services scattered across both. They need to talk to each other securely, without exposing ports to the internet.

WireGuard is the answer. And it's simpler than you think — once you understand what's actually happening.


What WireGuard Actually Is

WireGuard is a VPN implemented as a Linux kernel module. Not an app. Not a daemon you configure with XML. A kernel module — which means it's fast, it's lean, and it lives at the same level as your network stack.

The entire codebase is around 4,000 lines of C. OpenVPN is 600,000+. That's not just a fun fact — smaller code means smaller attack surface, easier auditing, fewer places for bugs to hide.

It uses modern cryptography with no configuration choices: Curve25519 for key exchange, ChaCha20 for encryption, Poly1305 for authentication. You don't pick a cipher suite. You don't negotiate algorithms. There's one way to do it, and it's the right way.

The performance difference versus OpenVPN or IPsec is significant. WireGuard runs in kernel space. OpenVPN runs in userspace and copies data back and forth across the kernel boundary. On a modern machine, WireGuard throughput is typically 3-5x faster.


The Mental Model

WireGuard thinks in interfaces and peers.

You create a WireGuard interface on a machine (like wg0). That interface has a private key and a virtual IP address inside your tunnel network.

A peer is any other machine you want to communicate with. Each peer has a public key and a list of IP addresses that are allowed to route through them.

That's it. No server/client distinction at the protocol level. No certificates, no certificate authority, no revocation lists. Just key pairs and allowed IP ranges.

The handshake is silent. WireGuard doesn't send keepalives unless you configure them. A connection exists when packets flow. If nothing's been sent in 3 minutes, the session expires — and resumes instantly when traffic starts again.


What We're Building

A hub-and-spoke setup:

[Home Lab] ──── wg tunnel ────▶ [VPS / Hub]
      │                            │
10.0.0.1/24                       

[Home Lab] = 10.0.0.2
[VPS] = 10.0.0.1

The VPS is the hub with a public IP. The home lab connects out to it. Once connected, both sides can reach each other's services over 10.0.0.x addresses — private, encrypted, no open ports exposed.


Install WireGuard

On Debian/Ubuntu (kernel 5.6+, WireGuard is built in):

apt install wireguard wireguard-tools

On older kernels:

apt install wireguard wireguard-dkms

Check it's available:

modprobe wireguard
lsmod | grep wireguard

Generate Key Pairs

Do this on both machines — the VPS and the home lab.

# Generate private key
wg genkey | tee /etc/wireguard/private.key

# Derive public key from private key
cat /etc/wireguard/private.key | wg pubkey | tee /etc/wireguard/public.key

# Lock down the private key
chmod 600 /etc/wireguard/private.key

Keep the private key private. The public key is what you share with peers.

If your private key leaks, generate a new pair and update your configs.

You'll end up with:

  • VPS private key + VPS public key
  • Home lab private key + home lab public key

Configure the VPS (Hub)

Create /etc/wireguard/wg0.conf on your VPS:

[Interface]
# The VPS's private key
PrivateKey = <VPS_PRIVATE_KEY>
# Virtual IP for the VPS inside the tunnel
Address = 10.0.0.1/24
# Port WireGuard listens on (open this in your firewall)
ListenPort = 51820

# Enable IP forwarding for routing between peers
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT

[Peer]
# The home lab's public key
PublicKey = <HOMELAB_PUBLIC_KEY>
# Which IPs are allowed to come from this peer
AllowedIPs = 10.0.0.2/32

AllowedIPs does double duty. It's a routing rule and an access control list combined:

  • Inbound: only accept packets from this peer if they claim to be from 10.0.0.2
  • Outbound: route packets destined for 10.0.0.2 through this peer

That PostUp/PostDown block enables IP forwarding so packets can route between peers. If you're only doing point-to-point (VPS talks to home lab, not routing between multiple peers), you can skip it.

Open the port in your firewall:

ufw allow 51820/udp

# or
firewall-cmd --add-port=51820/udp --permanent
firewall-cmd --reload

Configure the Home Lab (Peer)

Create /etc/wireguard/wg0.conf on your home lab:

[Interface]
# The home lab's private key
PrivateKey = <HOMELAB_PRIVATE_KEY>
# Virtual IP for the home lab inside the tunnel
Address = 10.0.0.2/24

[Peer]
# The VPS's public key
PublicKey = <VPS_PUBLIC_KEY>
# The VPS's real public IP and WireGuard port
Endpoint = <VPS_PUBLIC_IP>:51820
# Route all tunnel traffic through the VPS
# Or scope it: AllowedIPs = 10.0.0.0/24 (only tunnel traffic)
AllowedIPs = 10.0.0.0/24
# Send a keepalive every 25 seconds
# Needed if you're behind NAT (home lab usually is)
PersistentKeepalive = 25

PersistentKeepalive is important for the home lab side. Your home router does NAT — it maps your internal IP to a public IP and tracks connections. If WireGuard goes quiet for too long, the NAT table entry expires and the VPS can no longer reach you.

A keepalive packet every 25 seconds keeps that entry alive.

The VPS doesn't need a keepalive because it has a static public IP and no NAT.


Bring It Up

On both machines:

# Start the interface
wg-quick up wg0

# Check status
wg show

wg show is your best friend. It tells you:

  • Your public key
  • The listening port
  • Each peer's public key, endpoint, allowed IPs, last handshake time, and traffic counters
interface: wg0
  public key: ABC123...
  private key: (hidden)
  listening port: 51820

peer: XYZ789...
  endpoint: 1.2.3.4:51820
  allowed ips: 10.0.0.2/32
  latest handshake: 14 seconds ago
  transfer: 1.23 MiB received, 456 KiB sent

If latest handshake shows a time, you have a working connection. If it shows nothing, the handshake never happened — see the troubleshooting section below.

Test connectivity:

# From home lab, ping the VPS
ping 10.0.0.1

# From VPS, ping the home lab
ping 10.0.0.2

Make It Persistent with systemd

wg-quick is a convenience wrapper. Under the hood it creates the interface, sets up routes, and runs your PostUp commands.

systemd knows how to manage it.

# Enable on both machines
systemctl enable --now wg-quick@wg0

That @wg0 is a systemd template unit. The [email protected] file uses %i as the interface name. So wg-quick@wg0 manages wg0, wg-quick@wg1 would manage wg1, and so on.

Check status:

systemctl status wg-quick@wg0
journalctl -u wg-quick@wg0 -f

Now WireGuard comes up at boot, managed by systemd, with logs in the journal.


AllowedIPs: The Part That Confuses Everyone

This is where most WireGuard setups go wrong.

AllowedIPs controls routing. Whatever you put there gets added to your routing table pointing at that peer.

On the home lab config:

AllowedIPs = 10.0.0.0/24

This means: "route all traffic destined for 10.0.0.x through the VPS peer."

Only tunnel traffic goes through WireGuard. Everything else (your regular internet traffic) uses your normal default route.

AllowedIPs = 0.0.0.0/0

This means: "route ALL traffic through the VPS peer."

Your internet traffic now exits from the VPS's IP address. This is the "full tunnel" or "replace default gateway" mode — the same as a traditional VPN.

AllowedIPs = 10.0.0.0/24, 192.168.1.0/24

Split tunnel — route tunnel traffic AND a specific subnet through the peer. Useful if you want to reach the VPS's local network.

Most self-hosted setups want 10.0.0.0/24 — only the tunnel subnet routes through WireGuard. You keep your regular internet route untouched.


Adding More Peers

WireGuard scales naturally. To add a third machine (say, a remote server):

Generate its key pair, then add a [Peer] block to the VPS config:

[Peer]
PublicKey = <REMOTE_SERVER_PUBLIC_KEY>
AllowedIPs = 10.0.0.3/32

And the remote server's config points at the VPS:

[Interface]
PrivateKey = <REMOTE_SERVER_PRIVATE_KEY>
Address = 10.0.0.3/24

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
Endpoint = <VPS_PUBLIC_IP>:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25

Reload without dropping existing connections:

wg addpeer wg0 <PUBLIC_KEY> --allowed-ips 10.0.0.3/32

# or reload the config file
wg syncconf wg0 <(wg-quick strip wg0)

wg syncconf applies config changes without tearing down the interface. No dropped connections for existing peers.


Troubleshooting

Handshake never happens

Check the basics first:

# Is the port open on the VPS?
ss -ulnp | grep 51820

# Can the home lab reach the VPS port?
nc -zu <VPS_IP> 51820

# Is the WireGuard interface actually up?
ip addr show wg0

The most common causes:

  • Firewall blocking UDP 51820 on the VPS
  • Wrong public key (copied the private key instead)
  • The Endpoint IP is wrong or unreachable

Handshake succeeds but traffic doesn't flow

# Watch packets on the interface
tcpdump -i wg0 -n

# Check your routing table
ip route show table main
ip route show table 51820  # wg-quick creates a separate table

Usually means AllowedIPs is misconfigured — you're routing to an address that doesn't match what's in the peer's allowed list.

Home lab can't be reached from VPS

The NAT keepalive isn't working, or the initial connection was never made. The home lab must connect first — it knows the VPS endpoint. The VPS only learns the home lab's endpoint after the first handshake.

Check if keepalive is set on the home lab side:

wg show wg0

Look for persistent keepalive: every 25 seconds in the peer entry.

Wrong IP forwarding

If you're routing between peers and it's not working:

sysctl net.ipv4.ip_forward  # Should be 1

# Make it permanent
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/99-wireguard.conf
sysctl -p /etc/sysctl.d/99-wireguard.conf

Real Use Cases

Access self-hosted services without exposing ports

Your Jellyfin, Grafana, or Home Assistant runs on your home lab. Instead of port-forwarding to the internet, you VPN in through WireGuard and access it via 10.0.0.2:8096. Nothing exposed publicly.

Secure inter-service communication

Your VPS runs a reverse proxy. Your home lab runs the actual app. They talk over 10.0.0.x. No public ports open on either side for internal traffic.

Road warrior access

Install WireGuard on your phone. Add it as a peer on the VPS. Now you have a private tunnel back to your infrastructure from anywhere. The official WireGuard apps (iOS/Android) use the same config format.


Honest Verdict

WireGuard is the first VPN technology that doesn't feel like a compromise.

The setup is fast, the config is readable, the performance is excellent, and the failure modes are understandable.

The only thing it doesn't have is a management UI built in. If you're managing many peers, look at tools like wg-easy (a Docker-based web UI) or pivpn for an automated installer.

But for a handful of machines, raw config files are fine — they're simple enough to read and audit.

If you're still running OpenVPN because "it works," it's worth the hour to migrate. You won't go back.


Go Try It

apt install wireguard wireguard-tools
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
chmod 600 /etc/wireguard/private.key
cat /etc/wireguard/public.key

Generate keys on both machines. Share the public keys. Write the configs. Run wg-quick up wg0 and watch wg show until you see a handshake.

The whole thing takes under 30 minutes if you've done it before. Maybe an hour your first time.

Either way, you come out with a private network that actually makes sense.


Compiled by AI. Proofread by caffeine. ☕