|

How-to: install and configure WireGuard between a server and home

This tutorial links a hosted server (a Rocky Linux VPS) to the home network through a WireGuard tunnel. At the end, the server reaches the home machines, the house reaches the server, and only one UDP port is exposed to the Internet. It’s the basis for many uses: backups sent to a NAS, monitoring a…

This tutorial links a hosted server (a Rocky Linux VPS) to the home network through a WireGuard tunnel. At the end, the server reaches the home machines, the house reaches the server, and only one UDP port is exposed to the Internet. It’s the basis for many uses: backups sent to a NAS, monitoring a public server from home, access to services you really don’t want to publish.

Every configuration line is explained, along with the checks to run and the most common failures. The addresses are examples; adapt them to your own addressing plan.

Why WireGuard

WireGuard fits in a few thousand lines of code in the Linux kernel, against hundreds of thousands for OpenVPN or IPsec. It offers no choice of algorithm: the cryptography is fixed (Curve25519, ChaCha20-Poly1305, BLAKE2s), which removes a whole class of configuration mistakes. It looks stateless: no session to negotiate by hand, a handshake renewed automatically about every two minutes, and a tunnel that survives a peer’s address changing. And it’s silent: a packet not authenticated by a known key gets no answer, so the port looks closed to a scan.

Concepts to keep in mind

  • One key pair per end. The private key stays on the machine; the public key is its identity for the other side. There’s no user and no password;
  • Peers, not a client and a server. Each side declares the other as a peer. What looks like a “server” is simply the peer with a fixed address that waits to be called;
  • Cryptokey routing. The AllowedIPs directive ties networks to a peer. A packet for those networks leaves encrypted for that peer; a packet received from that peer is accepted only if its source address belongs to those networks;
  • UDP only. Everything goes through a single UDP port, which crosses ISP boxes’ address translation well.

The addressing plan

ItemValueRole
Tunnel network10.99.0.0/24addresses inside the tunnel, one per end
Router (tunnel)10.99.0.2home side, behind the ISP box
Server (tunnel)10.99.0.3the VPS
Home network192.168.10.0/24what the server must reach
Server (public)203.0.113.25the VPS’s fixed public address, UDP port 51820

Pick a tunnel network that exists nowhere else at home or at the provider: overlapping addresses produce failures that are very hard to understand.

Prerequisites

  • a Rocky Linux 9 or 10 server with a fixed public IPv4 address and root access;
  • a home router that does WireGuard (MikroTik RouterOS 7, OpenWrt, pfSense, OPNsense…), or failing that an always-on Linux machine;
  • console access to the server at the provider, in case a firewall mistake cuts SSH.

Step 1: install the tools

The kernel module ships with Rocky Linux since version 9. Only the userspace tools are missing, wg and wg-quick:

dnf install wireguard-tools
modprobe wireguard
lsmod | grep wireguard        # the module is loaded

Step 2: generate the keys

umask 077
cd /etc/wireguard
wg genkey | tee server.key | wg pubkey > server.pub
wg genpsk > home.psk          # preshared key, optional but recommended
cat server.pub

The umask 077 ensures the files are created readable by root only, with no window during which another user could read them. The preshared key adds a layer of symmetric encryption on top of the key exchange: it replaces nothing, but protects traffic recorded today against a future quantum computer able to break Curve25519. It must be identical on both sides and travel only over a safe channel.

Do the same on the router and note its public key. Private keys never appear in a version-controlled file in clear text: put them in a secrets store (Vault, OpenBao, Ansible Vault) if you automate the deployment.

Step 3: write /etc/wireguard/wg0.conf

[Interface]
Address    = 10.99.0.3/24
ListenPort = 51820
PrivateKey = <server private key>
MTU        = 1420
SaveConfig = false

[Peer]
# home router
PublicKey           = <router public key>
PresharedKey        = <preshared key>
AllowedIPs          = 192.168.10.0/24, 10.99.0.0/24
PersistentKeepalive = 25

The [Interface] section

  • Address: the server’s address in the tunnel. The /24 mask makes wg-quick create a route to the whole tunnel network, useful if other servers join the same tunnel later;
  • ListenPort: the UDP listening port. 51820 is the convention; nothing forces you to use it;
  • PrivateKey: the content of server.key. A variant is to leave it out of the file and load it with PostUp = wg set %i private-key /etc/wireguard/server.key;
  • MTU = 1420: the maximum packet size in the tunnel. WireGuard adds 60 bytes of header over IPv4 (80 over IPv6); 1420 leaves room on a standard 1500 Ethernet link. If the home box uses PPPoE (MTU 1492), go down to 1412, or 1380 when in doubt;
  • SaveConfig = false: keeps wg-quick from rewriting the file with the current state on shutdown, which would erase your comments and could freeze in a manual change made for a test.

The [Peer] section

  • PublicKey: the router’s identity;
  • PresharedKey: the shared key generated above;
  • AllowedIPs: the most important directive, and the most misunderstood. It’s both a routing table (packets to 192.168.10.0/24 and 10.99.0.0/24 go to this peer) and a filter (a packet from this peer whose source isn’t in these networks is dropped). Don’t put 0.0.0.0/0 here: only traffic for the house goes through the tunnel, the rest of the server’s traffic leaves normally. That’s a split tunnel;
  • no Endpoint: the server doesn’t know where to reach the house, whose public address may change. The router calls it; the server learns its address at the first handshake and updates it if it changes;
  • PersistentKeepalive = 25: a small packet every 25 seconds. Essential on the router side, because the box forgets an idle UDP translation after 30 to 120 seconds and the server could no longer reach the house. On the server side it’s harmless and keeps the tunnel active both ways.

