WireGuard VPN: Set Up Your Own Server Without Opening Unnecessary Holes

Was this helpful?

You want a VPN because your laptop roams through coffee-shop Wi‑Fi, hotel networks, and whatever that “FREE_AIRPORT_WIFI” thing is. You also want to self-host because you’ve read enough breach reports to know that “trust us” is not a security model.

The trap: people treat “set up a VPN” as “open a bunch of ports and hope.” That’s how you get a server that’s reachable, sure—by attackers, scanners, and your future self at 3 a.m. wondering why routing is broken. We’re going to do this the boring, correct way: one UDP port exposed (or none if you use an outbound-only path), a tight firewall, explicit routing, and a diagnosis routine that doesn’t rely on vibes.

Security posture: “one port, one job”

WireGuard is not a suite of daemons and plugins. It’s a small protocol and a small implementation. Treat it like a single-purpose appliance:
expose one UDP port (if you must), accept packets only on that port, from anywhere, and let WireGuard’s cryptographic handshake do the heavy lifting.

Your firewall is not there to “make the VPN secure.” Your firewall is there to keep you from accidentally exposing everything else while you’re busy being clever. Do not be clever.

What “unnecessary holes” usually looks like

  • Opening SSH to the world “just for now,” then forgetting for a year.
  • Forwarding random ports because a blog post said so, even though WireGuard needs one UDP port.
  • Allowing forwarding from the VPN interface to your LAN with no policy, because “it’s encrypted.”
  • Installing a web UI that runs as root because it “simplifies management.”

Encryption isn’t permission. It’s transport. Permission is routing and policy.

One quote, because it’s still true

“Hope is not a strategy.” — James Cameron

It’s not an ops quote, but it works: don’t hope your firewall is fine; verify it.

WireGuard facts and context (the stuff people misremember)

These are short on purpose. The point is to calibrate your mental model so you stop troubleshooting the wrong layer.

  1. WireGuard is UDP-only by design. If you’re chasing “TCP retries,” you’re debugging the wrong thing.
  2. It uses a Noise-based handshake (NoiseIK). That’s one reason it’s fast and relatively simple compared to older VPN stacks.
  3. It landed in the Linux kernel in 5.6 (2020). Before that, it ran out-of-tree; now it’s a first-class citizen on modern distros.
  4. “Peers” are not “users.” A peer is a keypair and routing policy; identity lives in keys, not usernames.
  5. AllowedIPs is both routing and access control. It determines what routes get installed and what traffic is accepted for a peer.
  6. NAT traversal is not magic; it’s keepalives. PersistentKeepalive is basically you politely knocking every N seconds so stateful firewalls don’t forget you.
  7. WireGuard does not have renegotiation the way IPsec does. Keys rotate, but the model is intentionally minimal.
  8. It avoids algorithm sprawl. The crypto choices are deliberately narrow; you don’t get to “pick whatever you like,” which is a feature, not a bug.

Joke #1: A VPN that “supports every cipher” is like a restaurant with a 40-page menu—nobody’s checking freshness.

Choose your topology: VPS hub, home hub, or “no inbound ports”

The cleanest way to avoid unnecessary holes is to decide what you’re trying to connect. There are three common patterns, and they’re not interchangeable.

Topology A: VPS as a hub (recommended for most people)

You rent a small VPS, expose one UDP port (e.g., 51820/udp), and connect clients to it. If you want access to home devices, you also run a WireGuard peer at home that dials out to the VPS and advertises your home routes.

Why it’s good:

  • You don’t need to expose your home IP or punch holes in a flaky ISP router.
  • You get a stable public endpoint and predictable firewalling.
  • You can keep SSH locked down to a management network or a bastion.

Topology B: Home server as the hub (works, but you’re on the hook for the router)

You port-forward one UDP port from your home router to your WireGuard server. It’s fine when it’s fine. It’s also one firmware update away from “why did the router reset all my rules.”

Do this only if you understand your home router’s stateful firewall behavior and you can tolerate occasional outages.

