Wednesday, December 10, 2025

How to Build a Simple, Secure VPN (WireGuard) — Explanation + Code

 

How to Build a Simple, Secure VPN (WireGuard) — Explanation + Code

Overview — what you’ll learn

This article explains how a Virtual Private Network (VPN) works at a high level and walks through building a lightweight, modern VPN using WireGuard. You’ll get the commands and configuration files to:

  • Generate keys for server and client
  • Configure a WireGuard server on a Linux VPS (Ubuntu/Debian)
  • Create a client configuration you can import to a phone or laptop
  • Configure IP forwarding and basic firewall rules
  • Test connectivity

Important legal note: A VPN can be used for legitimate privacy and networking purposes. Do not use it to break laws, violate terms of service, or commit wrongdoing. Always operate within local laws and your provider’s terms.

Quick primer: how VPNs work (short)

A VPN creates an encrypted tunnel between two endpoints: the client and the VPN server. When the tunnel is active, the client’s traffic is encapsulated and routed through the server, which can provide privacy, remote access to a private network, or allow your device to appear from the server’s network.

WireGuard is a modern, high-performance VPN protocol that’s simple to configure, uses modern cryptography, and is available in the Linux kernel (and on other platforms). That’s why we’ll use it here.

Requirements

  • A VPS with public IPv4 (Ubuntu 22.04 or Debian 12 recommended)
  • root or a user with sudo
  • Basic Linux command-line familiarity
  • WireGuard client app (available for Windows/macOS/Linux/Android/iOS)

Step 1 — Install WireGuard on the server

On Ubuntu/Debian:

# update and install
sudo apt update
sudo apt install -y wireguard iptables-persistent

iptables-persistent helps save firewall rules across reboots. On newer Ubuntu, you might prefer ufw — commands below will show both approaches.

Step 2 — Generate keys

WireGuard uses public/private key pairs. Create a directory for keys and generate them.

sudo mkdir -p /etc/wireguard/keys
sudo chmod 700 /etc/wireguard/keys
cd /etc/wireguard/keys

# server keys
wg genkey | tee server_private.key
 | wg pubkey > server_public.key

# client keys (example: client1)
wg genkey | tee client1_private.key
 | wg pubkey > client1_public.key

# show files (for copying to configs)
sudo cat server_private.key
sudo cat server_public.key
sudo cat client1_private.key
sudo cat client1_public.key

Store the private keys securely — only the owning machine needs them.

Step 3 — Server configuration

Create /etc/wireguard/wg0.conf and fill it like this. Replace placeholders (e.g., SERVER_PRIVATE_KEY, SERVER_PUBLIC_IP) with the actual values and IPs.

[Interface]
# Private key for server (from server_private.key)
PrivateKey = SERVER_PRIVATE_KEY
# VPN network address for the server
Address = 10.10.0.1/24
# Listen port for WireGuard
ListenPort = 51820
# Persist tun device across reboots (optional)
SaveConfig = true

# Example peer (client1)
[Peer]
# client's public key (client1_public.key)
PublicKey = CLIENT1_PUBLIC_KEY
# only allow client's VPN IP
 inside 10.10.0.0/24
AllowedIPs = 10.10.0.2/32

Save and set secure permissions:

sudo chmod 600 /etc/wireguard/wg0.conf

Step 4 — Enable IP forwarding & firewall (NAT)

Enable IP forwarding (temporary and persistent):

# temporary (until reboot)
sudo sysctl -w net.ipv4.ip_forward=1

# persistent
echo "net.ipv4.ip_forward=1" |
 sudo tee -a /etc/sysctl.conf
sudo sysctl -p

Configure NAT so client traffic is masqueraded through the server’s public interface (assume eth0 is public interface — adjust if different):

Using iptables:

# enable NAT
sudo iptables -t nat -A 
POSTROUTING -o eth0 -j MASQUERADE

# accept forwarding from wg0
sudo iptables -A FORWARD -i wg0 -j ACCEPT
sudo iptables -A FORWARD -o wg0 -j ACCEPT

# save rules (Debian/Ubuntu)
sudo netfilter-persistent save

Using ufw (if you prefer):

# allow WireGuard UDP port
sudo ufw allow 51820/udp

# allow forwarding and NAT via 
/etc/ufw/before.rules
# (You must edit /etc/ufw/before.rules
 and add a NAT section as 
WireGuard docs suggest.)
# Then enable:
sudo ufw enable

Note: Confirm your VPS provider allows IP forwarding/NAT on their plan.

Step 5 — Start WireGuard

Bring up the interface:

sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0

# Check status
sudo wg show

wg show will display interface, 

listening port, and peers.

Step 6 — Create client configuration

Create a client config that your device will import (e.g., use client1_private.key and server public key).

