Self-hosted WireGuard VPN tunnel demo
I was always fascinated by the fact that using a simple command like ssh host@ip one can get access into their host system. I gained more interest in this way of remotely accessing your devices and was also inspired to write this article after I read this medium article about a terminal based SSH portfolio by Kaustubh Patange (do give it a read).
So I went on and edited the ssh configs on my android using termux, with my hostname as my target device’s username and vice versa, and I kept accessing my devices for either executing some quick commands like merging a PR or running a workflow or transferring resources quickly over ssh with either scp (the old way) or rsync (the rsync way).
But here was the catch: With this approach, there were a couple of changes I frequently had to manually make and ensure certain conditions are met:
- Make sure that both of my devices are on the same network.
- Keep updating host IP from the network in the
.ssh/configon both of my devices (because I usually had to connect both ways).
The Problem: CG-NAT and ISP Restrictions
I started exploring ways in which I could access my device remotely without being in the same network. I knew that it was possible with port forwarding from the network router to my device’s port, but the networks I was mostly connected to were either my office wifi or my home wifi which I could not do anything about.
NAT table entry for port forwarding in my ISP’s portal
Unfortunately, my ISP is Airtel (the broadband I use is Airtel air fibre, meaning it’s just a sim card sitting in a booster circuit). No matter what I did, the forwarded traffic would never reach my router due to CG-NAT (Carrier Grade NAT). ISPs like Airtel and Jio usually block inbound traffic on port 22 for residential customers, so packets were dropped silently at the top layer.
The Solution: Self-Hosted WireGuard VPN on AWS
I needed a router available from anywhere with a static public IP. So I spun up an EC2 instance in my AWS free tier account and found WireGuard — a fast, lean, and easy to configure VPN.
The architecture is simple: The VPN server runs on the EC2 instance. When we SSH into the EC2 instance on a port, packets are looked up in the NAT table and forwarded through the WireGuard subnet to our target device via a secured & private tunnel.
ssh -p <port> -i <private_key_path> hostname@<static_public_ip_of_ec2_machine>
Setting Up the Remote EC2 Server
I’m using AWS’s EC2 t3.micro instance with 8GB SSD storage (available in the free tier). Once your instance is created and launched, configure the security groups:
Security Group inbound rules
The three rules:
- Allow WireGuard daemon to collect packets from the target device’s client
- Accept SSH traffic from your client device on port 2222
- Allow login to the EC2 instance from anywhere with your private key
Getting an Elastic IP
AWS provides Elastic IP which assigns your instance a public IP that doesn’t change when you restart the instance. Find it under Networking and Security > Elastic IPs and allocate one.
Installing WireGuard
$ sudo apt install wireguard -y
Generate the server-side key pair:
wg genkey | tee server_private.key | wg pubkey > server_public.key
Configure the WireGuard server:
# /etc/wireguard/wg0.conf
[Interface]
PrivateKey = <server_private.key>
Address = 10.0.0.1/24
MTU = 1420
[Peer]
PublicKey = <client_public.key>
AllowedIPs = 10.0.0.2/32
Enable IP forwarding:
sudo sysctl -w net.ipv4.ip_forward=1
Running the VPN Service
# Start the service
sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0
# Stop the service
sudo wg-quick down wg0
Target Device Configuration
Generate keys on the target device and configure WireGuard:
wg genkey | tee target_private.key | wg pubkey > target_public.key
# /etc/wireguard/wg0.conf on target device
[Interface]
PrivateKey = <target_private.key>
Address = 10.0.0.2/24
MTU = 1420
[Peer]
PublicKey = <server_public_key>
Endpoint = <EC2_instance_elastic_ip:52810>
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25
Setting Up NAT Rules
DNAT rule (PREROUTING) — forward incoming TCP traffic on port 2222 to the WireGuard subnet:
sudo iptables -t nat -A PREROUTING -d <EC2_elastic_public_ip> -p tcp --dport 2222 -j DNAT --to-destination 10.0.0.2:22
SNAT rule (POSTROUTING) — re-assign source IP for packets going through the tunnel:
sudo iptables -t nat -A POSTROUTING -d 10.0.0.2 -p tcp --dport 22 -j SNAT --to-source 10.0.0.1
Securing the Target Device
# in /etc/ssh/sshd_config of target device
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
PermitRootLogin no
AllowUsers <user_name_of_source_device>
sudo systemctl restart ssh
Generate an ED25519 key pair from your source device:
ssh-keygen -t ed25519 -f ~/target_device_access_key
Testing
Check the VPN tunnel status:
sudo wg show
Expected output:
interface: wg0
public key: <pub_key_of_target_device>
private key: (hidden)
listening port: 45859
peer: <pub_key_of_ec2_instance>
endpoint: <ec2_elastic_pub_ip>:52180
allowed ips: 10.0.0.0/24
latest handshake: 11 seconds ago
transfer: 92 B received, 476 B sent
persistent keepalive: every 25 seconds
Verify connectivity:
# from EC2 server
ping 10.0.0.1
# from target device
ping 10.0.0.2
Troubleshooting
If ping from server to target device is timing out, use tcpdump to inspect traffic:
# on client
sudo tcpdump -i wg0 -n 'port 22'
# on server
sudo tcpdump -i ens5 port 2222 -n
Ensure IP forwarding is enabled:
sudo sysctl net.ipv4.ip_forward # output should be 1
Final Tips
- Open a tmux session on your server and run the WireGuard service
- Use aliases for WireGuard-related commands
- Save SSH connections in
~/.ssh/configfor quick access - Add a custom terminal prompt indicator for active VPN connections
Custom terminal prompt indicating the successful tunnel connection
All config samples, my oh-my-posh custom terminal prompt config and WireGuard aliases are available in my GitHub repo: wg-vpn-tunnel-resources.
Originally published on Medium.