Topology C: “No inbound ports”: outbound-only using a rendezvous

If your environment forbids inbound ports (corporate networks, CGNAT, hostile ISPs), you can still build a working system. The usual approach is:

  • A public VPS runs the WireGuard server (one UDP port open on the VPS).
  • Everything else (home gateway, laptops) connects out to that VPS.

Note what’s happening: you didn’t eliminate exposure; you moved it to a controlled public box and kept home inbound closed. That’s usually the right trade.

Hardening decisions that actually matter

1) Put the WireGuard endpoint on a minimal host

Don’t co-host your VPN endpoint with your pet Kubernetes cluster, your family photo gallery, and an experimental Node app. Every additional service multiplies your blast radius. If you need a Swiss Army knife, fine. But don’t act surprised when you cut yourself.

2) Restrict SSH like you mean it

SSH is usually the real “unnecessary hole.” If this server is reachable on the public internet, default to:

  • SSH keys only, no password auth.
  • Firewall SSH to a trusted IP range (office, home, or your VPN subnet).
  • Consider a separate management interface or a bastion host if you’re running this for a team.

3) Firewall policy: allow WireGuard, allow established, drop the rest

The basic ruleset is boring and sufficient:

  • Inbound: allow UDP 51820 (or your chosen port) to the WireGuard host.
  • Inbound: allow SSH only from a restricted source.
  • Forwarding: allow only what you intend from wg0 to other interfaces.

The VPN interface is not a free pass to your LAN. It is a network segment. Treat it like one.

4) Routing policy is your access control

WireGuard doesn’t do “user permissions” like an enterprise VPN concentrator. Your control knobs are:

  • AllowedIPs per peer (what routes a peer is allowed to send and receive).
  • Firewall rules on wg0 (what traffic is permitted once it arrives).
  • IP forwarding / NAT decisions (what the server will route onward).

5) MTU: the silent killer

WireGuard is fast. It’s also happy to drop your throughput into a ditch if you’re encapsulating inside PPPoE, VLANs, or other tunnels and you don’t adjust MTU. When in doubt, start with 1420 on wg0 and measure.

6) Logging: enough to diagnose, not enough to leak

WireGuard is intentionally quiet. That’s good. But you still need system-level observability:

  • Interface counters and routes.
  • Firewall packet counters.
  • Kernel logs for dropped packets (carefully).

Checklists / step-by-step plan

Checklist: before you touch the server

  • Decide your VPN subnet (example: 10.6.0.0/24). Don’t reuse a LAN subnet.
  • Decide split tunnel vs full tunnel per client. Default to split tunnel unless you need “all traffic through VPN.”
  • Pick your endpoint port (default 51820/udp is fine). Security by obscurity is not a plan, but noise reduction is okay.
  • Write down which private subnets you want reachable (example: 192.168.50.0/24 at home).
  • Decide if the server should NAT client traffic to the internet (full tunnel) or just route to internal networks.

Step-by-step: VPS hub on Ubuntu with nftables

This flow assumes:

  • Server public interface: eth0
  • WireGuard interface: wg0
  • VPN subnet: 10.6.0.0/24
  • Server VPN IP: 10.6.0.1
  • Client VPN IP: 10.6.0.2

Checklist: what you should end up with

  • Only UDP 51820 inbound exposed (plus tightly restricted SSH if needed).
  • wg0 up at boot, configuration owned by root, permissions locked down.
  • IP forwarding enabled only if you actually route between interfaces.
  • Firewall rules that explicitly allow the traffic you want, and nothing else.
  • Client configs that match AllowedIPs, endpoint, and DNS choices.

Practical tasks: commands, outputs, and what you decide

This section is deliberately hands-on: command, realistic output, and then the decision you make. If you skip the “decision” part, you’re just typing spells.

Task 1: confirm WireGuard support and basic OS facts

cr0x@server:~$ uname -r
6.5.0-41-generic

