A self-hosted, full-tunnel WireGuard VPN running on a free-tier AWS EC2 instance in us-east-1. Every device (phone, laptop, tablet) routes its traffic through one encrypted tunnel to a t3.micro Ubuntu 24.04 server, which forwards it to the internet on their behalf. Websites therefore see the server's US-East-1 Elastic IP instead of the device's real location.
| Layer | Choice | Why |
|---|---|---|
| VPN protocol | WireGuard | Modern, audited crypto, ~100 lines of config, minimal overhead on low-end instances |
| Compute | EC2 t3.micro (Ubuntu 24.04) |
Free tier eligible (750 hrs/month); 1 GiB RAM is plenty for personal use |
| Region | us-east-1 | Free tier availability, low latency to the US |
| Firewall | EC2 security group | SSH only from my IP; UDP 51820 open to the world |
| Public address | Elastic IP | Stable endpoint so device configs never break across restarts |
Result: a private, encrypted network (10.0.0.0/24) that any device can join in under a minute, at zero monthly cost.
flowchart TD
subgraph AWS["AWS — us-east-1"]
EIP["Elastic IP<br/>(stable public address)"]
subgraph EC2INSTANCE["EC2 t3.micro — Ubuntu 24.04"]
W["eth0: physical interface<br/>connects to the internet"]
WG["wg0: virtual interface<br/>10.0.0.1/24 — the VPN side"]
end
end
Phone["Phone — 10.0.0.2"]
Laptop["Laptop — 10.0.0.3"]
Internet["Public internet"]
Web["Websites see the server's<br/>Elastic IP (us-east-1)"]
Phone <-->|"WireGuard tunnel<br/>encrypted UDP :51820"| EIP
Laptop <-->|"WireGuard tunnel<br/>encrypted UDP :51820"| EIP
EIP <--> W
W <-->|"NAT masquerade<br/>(source address rewrite)"| Internet
Internet --> Web
Two completely separate networks meet inside the server:
- VPN subnet
10.0.0.0/24— private, only reachable through the tunnel. Server is10.0.0.1, each device gets its own address (10.0.0.2,10.0.0.3, ...). - Public internet — the server's physical
eth0interface, reached via the Elastic IP.
Traffic flows between the two through iptables NAT (see the packet flow below).
sequenceDiagram
participant P as "Phone (10.0.0.2)"
participant S as "EC2 wg0 (10.0.0.1)"
participant E as "EC2 eth0 (public)"
participant W as "Website"
P->>P: Browser sends packet (dest: website IP)
P->>P: WireGuard encrypts it and wraps it in a UDP packet to :51820
P->>S: Encrypted UDP travels through the tunnel
S->>S: Decrypts the packet back to its original form
S->>E: iptables MASQUERADE rewrites source to the Elastic IP
E->>W: Ordinary request goes out to the internet
W-->>E: Response comes back to the Elastic IP
E-->>S: NAT reverses the rewrite (back to 10.0.0.2)
S-->>P: Encrypted response returns through the tunnel
P->>P: Decrypts and the browser renders the page
| Type | Protocol | Port | Source | Purpose |
|---|---|---|---|---|
| SSH | TCP | 22 | My IP only | Admin access to the server |
| Custom UDP | UDP | 51820 | 0.0.0.0/0 |
The WireGuard door — open to everyone, but only devices with a valid key get in |
- Each device has a cryptographic keypair — a secret private key and a public key it shares with the server.
- The server keeps a member list (
/etc/wireguard/wg0.conf): one[Peer]block per device containing that device's public key and its VPN IP. - When a device connects, the phone and server perform an authenticated handshake — no one without the right private key can impersonate a member.
- Every packet the phone sends is encrypted and wrapped (encapsulated) inside a UDP packet addressed to
Elastic-IP:51820. To the outside world it just looks like encrypted noise. - The server decrypts, rewrites the source address (NAT), and forwards the packet to the internet. Return traffic is rewritten back and sent through the tunnel. This is why websites see the server's US location.
- The phone's config says
AllowedIPs = 0.0.0.0/0— all traffic goes through the tunnel (full-tunnel mode).
| Concept | Plain meaning | Deep dive |
|---|---|---|
| Interface | A network connection point on a machine — physical (eth0) or virtual (wg0) |
docs/networking-guide.md |
| Subnet | A range of IP addresses, e.g. 10.0.0.0/24 = 256 addresses for the VPN |
docs/networking-guide.md |
| Tunnel / encapsulation | Wrapping an encrypted packet inside another packet | docs/networking-guide.md |
| NAT / MASQUERADE | Rewriting the source address so return traffic is routable | docs/networking-guide.md |
| AllowedIPs | Both a routing rule and a firewall filter in WireGuard | docs/networking-guide.md |
| Keypair | Private key (secret) + public key (shared) that prove a device's identity | docs/networking-guide.md |
| Elastic IP | A permanent public IP attached to the instance | docs/networking-guide.md |
Exact steps used to build this project (details in the docs):
- Launched Ubuntu 24.04,
t3.micro, in us-east-1, with a new key pair (vpn-key.pem). - Security group: SSH (22) from my IP, UDP 51820 from
0.0.0.0/0. - Allocated an Elastic IP and associated it with the instance.
ssh -i vpn-key.pem ubuntu@<ELASTIC-IP>
# Install WireGuard
sudo apt update && sudo apt install -y wireguard
# Enable IP forwarding (allow the server to route client traffic)
sudo sh -c 'echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf'
sudo sysctl -p
# Generate the server keypair
wg genkey | sudo tee /etc/wireguard/privatekey | wg pubkey | sudo tee /etc/wireguard/publickey[Interface]
PrivateKey = <server-private-key>
Address = 10.0.0.1/24
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o ens5 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o ens5 -j MASQUERADENote:
ens5is the physical interface name on t3 instances. Verify withip route | grep default— if yours sayseth0, useeth0.
# Start and enable at boot
sudo systemctl enable --now wg-quick@wg0# On your computer: generate the phone's keypair
wg genkey > phone_private.key
wg pubkey < phone_private.key > phone_public.keyphone.conf (scanned as a QR code by the official WireGuard app):
[Interface]
PrivateKey = <phone-private-key>
Address = 10.0.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = <server-public-key>
Endpoint = <ELASTIC-IP>:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25Add the phone to the server's member list, then hot-reload:
# Append to /etc/wireguard/wg0.conf:
[Peer]
PublicKey = <phone-public-key>
AllowedIPs = 10.0.0.2/32
# Apply without disconnecting anyone
sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'Display the QR and scan it from the app:
sudo apt install -y qrencode
qrencode -t ansiutf8 < phone.conf- docs/networking-guide.md — deep dive into interfaces, subnets, routing, NAT, encryption, and every concept behind this setup.
- docs/adding-devices.md — the 4-step recipe for adding (and removing) devices.
| Symptom | Likely cause | Fix |
|---|---|---|
| Tunnel connects but no sites load | Wrong interface in the NAT rule, or IP forwarding off | Check ip route | grep default; use the correct name (ens5/eth0) in PostUp; confirm sysctl net.ipv4.ip_forward = 1 |
No handshake at all (wg show shows stale peer) |
Security group missing/incorrect rule | Inbound must be UDP (not TCP) 51820 from 0.0.0.0/0 |
| Dies after a few minutes on cellular | Missing keepalive | PersistentKeepalive = 25 in the device config |
wg syncconf fails: "Permission denied" |
sudo didn't cover the process substitution |
Use sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)' |
| QR code won't scan | Terminal rendering | Enlarge font / fullscreen; or try qrencode -t UTF8 < phone.conf |
| VPN was working, stopped after a server reboot | Elastic IP not attached | Associate the Elastic IP (addresses change across stop/start otherwise) |