All systems operational
Support
EN
Language

More languages are on the way.

A WireGuard hub for your whole team on a $3 VPS

A hub-and-spoke VPN with per-device keys, split tunnelling and IPv6, in twenty lines of config and one systemd unit.

CNCheapServ NetworkWritten by 7 min read
A hub with tunnels of light connecting small devices
On this page7
  1. The design
  2. The hub, in ten commands
  3. Adding a device
  4. Private DNS, optionally
  5. Reaching servers behind the hub
  6. What a VPS-1 handles
  7. Operations

A team VPN used to mean a licence, a certificate authority and a week of onboarding. WireGuard replaced all of that with one UDP port and a key pair per device. This guide builds a hub-and-spoke VPN on a VPS-1, the cheapest plan we sell, with per-device keys, split tunnelling so only private traffic goes through the tunnel, and IPv6 inside the tunnel because there is no reason not to.

The design

The VPS is the hub. Every laptop, phone and office server is a spoke with its own key pair and its own tunnel address. Spokes reach each other through the hub, and they reach any private service you host behind it. Public traffic keeps using each device's normal connection, so nobody's video calls go the long way around.

  • Tunnel network: 10.66.0.0/24 and fd66::/64.
  • Hub: 10.66.0.1, listens on UDP 51820.
  • Spokes: 10.66.0.10 upward, one per device, never shared.

The hub, in ten commands

apt install -y wireguard
umask 077 && wg genkey | tee /etc/wireguard/hub.key | wg pubkey > /etc/wireguard/hub.pub
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.66.0.1/24, fd66::1/64
ListenPort = 51820
PrivateKey = $(cat /etc/wireguard/hub.key)
EOF
sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
echo -e "net.ipv4.ip_forward=1\nnet.ipv6.conf.all.forwarding=1" > /etc/sysctl.d/70-wg.conf
ufw allow 51820/udp comment wireguard
systemctl enable --now wg-quick@wg0

Forwarding is what lets spokes talk to each other through the hub. Without a NAT rule, the hub does not route spokes to the internet, which is exactly the point of split tunnelling; add one later if you want a full-tunnel option for public Wi-Fi.

Adding a device

Each device gets a key pair generated on the device when possible, so the private key never travels. On a laptop with the WireGuard app, create an empty tunnel and copy its public key; on a phone, the app shows it under the tunnel's settings. Then register it on the hub:

wg set wg0 peer <DEVICE_PUBLIC_KEY> allowed-ips 10.66.0.10/32,fd66::10/128
wg-quick save wg0

And paste this into the device's tunnel config:

[Interface]
PrivateKey = <generated on the device>
Address = 10.66.0.10/32, fd66::10/128
DNS = 10.66.0.1

[Peer]
PublicKey = <contents of /etc/wireguard/hub.pub>
Endpoint = hub.example.com:51820
AllowedIPs = 10.66.0.0/24, fd66::/64
PersistentKeepalive = 25

AllowedIPs on the device is the split-tunnel switch: it lists only the tunnel network, so only that traffic is routed through WireGuard. PersistentKeepalive keeps the NAT mapping alive on phones and home routers. For a phone, generate a QR code from the config on the hub with qrencode -t ansiutf8 < device.conf and scan it; the WireGuard one-click app on our order form does exactly this for five devices at provisioning.

Private DNS, optionally

The DNS = 10.66.0.1 line only works if the hub resolves names. The easy path is unbound as a local recursive resolver with a few static records for your private hosts:

apt install -y unbound
cat > /etc/unbound/unbound.conf.d/hub.conf <<'EOF'
server:
  interface: 10.66.0.1
  interface: fd66::1
  access-control: 10.66.0.0/24 allow
  access-control: fd66::/64 allow
  local-zone: "team.internal." static
  local-data: "git.team.internal. A 10.66.0.20"
  local-data: "wiki.team.internal. A 10.66.0.21"
EOF
systemctl restart unbound

Now git.team.internal works from every spoke, and queries for public names are resolved recursively by the hub rather than by the coffee shop's DNS.

Reaching servers behind the hub

Any other CheapServ server can join as a spoke too, with its own key and address, and be reachable only through the tunnel. The usual pattern is to bind admin interfaces, databases and dashboards to the server's tunnel address and close those ports on the public firewall entirely. A database that listens on 10.66.0.20 only is not on the internet, whatever the scanner bots try.

What a VPS-1 handles

WireGuard's ChaCha20 encryption runs at several gigabits per second per core on a modern CPU, so a single vCPU is not the limit. The port is: VPS-1 has a 1 Gbps port and 4 TB of monthly traffic, which is more than a team of ten will push through a management VPN. In our own use, a hub with 30 peers idles at 1% CPU and 40 MB of RAM. If you later route everyone's internet traffic through it, bandwidth is the number to watch, not CPU.

Operations

  • wg show lists peers with their last handshake; a peer that has not handshaken in minutes is offline or misconfigured.
  • Removing a device is wg set wg0 peer <KEY> remove && wg-quick save wg0; no revocation lists, no expiry, the key simply stops working.
  • Back up /etc/wireguard/ with the rest of your config; the hub's private key is the only secret that matters.
  • Rotate the hub key once a year or when a laptop is lost; every device then needs the new public key, which is a good excuse to clean up the peer list.

If you would rather not maintain a hub at all, the Tailscale add-on gives you the same mesh with a hosted coordination server; the trade-off is that a third party knows who your peers are. For a team that wants the keys in its own hands, twenty lines of WireGuard on a $3 VPS is hard to beat.

CN
CheapServ Network

Runs the backbone, the transit mix and the DDoS mitigation in all six locations.

Deploy your first server in under a minute.

Top up from $25 in BTC, ETH, XMR or USDT. Your balance never expires and unused funds are refundable.

Sign up now