Tuesday, 27 October 2026

Running static linked wireguard-{go,rs} on an old server

Why do I need Wireguard on an antiquated server?

Some time ago I took on admin of a club server running Debian 8 Jessie (EOL 17 Jun 2018) hosting a small PHP web app that's quite important to the club. With recent PHP2Shell vulnerabilities (CVE-2026-109618 and CVE-2026-109619), hosting the app on open internet is suicidal. Yes, there was CloudFlare proxy up front with WAF rules, but hey - WAF is temp fix and can by bypassed as new attack paths emerge, and I didn't want to take risk.

Best way to protect it, except wholesale system update to get to lated Debian 13, battling broken updates, breaking changes, etc. is to take the app off public network and expose on VPN. Fallback plan would be  mTLS for legitimate users but that would require installing software from long archived repos (good luck!)

Wireguard-go or Wireguard-rs?

No difference in real terms, but pick whichever one floats your boat - I know you have a favourite.

I got both, cross-compiled for cpu architecture I needed and linked statically so I didn't need anything more than binaries. This allowed me to work around installing DKMS kernel modules, wg-tools or other packages - basically a self-contained solution, as much as can be...

CAVEAT - neither of those two uses standard wireguard config files, instead you configure server by writing to a unix socket file, and format of those commands is different than normal config file, and rather poorly documented (hence this writeup).

Reality check, gotchas and getting it to work

To start with, Debian 8 has tun module, my server had netcat version that doesn't support -U flag needed to work with unix socket, so had to write python scripts to handle writes/reads, and Python 3 is old 3.4.x and doesn't support f-strings (introduced in 3.6) so had to use older format() approach.

The big PITA was that wireguard-go which I somehow left in place doesn't take keys in normal format, instead it's in hex - more fun, and don't forget the keys are Curve25519 based!

Key generaion:

Option 1 - install wg-tools on another host

That's what I've done being lazy

Option 2 - use openssl to generate key material (didn't work on old Debian though)

openssl genpkey -algorithm X25519 -outform DER | tail -c 32 | openssl base64 > privatekey

openssl pkey -inform DER -pubout -outform DER -in <(openssl pkey -inform DER -in <(openssl base64 -d -in privatekey | awk '{printf "\x30\x2e\x02\x01\x00\x30\x05\x06\x03\x2b\x65\x6e\x04\x22\x04\x20"}{print}') ) | tail -c 32 | openssl base64 > publickey

Convert keys from base64 format to hex acceptable in UAPI:

wg genkey > private.key && cat private.key 
wg pubkey < private.key > public.key && cat public.key
cat public.key | base64 -d | xxd -p -c 32

What you get from xxd is the hex string that needs to go into UAPI statement. 

Wireguard-go setup (brain dump)

Binary at /etc/wireguard/bin/wireguard-go, config files in /etc/wireguard folder.

UAPI interface is simplistic, but operational. First define private key of server, listening port and then each public_key is followed by settings for particular client, until next public_key is defined and so on. 

The /etc/wireguard/uapi.conf contains config commands that will be injected into socket:

set=1
private_key=88...1471
listen_port=51820
public_key=4b...673a
allowed_ip=10.1.1.2/32
public_key=2c...2122
allowed_ip=10.1.1.3/32
public_key=3e...0949
allowed_ip=10.1.1.4/32
public_key=80...2b17
allowed_ip=10.1.1.5/32

To start/reload wireguard I wrote a small script that first stops any running instance, starts clean one up, sets up IP networking on wg0 and then injects UAPI into socket using Python.

/etc/wireguard/run.sh:

#!/bin/bash
rm /var/run/wireguard/wg0.sock /var/run/wireguard/wireguard.pid 2>/dev/null
sleep 1
/etc/wireguard/bin/wireguard-go wg0
WG_PID=`ps ax | grep wireguard-go|grep -v grep | awk '{print $1}'`
echo $WG_PID > /var/run/wireguard/wireguard.pid
sleep 1
ip address add 10.1.1.1/24 dev wg0
ip link set up dev wg0
sleep 1
/usr/bin/python3 /etc/wireguard/wg-configure.py

/etc/wireguard/wg-configure.py:

import socket, sys
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.connect("/var/run/wireguard/wg0.sock")
s.sendall(open("/etc/wireguard/uapi.conf", "rb").read())
s.close()

/etc/systemd/system/wireguard.service:

[Unit]
Description=WireGuard User-Space VPN (wg0)
After=network.target network-online.target ufw.service
Wants=network-online.target 
Before=ssh.service apache2.service postgresql.service  
 
[Service]
Type=forking
ExecStart=/etc/wireguard/run.sh 
 
# Tell systemd how to find and kill the background process when stopping the service
PIDFile=/var/run/wireguard/wireguard.pid
TimeoutSec=30
Restart=on-failure 
 
ExecStop=/sbin/ip link set down dev wg0
ExecStop=/bin/rm -f /var/run/wireguard/wg0.sock  /var/run/wireguard/wireguard.pid 
 
[Install]
WantedBy=multi-user.target

Sample client config:

[Interface]
PrivateKey = 8...w=
Address = 10.19.1.3/24

[Peer]
PublicKey = F...M=
AllowedIPs = 10.1.1.1/24
Endpoint = x.x.x.x:51820
PersistentKeepalive = 25

Finally set firewall rules to whatever is needed in given scenario. I wanted apache to listen on wg0 only, hence wireguard had to start before web and database servers.

Fun fact - deleting wg0.sock file stops the wireguard server - love the clean unix way of doing so :-)