Step 4: the home side

The router declares the server as a peer, this time with an Endpoint: the VPS’s public address. Its AllowedIPs for that peer is just the server’s tunnel address. Example on a MikroTik running RouterOS 7:

/interface wireguard add name=wg-servers listen-port=13231 private-key="<router private key>"
/ip address add address=10.99.0.2/24 interface=wg-servers
/interface wireguard peers add interface=wg-servers \
    public-key="<server public key>" preshared-key="<preshared key>" \
    endpoint-address=203.0.113.25 endpoint-port=51820 \
    allowed-address=10.99.0.3/32 persistent-keepalive=25s

The router’s firewall must also allow traffic between the WireGuard interface and the LAN (forward chain), placed before the generic drop rules. A common rule that drops private addresses (“bogons”) must apply only to the Internet-facing interface: otherwise it drops the tunnel network itself.

Step 5: the server’s firewall

firewall-cmd --permanent --add-port=51820/udp
# attach wg0 to a zone: 'trusted' to allow everything from home,
# or a dedicated zone that opens only what's needed (SSH, exporters…)
firewall-cmd --permanent --new-zone=wireguard
firewall-cmd --permanent --zone=wireguard --add-interface=wg0
firewall-cmd --permanent --zone=wireguard --add-service=ssh
firewall-cmd --reload

A dedicated zone is more careful than trusted: if a home machine were compromised, it would see only the services you chose on the server. The server doesn’t need to forward packets between networks (net.ipv4.ip_forward can stay at 0) since it’s only an endpoint.

Step 6: start

systemctl enable --now wg-quick@wg0

wg-quick creates the wg0 interface, gives it its address, loads the WireGuard configuration and adds the routes derived from AllowedIPs. The service comes back at boot. To apply a change to the file without dropping the tunnel: wg syncconf wg0 <(wg-quick strip wg0).

Step 7: check, in order

wg show wg0
#   peer: <router public key>
#     endpoint: 198.51.100.7:13231
#     allowed ips: 192.168.10.0/24, 10.99.0.0/24
#     latest handshake: 41 seconds ago
#     transfer: 1.20 MiB received, 3.45 MiB sent
  • latest handshake under two minutes: keys and UDP port are right;
  • endpoint filled in: the server knows the house’s current address;
  • transfer growing both ways: traffic flows.
ip route get 192.168.10.20        # must answer "dev wg0"
ping -c3 10.99.0.2                 # the router in the tunnel
ping -c3 192.168.10.20             # a home machine
ssh admin@192.168.10.20            # a real TCP connection
# from a home machine, the other way:
ssh root@10.99.0.3

Don’t stop at ping. A firewall can accept ICMP early in its chain and reject new TCP connections further down: ping works, nothing else. Test a real connection, in both directions.

The most common failures

No handshake

The UDP port is closed (at the provider or in firewalld), the public keys are swapped, or the router has the wrong Endpoint address. tcpdump -ni eth0 udp port 51820 on the server shows whether the router’s packets arrive: if they arrive with no answer, it’s a key; if they don’t arrive, it’s the network.

Handshake, but no traffic

The AllowedIPs don’t match on both sides: the router sends packets whose source isn’t in the server’s list, and the server silently drops them. Or the return route is missing at home.

Ping works, TCP doesn’t

A firewall rule treats ICMP separately. The typical case is a “bogons” address list containing 10.0.0.0/8 applied to every interface: it drops the tunnel network in the forward chain. Restrict it to the Internet interface.

Small exchanges work, large transfers freeze

An MTU problem. Full-size packets exceed the allowed size somewhere on the path and are lost. Test with ping -M do -s 1372 192.168.10.20, increasing the size until it fails, then lower the tunnel’s MTU accordingly on both sides.

Everything works, then stops after a while

PersistentKeepalive is missing on the router side, and the box forgot the address translation. Or the house’s public address changed and the server hasn’t learned it yet: it’s up to the router to restart the handshake, which the keepalive does on its own.

Home machine names don’t resolve

The tunnel carries addresses, not names. To resolve nas.home.lan from the server, send only that domain to the home DNS server, for example with NetworkManager’s dnsmasq plugin, rather than replacing the server’s whole DNS.

Maintaining the tunnel

  • Rotating keys without losing access: first add the server’s new public key as a second peer on the router, switch the server to its new key, check the handshake, then remove the old peer. Never change both sides blindly from outside;
  • Adding a second server: new tunnel address (10.99.0.4), new peer on the router with allowed-address=10.99.0.4/32. Each address must appear in a single peer only, or routing becomes unpredictable;
  • Monitoring: the age of the latest handshake is the best indicator. Beyond three minutes, the tunnel is down. A script that reads wg show wg0 latest-handshakes and publishes the value for Prometheus, or a simple probe from home, is enough;
  • Changing a router remotely: turn on its safe mode (Safe Mode on RouterOS) before any change. If the session is lost, the router undoes everything done since.

Going further

The same tunnel can carry monitoring (Prometheus querying the server’s exporters), backups (rsync to an account jailed in a NAS directory) and administration (SSH limited to the tunnel interface). Once the tunnel is in place, many ports you might have been tempted to open to the Internet no longer need to be.


This post is part of a series of sysadmin tutorials (tag “how-to”).
Series “How-to”, tutorial 5/57. · ← Previous tutorial · Next tutorial → (coming 1 Nov) · Version française

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *