I Stopped Trusting VPN Ads and Built My Own (Then Automated It) :D
A build log: an SSH SOCKS5 tunnel, a self-hosted OpenVPN server on Oracle Cloud, and a Terraform + WireGuard version I can rebuild from scratch. My traffic left Texas and came out in France. Then it came out in Arizona
A build log: an SSH SOCKS5 tunnel, a self-hosted OpenVPN server on Oracle Cloud, and a Terraform + WireGuard version I can rebuild from scratch.
My traffic left Texas and came out in France.
Then it came out in Arizona.
I never left my desk. :D
I've always been the person who wants to know what actually happens after you hit Enter.
VPN ads are everywhere, and they all promise the same thing: privacy, security, "encryption". But when I asked myself what a VPN really does, packet by packet, I didn't have a satisfying answer. I knew the words. I'd never built one.
And there's a catch nobody puts in the ad. A commercial VPN doesn't remove the trust problem, it just moves it from your ISP to a company you can't audit.
So I decided to own the whole path. Here's what I built, what surprised me, and what I learned.
Note: This is based on my own setup and experience. If you know a better way to do any of this, tell me in the comments and I'll update the post.
The idea in one sentence
A VPN is like renting a PO box in another city: you put your mail in a locked bag, a courier carries it there, and the world only ever sees the PO box address, never your house.
Keep that picture in mind. It comes back a few times.
1. The quick version: an SSH tunnel
I started with the smallest thing that could possibly work: a Linux VPS at Contabo and SSH.
SSH already encrypts everything and can carry extra connections next to your shell. With port forwarding, a port on your laptop becomes the entrance to that encrypted pipe.
In Termius I created a local forwarding rule bound to 127.0.0.1:1080, sent through the VPS. Then I pointed Firefox at it:
Firefox > Settings > Network Settings > Manual proxy
SOCKS Host: 127.0.0.1 Port: 1080 SOCKS v5
Refreshed a "what is my IP" page and...
Charter Communications, Denton, Texas became Contabo GmbH, Lauterbourg, France. π«π·
In PO box terms: this courier only carries packages from one sender. Only Firefox went through the tunnel. Every other app on my laptop was still mailing from home.
Pro Tip: Bind to
127.0.0.1, never0.0.0.0. Otherwise anyone on the same coffee-shop Wi-Fi can use your tunnel. And turn onnetwork.proxy.socks_remote_dnsin Firefox, or your DNS lookups still leak to your ISP even though the traffic is tunneled.
If you'd rather skip the GUI, plain OpenSSH can be the SOCKS proxy itself:
ssh -N -D 127.0.0.1:1080 user@your-vps
2. The real version: OpenVPN on Oracle Cloud
A browser-only tunnel is cool. I wanted the whole device.
So I launched the OpenVPN Access Server BYOL image (version 2.14.3) from the Oracle Cloud marketplace in US West (Phoenix). The free license allows 2 concurrent VPN connections, which is plenty for one person.
The steps, roughly:
- Put the instance in a VCN with a public IP
- Generate an SSH key pair with PuTTYgen and attach the public half
- Open the firewall (security list ingress rule)
- SSH in and run the first-boot wizard
- In the admin UI, route all client internet traffic through the VPN and push DNS
1.1.1.1and1.0.0.1 - Create a separate VPN user (never connect as admin)
- Install OpenVPN Connect, accept the cert, connect
That "route client internet traffic" switch is the whole game. Without it, you've built remote access, not privacy.
[Screenshot: VPN routing and DNS settings in the Access Server admin UI]
Connected, refreshed the IP page:
Oracle Corporation, Phoenix, Arizona. Every app on the laptop, not just the browser.
3. The stuff I'm not proud of (but learned from)
Honest list of what my first build did "demo grade":
-
Self-signed certificate. The client threw
SELF_SIGNED_CERT_IN_CHAINand I clicked Accept. Training yourself to click through cert warnings is exactly how machine-in-the-middle attacks win. -
Admin port open to the world. My ingress rule was TCP
943,443from0.0.0.0/0. Port 943 includes the admin UI. Yikes. - Generated admin password. The wizard prints it in plain text in your terminal. Rotate it immediately.
- MFA off. It's disabled by default. Turn it on.
Fun detail: I never opened UDP 1194, so my client quietly connected over TCP 443 instead. It worked, but it's a reminder that "it connects" and "it's configured the way you think" are two different things.
4. The plot twist: rebuilding it as code
Clicking through a web console is great for learning. It's terrible for repeating.
If I tore the server down, I'd have to redo every screen from memory. So I rebuilt the idea with Terraform + cloud-init + WireGuard, and fixed the mistakes above on the way.
The Terraform creates the network, a security list, and one Ubuntu instance. The security list is intentionally tiny:
# Only two ways in: SSH from my IP, WireGuard from anywhere.
ingress_security_rules {
protocol = "6" # TCP
source = var.admin_cidr # e.g. my.ip/32
tcp_options { min = 22 max = 22 }
}
And the admin CIDR literally refuses to be the whole internet:
validation {
condition = can(cidrhost(var.admin_cidr, 0)) && var.admin_cidr != "0.0.0.0/0"
error_message = "admin_cidr must not be 0.0.0.0/0. Use your own address with /32."
}
Here's the twist I didn't expect: Oracle has two firewalls. The cloud security list is one. But Oracle's Ubuntu images also ship iptables rules that reject everything except SSH. Open a port in the console and traffic still dies at the server.
So cloud-init disables those rules and loads my own nftables ruleset on first boot. VPN clients can reach the internet, but not each other, and not the cloud metadata service or any private range behind the server.
In PO box terms: the courier delivers your mail, but can't wander into the post office's back room.
Pro Tip: On Oracle Cloud, if "the port is open but nothing connects", check the instance firewall, not just the security list.
5. Where it gets good
Adding a new device in the manual build meant logging into the admin UI, creating a user, and downloading a profile.
Now it's one line from my laptop:
./automation/scripts/new-client.sh -H <server-ip> phone -q
That SSHes in, runs a small wgctl add script, generates fresh keys plus a preshared key, saves the config locally with mode 600, and prints a QR code for my phone.
And instead of eyeballing an IP website, there's a checker:
./automation/scripts/verify-vpn.sh baseline # before connecting
./automation/scripts/verify-vpn.sh check --expect <server-ip>
It checks that my IPv4 changed, that my real IPv6 isn't leaking, and that DNS stopped going through my old resolver.
6. What's tested (and what isn't)
I care about being upfront about this one.
| Check | Result |
|---|---|
wgctl peer manager tests |
β 31 passing |
verify-vpn.sh tests |
β 12 passing |
| SSH tunnel tool tests (local throwaway sshd) | β 15 passing |
Terraform checks, incl. validate against the real OCI provider |
β 9 passing |
cloud-init output renders, firewall parses (nft -c) |
β passing |
Live terraform apply on Oracle Cloud |
β not run yet |
| Real WireGuard handshake through the automated server | β not run yet |
The manual SSH and OpenVPN builds were verified end to end with before and after IP checks. The automated WireGuard version is validated and tested locally, but the first real apply is still ahead of me. I'd rather say that than pretend.
Note: The repo also runs shellcheck, gitleaks, and an OCR scan over the screenshots in CI. My first redaction pass looked clean and wasn't: an IPv6 address and cloud resource IDs in the browser address bar slipped through. Now every image gets a machine scan AND a human look.
What I learned
- Privacy is a routing decision. The encryption was never the hard part. Deciding what goes through the tunnel (all traffic, DNS, IPv6) is where privacy is won or lost.
- Defaults are opinions. Self-signed certs, open admin ports, and disabled MFA were all defaults. Spotting them is the actual skill.
- Code makes you fix things. Writing the Terraform forced me to decide every firewall rule on purpose, instead of clicking "Add" until it worked.
What's next
Running the first live terraform apply, connecting a real phone over WireGuard, and writing a part 2 about what broke. There's a GOOD chance something will.
That's it. It's not perfect, and there's a good chance I'm missing something. If so, tell me!
Repo: https://github.com/VibhavChennamadhava/vpn-tunnel-privacy-automation
Have you ever built your own VPN or home lab tunnel? Did you go OpenVPN, WireGuard, or something else entirely? I'd love to hear your setup.
Questions or comments? Love to hear from you!
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.