What it means: Modern kernel; WireGuard in-kernel support is available.
Decision: Use wireguard-tools and the kernel module; avoid userspace shims unless you have a weird platform constraint.

Task 2: install WireGuard tooling

cr0x@server:~$ sudo apt-get update
Hit:1 http://archive.ubuntu.com/ubuntu jammy InRelease
Reading package lists... Done
cr0x@server:~$ sudo apt-get install -y wireguard wireguard-tools nftables
Reading package lists... Done
Setting up wireguard-tools (1.0.20210914-1ubuntu2) ...
Setting up wireguard (1.0.20210914-1ubuntu2) ...
Setting up nftables (1.0.2-1ubuntu1) ...

What it means: Tools are present. Decision: Proceed with systemd-managed interface and nftables rules. If you’re on a distribution that defaults to ufw, decide now whether you’ll stick with it or disable it; mixing firewalls is a hobby, not a career.

Task 3: generate server keys with correct permissions

cr0x@server:~$ umask 077
cr0x@server:~$ sudo mkdir -p /etc/wireguard
cr0x@server:~$ sudo bash -c 'wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub'
cr0x@server:~$ sudo ls -l /etc/wireguard
total 8
-rw------- 1 root root 45 Oct 12 10:20 server.key
-rw------- 1 root root 45 Oct 12 10:20 server.pub

What it means: Keys exist and are not world-readable.
Decision: If permissions are broader than 600, fix them; don’t proceed with sloppy key handling.

Task 4: write the server interface config

cr0x@server:~$ sudo bash -c 'cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.6.0.1/24
ListenPort = 51820
PrivateKey = '"$(sudo cat /etc/wireguard/server.key)"'
SaveConfig = false

# If you will route to other networks, enable forwarding and add firewall rules (below).
EOF'
cr0x@server:~$ sudo chmod 600 /etc/wireguard/wg0.conf

What it means: Server is configured with an internal address and a UDP port.
Decision: Keep SaveConfig=false unless you want runtime changes written back automatically. Auto-writing configs is convenient until it isn’t.

Task 5: bring up the interface and verify it exists

cr0x@server:~$ sudo systemctl enable --now wg-quick@wg0
Created symlink /etc/systemd/system/multi-user.target.wants/wg-quick@wg0.service → /lib/systemd/system/wg-quick@.service.
cr0x@server:~$ ip a show wg0
6: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none
    inet 10.6.0.1/24 scope global wg0
       valid_lft forever preferred_lft forever

What it means: The interface is up with MTU 1420 (wg-quick default).
Decision: If your environment needs a different MTU (common with PPPoE), set it explicitly now rather than chasing ghosts later.

Task 6: confirm the server is listening on the chosen UDP port

cr0x@server:~$ sudo ss -lunp | grep 51820
UNCONN 0      0           0.0.0.0:51820       0.0.0.0:*    users:(("wireguard",pid=1123,fd=6))

What it means: A UDP socket is open and bound.
Decision: If it’s not listening, stop and inspect journalctl for wg-quick failures before you touch firewall rules.

Task 7: configure nftables to allow only what you need

