WIUNIX ACADEMY
Set up a routed WireGuard site-to-site VPN on Ubuntu and Debian
Page 2 of 8. Review the earlier steps and prerequisites before continuing.
YOUR SYSTEM. YOUR STEPS.
Where are you working?
Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.
USE YOUR OWN VALUES
Configure this tutorial
Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.
Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.
Set values for my environment24 editable values
LET’S BUILD
Retain the forwarding baseline
On A/B after fresh-host checks. Existing files cause failure, not overwrite. Forwarding changes can reset IPv4 host/router defaults: compare sysctls before/after and account for tuning before production. This recipe assumes fresh stock gateways.
sudo bash -eu <<'BASELINE'
umask 077
install -d -m 0700 /etc/wireguard
for p in /etc/wireguard/private.key /etc/wireguard/wg0.conf /etc/wireguard/forwarding.before /etc/sysctl.d/70-academy-wireguard.conf /etc/systemd/system/academy-wg-filter.service; do
if test -e "$p"; then echo 'Existing tutorial state: STOP' >&2; exit 1; fi
done
rules=$(nft list ruleset)
if test -n "$rules"; then echo 'Existing ruleset: STOP' >&2; exit 1; fi
sysctl -n net.ipv4.ip_forward > /etc/wireguard/forwarding.before
sysctl -a 2>/dev/null > /etc/wireguard/sysctl.before
BASELINE
Original forwarding value and sysctl inventory are retained in a root-only directory.
LET’S BUILD
Generate keys locally; share only public keys
Run separately on A/B. Disable shell tracing and terminal recording. Private keys never enter command arguments, logs, Git or tickets. This design uses unique key pairs without an optional preshared key. If your threat model requires a PSK, manage it as a separate protected shared secret. Verify exchanged public keys through an authenticated channel.
sudo bash -eu <<'KEYS'
umask 077
test ! -e /etc/wireguard/private.key
test ! -e /etc/wireguard/public.key
wg genkey > /etc/wireguard/private.key
wg pubkey < /etc/wireguard/private.key > /etc/wireguard/public.key
chmod 0600 /etc/wireguard/private.key /etc/wireguard/public.key
KEYS
sudo cat /etc/wireguard/public.key
A/B have different keys; only the public key is printed.
LET’S BUILD
Create each site’s protected template
On each gateway put only its next FILE CONTENT in the editor. Replace peer PUBLIC keys and public endpoints; leave the local private-key marker until the protected insertion step. Never copy the other gateway’s private key.
sudo install -o root -g root -m 0600 /dev/null /etc/wireguard/wg0.conf
sudo nano /etc/wireguard/wg0.conf
The protected file contains this site’s template and opposite public identity.
A: wg0.conf contents
FILE CONTENT on A only: peer B’s identity and prefixes. No SaveConfig, DNS or default-route setting.
[Interface]
Address = 10.255.255.1/30
ListenPort = 51820
PrivateKey = REPLACE_LOCAL_PRIVATE_KEY
[Peer]
PublicKey = REPLACE_B_PUBLIC_KEY
Endpoint = 203.0.113.20:51820
AllowedIPs = 10.255.255.2/32, 10.20.20.0/24
B: wg0.conf contents
FILE CONTENT on B only: peer A’s identity and prefixes. The baseline assumes stable reachable public endpoints on both sides.
[Interface]
Address = 10.255.255.2/30
ListenPort = 51820
PrivateKey = REPLACE_LOCAL_PRIVATE_KEY
[Peer]
PublicKey = REPLACE_A_PUBLIC_KEY
Endpoint = 198.51.100.10:51820
AllowedIPs = 10.255.255.1/32, 10.10.10.0/24
LET’S BUILD
Insert the local private key without displaying it
On A/B after editing public placeholders. The script refuses missing/duplicate markers and remaining peer placeholders. Do not share showconf, dump, completed config output or editor screenshots: they can contain secrets.
sudo python3 - <<'CONFIG'
import os
from pathlib import Path
p = Path('/etc/wireguard/wg0.conf')
s = p.read_text()
marker = 'REPLACE_LOCAL_PRIVATE_KEY'
if s.count(marker) != 1 or 'REPLACE_' in s.replace(marker, ''):
raise SystemExit('Unedited or duplicate placeholders: STOP')
k = Path('/etc/wireguard/private.key').read_text().strip()
if len(k) != 44:
raise SystemExit('Invalid local key length: STOP')
p.write_text(s.replace(marker, k))
os.chmod(p, 0o600)
print('Local key inserted; configuration not displayed')
CONFIG
Only a completion message appears; the private key stays in protected files.