TTR Networks

Intermediate · WireGuard on Debian: users, routing and keys · section 2 of 10

Keys and the server

✓ Lab-tested 21 October 2026 on Debian 13.7, wireguard-tools 1.0.20210914, nftables 1.1.3

A WireGuard server is a key pair, a short configuration file and a systemd service. On Debian, the tools come from one package, and the WireGuard code itself is already in the kernel.

apt install wireguard-tools

Keys

Every device makes its own key pair with wg genkey (private) and wg pubkey (public). The private key never leaves the device that made it: users send you their public key, and you give them the server's. That's the whole of WireGuard's identity system. umask 077 makes sure the key files are readable by root only.

The configuration

/etc/wireguard/wg0.conf has an [Interface] section for the server itself, and one [Peer] section per user:

  • Address and ListenPort: the server's VPN address (10.9.0.1/24), and the UDP port users connect to.
  • The private key is loaded from its own file by PostUp, instead of being pasted into the config. It's a neat way to keep the secret out of a file you'll edit and copy around, but it has a catch, covered in Managing users.
  • Each peer's AllowedIPs lists the addresses that belong to that user: one /32 each, plus the whole branch LAN for the branch router. This line does more than it looks; the next sections show exactly what.

As with any VPN server, it needs IP forwarding (/etc/sysctl.d/99-wireguard.conf) and a firewall. This one is the OpenVPN lesson's ruleset, with wg0 and port 51820.

Starting it

systemctl enable --now wg-quick@wg0 brings the tunnel up now and at every boot. The service shows active (exited): wg-quick configures the kernel and exits, because there's no daemon to keep running. wg show is your view into it: the server's public key, its port, and each peer with its allowed IPs. Note that wg-quick added a route for the branch LAN by itself, from that peer's AllowedIPs.

The versions in this lab (real output)
root@vpn:~# cat /etc/debian_version; dpkg-query -W wireguard-tools nftables; uname -r; ip link add wgtest type wireguard && ip -d link show wgtest | head -3 && ip link del wgtest
13.7
nftables	1.1.3-1
wireguard-tools	1.0.20210914-3
7.0.14-linuxkit
13: wgtest: <POINTOPOINT,NOARP> mtu 1420 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/none  promiscuity 0 allmulti 0 minmtu 0 maxmtu 2147483552 
    wireguard addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535 tso_max_size 524280 tso_max_segs 65535 gro_max_size 65536 gso_ipv4_max_size 65536 gro_ipv4_max_size 65536 
  • Line 2: Debian 13 (trixie).
  • Lines 3–4: nftables and wireguard-tools from Debian's own repository. The tools are all you install; the tunnel itself is kernel code.
  • Lines 6–8: A throwaway wireguard interface proves the kernel supports it. Note the default MTU: 1420.
Generating key pairs (real output)
root@vpn:~# cd /etc/wireguard && umask 077 && wg genkey | tee server.key | wg pubkey > server.pub && ls -l && cat server.pub
total 8
-rw------- 1 root root 45 Oct 21 06:46 server.key
-rw------- 1 root root 45 Oct 21 06:46 server.pub
/S03YCIRbPGjlhE53GYhxFEh8LVo/Joel3BkEgWeeXk=

root@alice:~# mkdir -p /etc/wireguard && cd /etc/wireguard && umask 077 && wg genkey | tee alice.key | wg pubkey > alice.pub && cat alice.pub
hwCdM7ImFlIKfF0sQHngu9THMAenVRLSD1JYIrqR2Qw=

root@bob:~# mkdir -p /etc/wireguard && cd /etc/wireguard && umask 077 && wg genkey | tee bob.key | wg pubkey > bob.pub && cat bob.pub
8BpkL72eT8cHe8HTgKE8yWD464E0FVbQGlQ1FPi0tnc=

root@branch:~# mkdir -p /etc/wireguard && cd /etc/wireguard && umask 077 && wg genkey | tee branch.key | wg pubkey > branch.pub && cat branch.pub
sh5Ul4rjT56yt7ZYYVSrpJvxEIqKFx/HxkeICJIgGws=

root@carol:~# mkdir -p /etc/wireguard && cd /etc/wireguard && umask 077 && wg genkey | tee carol.key | wg pubkey > carol.pub && cat carol.pub
P+TMulXP07Ldwcf65Rx1CvXDOxK57C3HWV5aY5oDHEo=
  • Lines 1–4: wg genkey makes the private key, wg pubkey derives the public one. With umask 077, only root can read the files.
  • Line 5: The server's public key: this goes into every user's config.
  • Lines 7–17: Each user makes their own pair on their own machine, and sends you only the public key.
/etc/wireguard/wg0.conf on the server
# /etc/wireguard/wg0.conf on the VPN server  (systemctl enable --now wg-quick@wg0)
[Interface]
Address = 10.9.0.1/24
ListenPort = 51820
# The private key stays in its own root-only file, not in this config.
PostUp = wg set %i private-key /etc/wireguard/server.key

# One [Peer] per user. AllowedIPs is both the route to them and the only
# source addresses accepted from them.
[Peer]
# alice
PublicKey = hwCdM7ImFlIKfF0sQHngu9THMAenVRLSD1JYIrqR2Qw=
AllowedIPs = 10.9.0.10/32