cr0x@server:~$ sudo bash -c 'cat > /etc/nftables.conf <<EOF
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority 0;
    policy drop;

    ct state established,related accept
    iif "lo" accept

    # Allow WireGuard
    udp dport 51820 accept

    # Allow SSH only from a trusted source (example: your office IP)
    ip saddr 203.0.113.10 tcp dport 22 accept

    # Optional: allow ICMP for debugging (rate-limit in real environments)
    ip protocol icmp accept
    ip6 nexthdr icmpv6 accept

    counter reject with icmpx type port-unreachable
  }

  chain forward {
    type filter hook forward priority 0;
    policy drop;

    ct state established,related accept
    # Allow VPN clients to reach the server itself is handled in input;
    # forward is for routed traffic to other interfaces.

    # Example: allow VPN clients to reach a private LAN behind this server
    # iif "wg0" oif "eth0" ip daddr 192.168.50.0/24 accept
    # iif "eth0" oif "wg0" ip saddr 192.168.50.0/24 accept
  }

  chain output {
    type filter hook output priority 0;
    policy accept;
  }
}
EOF'
cr0x@server:~$ sudo systemctl enable --now nftables
cr0x@server:~$ sudo nft list ruleset | sed -n '1,60p'
table inet filter {
	chain input {
		type filter hook input priority filter; policy drop;
		ct state established,related accept
		iif "lo" accept
		udp dport 51820 accept
		ip saddr 203.0.113.10 tcp dport 22 accept
		ip protocol icmp accept
		ip6 nexthdr icmpv6 accept
		counter reject with icmpx type port-unreachable
	}

What it means: Default drop, explicit allow. That’s what you want on an internet-exposed host.
Decision: If you can’t restrict SSH by source IP, strongly consider moving SSH behind the VPN instead (meaning: no public SSH at all).

Task 8: verify you didn’t accidentally expose other services

cr0x@server:~$ sudo ss -lntup
Netid State  Recv-Q Send-Q Local Address:Port   Peer Address:Port Process
udp   UNCONN 0      0      0.0.0.0:51820       0.0.0.0:*     users:(("wireguard",pid=1123,fd=6))
tcp   LISTEN 0      4096   0.0.0.0:22          0.0.0.0:*     users:(("sshd",pid=904,fd=3))

What it means: Only SSH and WireGuard are listening. Good.
Decision: If you see surprise listeners (web dashboards, exporters, random agents), remove or firewall them. “But it’s bound to 0.0.0.0” is not a personality trait.

Task 9: generate a client keypair and add a peer

cr0x@server:~$ wg genkey | tee /tmp/client1.key | wg pubkey > /tmp/client1.pub
cr0x@server:~$ sudo wg set wg0 peer $(cat /tmp/client1.pub) allowed-ips 10.6.0.2/32
cr0x@server:~$ sudo wg show wg0
interface: wg0
  public key: 3rYlF8wF1aV9rS8dK0aJ7xw5w2Vn9pYqHc0Rr3X0u2M=
  private key: (hidden)
  listening port: 51820

peer: 7bTt2yqg3fJ0zq5fQm2v0QwqYk9mM9t8Gq2dZ1gk3jA=
  allowed ips: 10.6.0.2/32

What it means: The server now knows a peer key and which VPN IP it owns.
Decision: Keep AllowedIPs tight: /32 per client unless you’re deliberately doing site-to-site routing. Wide AllowedIPs cause accidental routing leaks and peer conflicts.

Task 10: create the client configuration (split tunnel default)

cr0x@server:~$ SERVER_PUB=$(sudo cat /etc/wireguard/server.pub); echo "$SERVER_PUB"
3rYlF8wF1aV9rS8dK0aJ7xw5w2Vn9pYqHc0Rr3X0u2M=
cr0x@server:~$ cat > /tmp/client1.conf <<EOF
[Interface]
PrivateKey = $(cat /tmp/client1.key)
Address = 10.6.0.2/32
DNS = 1.1.1.1

[Peer]
PublicKey = $(sudo cat /etc/wireguard/server.pub)
Endpoint = 198.51.100.25:51820
AllowedIPs = 10.6.0.0/24
PersistentKeepalive = 25
EOF

What it means: Split tunnel: the client only routes VPN subnet traffic into the tunnel.
Decision: Start with split tunnel. Turn on full tunnel only when you can articulate why you need it (and you’ve tested DNS behavior).

Task 11: check handshake and traffic counters on the server

cr0x@server:~$ sudo wg show
interface: wg0
  public key: 3rYlF8wF1aV9rS8dK0aJ7xw5w2Vn9pYqHc0Rr3X0u2M=
  private key: (hidden)
  listening port: 51820

peer: 7bTt2yqg3fJ0zq5fQm2v0QwqYk9mM9t8Gq2dZ1gk3jA=
  allowed ips: 10.6.0.2/32
  latest handshake: 24 seconds ago
  transfer: 18.21 KiB received, 22.77 KiB sent

What it means: Handshake succeeded and traffic is flowing.
Decision: If “latest handshake” is empty or ancient, debug reachability (firewall, endpoint, NAT) before you touch routes.

Task 12: verify routing table and policy on the server

cr0x@server:~$ ip route show
default via 203.0.113.1 dev eth0 proto dhcp src 198.51.100.25 metric 100
10.6.0.0/24 dev wg0 proto kernel scope link src 10.6.0.1
203.0.113.0/24 dev eth0 proto kernel scope link src 198.51.100.25

What it means: The server knows the VPN subnet is on wg0.
Decision: If the route to 10.6.0.0/24 is missing, wg0 isn’t configured correctly; do not proceed to NAT/routing “fixes” until the base interface is sane.

Task 13: enable IP forwarding only if you route beyond the server

cr0x@server:~$ sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0

What it means: The server is not routing packets between interfaces.
Decision: Leave it off unless you need it. If you’re doing full tunnel or site-to-site, turn it on deliberately and add firewall rules to match.

cr0x@server:~$ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1
cr0x@server:~$ sudo bash -c 'printf "net.ipv4.ip_forward=1\n" > /etc/sysctl.d/99-wireguard-forward.conf'

Task 14: add NAT for full-tunnel clients (only when you mean it)

If you want clients to use the VPS as their internet egress, you must NAT traffic from wg0 to eth0. Without it, you’ll get “connected but no internet.”

cr0x@server:~$ sudo nft add table ip nat
cr0x@server:~$ sudo nft 'add chain ip nat postrouting { type nat hook postrouting priority 100 ; }'
cr0x@server:~$ sudo nft add rule ip nat postrouting oif "eth0" ip saddr 10.6.0.0/24 masquerade
cr0x@server:~$ sudo nft list table ip nat
table ip nat {
	chain postrouting {
		type nat hook postrouting priority srcnat; policy accept;
		oif "eth0" ip saddr 10.6.0.0/24 masquerade
	}
}

What it means: Client traffic will egress with the server’s public IP.
Decision: If you don’t need full tunnel, don’t do this. NAT hides mistakes as “it works,” until you need site-to-site clarity.

Task 15: validate firewall counters during a test

cr0x@server:~$ sudo nft list chain inet filter input
chain input {
	type filter hook input priority filter; policy drop;
	ct state established,related accept
	iif "lo" accept
	udp dport 51820 accept
	ip saddr 203.0.113.10 tcp dport 22 accept
	ip protocol icmp accept
	ip6 nexthdr icmpv6 accept
	counter packets 12 bytes 672 reject with icmpx type port-unreachable
}

What it means: The reject counter increments when random internet noise hits you.
Decision: If counters for unexpected accepts are increasing (e.g., SSH), tighten rules. If UDP 51820 counters stay at zero while clients connect, you’re not actually reaching the host—check upstream security groups.

Task 16: packet capture for “handshake never happens”

cr0x@server:~$ sudo tcpdump -n -i eth0 udp port 51820 -c 5
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
12:44:10.112233 IP 203.0.113.77.49822 > 198.51.100.25.51820: UDP, length 148
12:44:10.212244 IP 203.0.113.77.49822 > 198.51.100.25.51820: UDP, length 148
5 packets captured

What it means: Packets arrive at the server. If wg still shows no handshake, the issue is likely keys, peer config, or AllowedIPs.
Decision: If tcpdump shows nothing, stop editing WireGuard configs and go fix reachability (cloud firewall/security group, NAT, port forward, wrong IP).

Routing and NAT: split tunnel vs full tunnel without self-sabotage

Split tunnel: minimal risk, minimal surprise

In split tunnel, the client routes only specific subnets to the VPN. Typical AllowedIPs on the client:

  • 10.6.0.0/24 to reach other VPN peers
  • 192.168.50.0/24 to reach your home LAN via a site-to-site peer

Pros:

  • Less chance of breaking client internet access.
  • Less bandwidth usage on the server.
  • Less “why is Netflix in another country” drama.

Cons: you’re not hiding all your traffic from the local network. If you need that (hostile Wi‑Fi), use full tunnel temporarily.

Full tunnel: powerful, breaks things faster

Full tunnel means the client routes 0.0.0.0/0 (and ::/0 for IPv6) through the VPN. That implies:

  • NAT or proper routing on the server.
  • DNS handled carefully to avoid leaks or outages.
  • MTU issues become more visible because everything uses the tunnel.

The client config line is the difference between “nice tool” and “whole network dependency”:

  • Split: AllowedIPs = 10.6.0.0/24, 192.168.50.0/24
  • Full: AllowedIPs = 0.0.0.0/0, ::/0

Site-to-site via VPS: the clean way to reach home without opening home ports

Run WireGuard on a small box at home (router, mini PC, NAS if you must) as a peer that connects outbound to the VPS.
On the VPS, you add a peer with AllowedIPs equal to your home LAN subnet. On the home peer, you add a route and firewall rules to forward between wg0 and lan0.

Two important guardrails:

  • Only one peer should “own” a given route in AllowedIPs. If two peers claim 192.168.50.0/24, traffic will go to whichever was added last (and you’ll hate your life).
  • On the home side, firewall forwarding is not optional. You are creating a router. Act like it.

Fast diagnosis playbook

When WireGuard “doesn’t work,” people flail: they change ports, reinstall packages, reboot, and then declare victory for the wrong reason. Here’s the sequence that finds the bottleneck quickly.

First: can packets reach the server’s UDP port?

  • On the server: ss -lunp | grep 51820 (is it listening?)
  • On the server during a connection attempt: tcpdump -n -i eth0 udp port 51820 (do packets arrive?)
  • In cloud environments: confirm the provider firewall/security group allows UDP 51820.

If packets don’t arrive, nothing else matters. Fix the network path.

Second: does WireGuard form a handshake?

  • wg show → check “latest handshake”
  • If there’s no handshake: wrong endpoint IP/port, wrong keys, or server-side firewall dropping UDP.

Third: can you pass traffic inside the tunnel?

  • Ping server’s wg IP from the client.
  • Server: watch wg show transfer counters increase.
  • If handshake exists but traffic fails, suspect AllowedIPs mismatch, firewall on wg0, or MTU.

Fourth: routing/NAT and DNS (the “connected but useless” category)

  • Split tunnel: ensure routes exist on the client for the private subnets you expect.
  • Full tunnel: ensure server has NAT masquerade and forwarding enabled.
  • DNS: ensure your client points to a resolver reachable via the chosen routing mode.

Fifth: performance bottlenecks

  • Check MTU and fragmentation symptoms first (slow HTTPS, stalls, some sites fail).
  • Check CPU saturation on the server (cheap VPS + high throughput can bottleneck).
  • Check path MTU and carrier-grade NAT behavior if on mobile networks.

Joke #2: VPN troubleshooting is like plumbing—99% of the time, the problem is where you didn’t look because it was “obvious.”

Common mistakes (symptoms → root cause → fix)

1) Symptom: “Handshake never happens”