client1.conf:

[Interface]
PrivateKey = CLIENT1_PRIVATE_KEY
Address = 10.10.0.2/24
DNS = 1.1.1.1

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = SERVER_PUBLIC_IP:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
  • Address is the VPN IP for the client.
  • AllowedIPs = 0.0.0.0/0 routes all traffic through the VPN (full-tunnel). If you only want to route specific subnets, set AllowedIPs accordingly (e.g., 10.10.0.0/24 for split-tunnel).

Transfer this .conf to your client device securely (e.g., SFTP or paste via secure channel). On mobile clients, WireGuard apps can scan a QR code to import the config.

You can generate a QR code on the server:

sudo apt install -y qrencode
qrencode -t ansiutf8 < client1.conf
# or produce PNG
qrencode -o client1.png < client1.conf

Step 7 — Add the client peer to the server (automate)

If you created the server config earlier without the client peer, you can add the peer dynamically:

# Example: add peer using wg (runtime) 
and persist using wg-quick save if 
SaveConfig=true
sudo wg set wg0 peer "$
(cat /etc/wireguard/keys/client1_public.key)" \
    allowed-ips 10.10.0.2/32

If SaveConfig=true in wg0.conf, you can then sudo wg-quick save wg0 to persist it.

Simple automation script (create client)

Here’s a small bash script to generate keys and produce a client config file:

#!/bin/bash
set -e
CLIENT_NAME=${1:-client1}
KEY_DIR=/etc/wireguard/keys
mkdir -p $KEY_DIR
cd $KEY_DIR

# generate keys
wg genkey | tee ${CLIENT_NAME}_private.key
 | wg pubkey > ${CLIENT_NAME}_public.key

SERVER_PUB=$(cat server_public.key)
CLIENT_PRIV=$(cat ${CLIENT_NAME}_private.key)
CLIENT_PUB=$(cat ${CLIENT_NAME}_public.key)

# variables - adapt to your network
SERVER_IP="YOUR_SERVER_PUBLIC_IP"
WG_NETWORK="10.10.0"
CLIENT_IP="${WG_NETWORK}.2/24"
WG_PORT=51820

cat > /etc/wireguard/${CLIENT_NAME}.conf <<EOF
[Interface]
PrivateKey = ${CLIENT_PRIV}
Address = ${CLIENT_IP}
DNS = 1.1.1.1

[Peer]
PublicKey = ${SERVER_PUB}
Endpoint = ${SERVER_IP}:${WG_PORT}
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
EOF

# add peer to server runtime
sudo wg set wg0 peer ${CLIENT_PUB}
 allowed-ips ${WG_NETWORK}.2/32

echo "Created /etc/wireguard/$
{CLIENT_NAME}.conf and added peer to wg0"

Run as root: sudo bash 

create-client.sh client2

Testing connectivity

  1. Import the client config into the WireGuard app.
  2. Activate the tunnel on the client.
  3. On the server: sudo wg show — you should see handshakes and data transfer bytes for the peer.
  4. On client, check public IP via https://ifconfig.co or curl ifconfig.co (should show server's public IP if full-tunnel).
  5. Test ping to server VPN IP: ping 10.10.0.1 from client.

Security best practices

  • Rotate keys if a private key is exposed.
  • Use a strong DNS provider or your own DNS resolver to avoid leaks.
  • Monitor logs for unfamiliar peers or traffic patterns.
  • Keep system packages and WireGuard up to date.
  • Limit which client IPs can access internal networks with AllowedIPs and firewall rules.

Troubleshooting tips

  • If wg show shows no handshake, ensure UDP port (51820) is allowed in VPS firewall and provider-level firewall.
  • Confirm sysctl net.ipv4.ip_forward is 1.
  • Verify NAT rules apply to the correct public interface (replace eth0 if your server uses a different name).
  • Use sudo tcpdump -n -i wg0 and -i eth0 to observe packets when debugging.

Alternatives & when to choose them

  • OpenVPN: older, feature-rich, more complex configuration; good for compatibility with legacy systems.
  • WireGuard: preferred for simplicity and performance for most modern use cases.

Final notes

This guide gives a compact, practical way to set up a minimal, secure WireGuard VPN server and client. Adapt IP ranges, firewall rules, and AllowedIPs to your needs. If you want, I can produce step-by-step instructions tailored to your VPS provider (e.g., DigitalOcean, AWS, Linode) or generate client configs for multiple devices — tell me the OS(es) or constraints and I’ll produce ready-to-import files.

How to Build a Simple, Secure VPN (WireGuard) — Explanation + Code

  How to Build a Simple, Secure VPN (WireGuard) — Explanation + Code Overview — what you’ll learn This article explains how a Virtual Priv...