[Peer]
# bob
PublicKey = 8BpkL72eT8cHe8HTgKE8yWD464E0FVbQGlQ1FPi0tnc=
AllowedIPs = 10.9.0.11/32

[Peer]
# branch: a router, with the branch LAN behind it
PublicKey = sh5Ul4rjT56yt7ZYYVSrpJvxEIqKFx/HxkeICJIgGws=
AllowedIPs = 10.9.0.20/32, 10.20.0.0/24
  • Lines 3–4: The server's VPN address and the UDP port users connect to.
  • Line 6: %i is the interface name. The private key is loaded from its own file after the interface comes up. Remember this line in Managing users.
  • Lines 10–13: A peer is a public key plus the addresses that belong to it. alice is 10.9.0.10, and only that.
  • Lines 20–23: The branch router owns its VPN address and the whole branch LAN behind it.
/etc/sysctl.d/99-wireguard.conf
# /etc/sysctl.d/99-wireguard.conf: route between the VPN and the office LAN.
net.ipv4.ip_forward = 1
/etc/nftables.conf on the server
#!/usr/sbin/nft -f
# /etc/nftables.conf on the VPN server (enabled with: systemctl enable --now nftables)
flush ruleset

table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif lo accept
    icmp type echo-request accept
    udp dport 51820 accept comment "WireGuard"
    tcp dport 22 accept comment "SSH"
  }

  chain forward {
    type filter hook forward priority 0; policy drop;
    ct state established,related accept
    iifname "wg0" oifname "eth1" accept comment "VPN users to the office LAN"
    iifname "eth1" oifname "wg0" ip daddr 10.20.0.0/24 accept comment "office to the branch LAN"
    iifname "wg0" oifname "eth0" ip saddr 10.9.0.10 accept comment "alice's full tunnel to the internet"
    counter comment "everything else, including user to user, is dropped"
  }
}

table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat;
    oifname "eth0" ip saddr 10.9.0.0/24 masquerade comment "full-tunnel users appear as the server"
  }
}
  • Line 11: WireGuard's port, open to the internet. Packets without a valid key get no reply at all.
  • Lines 18–19: Users to the office LAN, and the office to the branch LAN.
  • Line 20: Only alice may use the server to reach the internet (her full tunnel).
  • Line 21: Everything else is counted and dropped, including traffic from one user to another.
  • Line 28: Full-tunnel traffic leaves with the server's public address.
Starting the server (real output)
root@vpn:~# sysctl --system | grep ip_forward; systemctl enable --now nftables
net.ipv4.ip_forward = 1
Created symlink '/etc/systemd/system/sysinit.target.wants/nftables.service' → '/usr/lib/systemd/system/nftables.service'.

root@vpn:~# systemctl enable --now wg-quick@wg0
Created symlink '/etc/systemd/system/multi-user.target.wants/wg-quick@wg0.service' → '/usr/lib/systemd/system/wg-quick@.service'.

root@vpn:~# systemctl status wg-quick@wg0 --no-pager | head -16
● wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0
     Loaded: loaded (/usr/lib/systemd/system/wg-quick@.service; enabled; preset: enabled)
     Active: active (exited) since Wed 2026-10-21 06:46:47 UTC; 77ms ago
 Invocation: 9d7b0c34c12240ada910646d282fa5cc
       Docs: man:wg-quick(8)
             man:wg(8)
             https://www.wireguard.com/
             https://www.wireguard.com/quickstart/
             https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8
             https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8
    Process: 190 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)
   Main PID: 190 (code=exited, status=0/SUCCESS)
   Mem peak: 7.8M
        CPU: 21ms

Oct 21 06:46:47 vpn systemd[1]: Starting wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0...

root@vpn:~# wg show
interface: wg0
  public key: /S03YCIRbPGjlhE53GYhxFEh8LVo/Joel3BkEgWeeXk=
  private key: (hidden)
  listening port: 51820

peer: hwCdM7ImFlIKfF0sQHngu9THMAenVRLSD1JYIrqR2Qw=
  allowed ips: 10.9.0.10/32

peer: 8BpkL72eT8cHe8HTgKE8yWD464E0FVbQGlQ1FPi0tnc=
  allowed ips: 10.9.0.11/32

peer: sh5Ul4rjT56yt7ZYYVSrpJvxEIqKFx/HxkeICJIgGws=
  allowed ips: 10.9.0.20/32, 10.20.0.0/24

root@vpn:~# ip -br addr show wg0; ip route | grep wg0
wg0              UNKNOWN        10.9.0.1/24 
10.9.0.0/24 dev wg0 proto kernel scope link src 10.9.0.1 
10.20.0.0/24 dev wg0 scope link 
  • Lines 1–3: Forwarding on, and the firewall loaded at every boot.
  • Lines 5–6: The wg-quick@ template service: @wg0 means /etc/wireguard/wg0.conf.
  • Line 11: active (exited): wg-quick set up the kernel and finished. There's no daemon.
  • Lines 26–39: The interface and its three peers. No endpoints or handshakes yet: nobody has connected.
  • Lines 43–44: The VPN subnet comes from Address; the branch LAN route was added by wg-quick from the branch peer's AllowedIPs.

Check your understanding

A new user, dave, sends you his public key. What do you send him back?

  • The server's public key, its address and port, and the VPN address you've given him.
  • The server's private key, so he can authenticate it.
  • A certificate signed by your CA.
  • A private key you generated for him.