Root cause: UDP port blocked upstream (cloud firewall, ISP, wrong port forward), or wrong endpoint IP/port in the client config.

Fix: Verify with ss that the server is listening, then tcpdump to see inbound UDP. If packets don’t arrive, fix upstream rules before touching WireGuard configs.

2) Symptom: “Handshake happens, but I can’t ping 10.6.0.1”

Root cause: Server firewall drops traffic on wg0 (input chain doesn’t allow it), or the client’s Address/AllowedIPs is wrong.

Fix: Allow input on wg0 or permit ICMP for debugging; confirm client Address is 10.6.0.2/32 and server peer AllowedIPs includes 10.6.0.2/32.

3) Symptom: “Connected, but no internet in full tunnel”

Root cause: Missing IP forwarding and/or NAT masquerade on the server.

Fix: Enable net.ipv4.ip_forward=1 and add nftables masquerade from wg subnet to the egress interface.

4) Symptom: “Some sites load, others hang (especially HTTPS)”

Root cause: MTU mismatch causing fragmentation/blackholing.

Fix: Reduce wg0 MTU (try 1420 → 1380 → 1360). If using PPPoE or double tunnels, you often need smaller.

5) Symptom: “One client knocks another offline”

Root cause: Two peers configured with overlapping AllowedIPs (e.g., both claim 10.6.0.2/32 or both claim the same LAN subnet).

Fix: Make peer AllowedIPs unique. Use /32 per client. For site subnets, ensure only one peer advertises each subnet.

6) Symptom: “VPN works from home Wi‑Fi but not from mobile”

Root cause: Mobile carrier NAT/stateful firewall timing out UDP mappings; no keepalive set.

Fix: Set PersistentKeepalive = 25 on the client. Don’t set it on the server for road-warrior clients; the client is the one behind NAT.

7) Symptom: “VPN is up, but I can’t reach my home LAN via the VPS”

Root cause: Home peer is not forwarding between its LAN and wg0, or return routes are missing on LAN devices.

Fix: Enable forwarding and add firewall rules on the home gateway. Consider NAT on the home gateway for simplicity if you can’t add return routes.

8) Symptom: “DNS leaks or weird split-brain resolution”

Root cause: Client is using local DNS despite routing internal names via VPN, or you pointed DNS to an internal resolver not reachable in split tunnel.

Fix: In split tunnel, either keep public DNS or route to your internal DNS and include its subnet in AllowedIPs. In full tunnel, set DNS explicitly and verify it’s reachable through the tunnel.

Three corporate-world mini-stories

Mini-story 1: The incident caused by a wrong assumption

A mid-sized company wanted a quick VPN for on-call engineers. The plan was “simple”: deploy WireGuard on a cloud VM, let engineers connect, then route to production subnets. The engineer implementing it assumed the cloud security group was already permissive for UDP because “we allow web traffic.”

On Monday night, an on-call tried to connect during an incident. The client showed “connected” in the GUI, because the interface came up locally. But the server never saw a handshake. People chased keys, configs, and MTU for an hour, because those are the knobs you can turn without asking anyone else.

The real issue: UDP 51820 was blocked at the provider firewall layer. The server was listening. The OS firewall was correct. But packets never arrived. The team had treated “server firewall” as the only firewall.

The fix was trivial once identified: add one inbound UDP rule at the cloud edge. The lesson was expensive: validate reachability first, and document where policy actually lives. Cloud networks are layered, and each layer is a chance to be confidently wrong.

Mini-story 2: The optimization that backfired

Another org ran WireGuard as a hub for developers. Someone noticed that adding more peers and routes made the configs hard to manage. Their “optimization” was to widen AllowedIPs everywhere: instead of /32 per client, they used 10.6.0.0/24 for each peer on the server, figuring it would reduce churn.

It worked until it didn’t. A developer’s laptop connected and silently hijacked traffic destined for other peers. Not maliciously—just because WireGuard now believed that peer could legitimately send traffic for the whole subnet. The routing became nondeterministic based on peer insertion order and interface state.

The symptoms were wonderfully chaotic: two engineers could connect fine, the third could connect but couldn’t reach internal services, and sometimes the problem “fixed itself” after restarting wg0. Those are the best outages because they teach humility and profanity at the same time.

The rollback was to revert to strict AllowedIPs: /32 per road-warrior, and only specific subnets for site peers. Management got slightly more tedious. The network stopped being haunted. That’s a good trade.

Mini-story 3: The boring but correct practice that saved the day

A financial services team ran WireGuard for a small set of automation agents in multiple VPCs. Their setup was unglamorous: one tiny VM as the hub, one UDP port open, SSH only from a dedicated management subnet, and nftables rules with explicit forward policies. They also kept a runbook: “reachability → handshake → routes → DNS.”

During a provider incident, packet loss spiked and latencies went sideways. The automation agents started failing health checks. The team didn’t panic and they didn’t redeploy. They pulled their runbook and executed it like they were paying rent on it.

They quickly proved: UDP packets were arriving, handshakes were recent, and transfer counters were increasing. That eliminated most local causes. Then they checked interface errors and MTU: clean. At that point, they escalated to the provider with evidence instead of feelings.

The provider issue resolved later, but the key point is that the team avoided self-inflicted wounds. The boring practice wasn’t “having WireGuard.” It was having a repeatable diagnostic flow, plus firewall policy that didn’t change during stress.

FAQ

1) Do I need to change the default WireGuard port for security?

Not for security. WireGuard’s security comes from keys, not port numbers. Changing the port can reduce random scanning noise. If you change it, change it everywhere and document it.

2) Can I run WireGuard and keep SSH closed to the internet?

Yes, and it’s often the right move. Bring up WireGuard first via a console/out-of-band method, then restrict SSH to the VPN subnet (or to a bastion). If you can’t guarantee you won’t lock yourself out, keep a restricted management path.

3) What’s the minimum I must expose to the internet?

One UDP port to the WireGuard host (for example 51820/udp). That’s it. Everything else should be blocked or restricted to trusted sources.

4) How do I prevent a VPN client from reaching my whole LAN?

Two layers: keep the client’s AllowedIPs limited (don’t route the LAN to them), and enforce forwarding rules on the server (drop wg0 → lan traffic unless explicitly allowed).

5) Should I use NAT or routing for site-to-site?

Prefer routing when you control both ends and can add return routes; it’s cleaner and debuggable. Use NAT when you don’t control the LAN devices or can’t add routes; it’s pragmatic but hides network reality.

6) Why does “connected” not mean “working”?

Many clients show “connected” when the interface is up locally, even without a handshake. Trust wg show and packet counters, not UI status badges.

7) Do I need PersistentKeepalive?

For roaming clients behind NAT (mobile, hotel Wi‑Fi), yes—often. Set it on the client to something like 25 seconds. For a server with a public IP, usually no.

8) Is WireGuard safe for production?

Yes, with a sane deployment model: minimal exposed services, strict peer AllowedIPs, explicit firewalling, and a plan for key rotation and offboarding. Most “VPN incidents” are routing and policy failures, not WireGuard cryptography failures.

9) What about IPv6?

If your environment uses IPv6, treat it as first-class: assign IPv6 addresses on wg0, include ::/0 only if you mean full-tunnel IPv6, and write IPv6 firewall rules. Ignoring IPv6 is a common way to accidentally create “unnecessary holes” you never monitored.

10) How do I rotate keys without downtime?

Add a new peer key (or update a peer) while keeping the old one briefly, then remove the old key after clients confirm. In practice: stage changes, verify handshake, then prune. Don’t do “flag day” rotations unless you like surprise calls.

Conclusion: next steps you can do today

If you take one idea from this: expose one UDP port and be explicit about everything else. WireGuard is simple enough that you can keep the whole system in your head—until you start adding “helpful” shortcuts.

Practical next steps:

  1. Pick a topology (VPS hub is usually the least painful) and write down the subnets you will route.
  2. Implement a default-drop firewall with explicit allows for UDP 51820 and restricted SSH.
  3. Configure peers with tight AllowedIPs (/32 per client) and test handshake plus counters with wg show.
  4. If you need full tunnel, enable forwarding and NAT deliberately, then validate DNS behavior.
  5. Print (or paste) the Fast diagnosis playbook into your runbook. Your future self will send you a thank-you note in the form of fewer outages.
← Previous
ZFS SLOG: When a Log Device Helps, When It’s Useless, When It’s Dangerous
Next →
Docker Build Is Slow: BuildKit Caching That Actually Speeds It Up

Leave a comment