Skip to content

Repository files navigation

exitramp

A static exit IP for your local dev traffic.

A self-hosted static egress gateway for local development against IP-allowlisted APIs.

Your vendor says "give us an IP to whitelist" — but your home IP is dynamic, and you want to test uncommitted local code without deploying to staging first. exitramp puts a tiny WireGuard gateway on a $3/month cloud VM with a static IP. Flip the VPN on, and your local backend's requests to the vendor leave from that IP. Flip it off when you're done.

iOS Simulator / local app
        ↓
localhost:3000  (your local backend — breakpoints, hot reload, uncommitted code)
        ↓
WireGuard tunnel
        ↓
Cloud VM + static IP   ←── the IP your vendor allowlists
        ↓
Vendor API

No code changes. No proxy agents. Works with Node, Python, Go, PHP, Postman, curl — anything, because it's the operating system routing the traffic, not an HTTP proxy.

Why not ngrok / Cloudflare Tunnel?

Those solve inbound traffic (exposing your local server to the internet). IP allowlisting is an outbound problem: your requests to the vendor must originate from a fixed IP. That needs an egress gateway, which is what this is.

Quick start

There are two ways to set up the gateway — pick one. Both end in the identical result (a locked-down WireGuard gateway with a static IP); everything after this section is the same regardless of which path you chose.

  • Option A — AWS via Terraform: you don't have a server yet and want AWS. Terraform provisions the VM + Elastic IP and runs the install automatically.
  • Option B — any Ubuntu/Debian VM: you already have a VM (any provider: Hetzner, DigitalOcean, Linode, a hand-made EC2 instance...) with a static public IP. You run the install script yourself.

Option A: AWS, via Terraform

Provisions a t4g.nano EC2 instance + Elastic IP (~$3–5/month total), installs the gateway automatically on first boot, and places the peer-management scripts on the server for you.

Prerequisites:

  • An AWS account with credentials configured locally — run aws configure once, or have a profile in ~/.aws/credentials. The credentials need permission to create EC2 instances, Elastic IPs, security groups, and key pairs.
  • Terraform 1.5 or newer.
  • An SSH keypair. If ls ~/.ssh/*.pub shows nothing, create one: ssh-keygen -t ed25519.

1. Clone the repo and deploy:

git clone https://github.com/onamfc/exitramp.git
cd exitramp/terraform/aws

terraform init
terraform apply \
  -var "region=us-east-1" \
  -var "ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var "admin_cidr=$(curl -4 -s https://checkip.amazonaws.com)/32"

What those variables mean:

  • region — any AWS region; pick one near you. If you use a non-default AWS profile, prefix the command: AWS_PROFILE=myprofile terraform apply ...
  • ssh_public_key — your public key, so you (and only you) can SSH into the gateway as the ubuntu user.
  • admin_cidr — the only IP allowed to SSH in. The command above uses your current public IP. When your home IP changes, re-run the same terraform apply with the new value to regain SSH access (the VPN itself is unaffected — only SSH).

2. Note the IP it prints. The exitramp_ip output (also shown in next_steps) is your permanent static egress IP — this is what you hand to your vendor for allowlisting.

3. Wait ~2 minutes for the first-boot install to finish, then create your personal config (replace yourname — letters, digits, -, _):

ssh ubuntu@<exitramp_ip> 'sudo ./add-peer.sh yourname'
scp ubuntu@<exitramp_ip>:yourname.conf .

If the ssh succeeds but add-peer.sh isn't found yet, first boot hasn't finished — wait a minute and retry.

Now jump to Connecting below. Tear everything down anytime with terraform destroy (the Elastic IP is released — tell your vendor before you do this).

Option B: any Ubuntu/Debian VM

Works on AWS, Hetzner, DigitalOcean, Linode, anywhere.

Prerequisites: a fresh Ubuntu 22.04+/Debian 12+ VM with a static public IP and SSH access to a user with sudo. In your cloud provider's firewall, allow inbound UDP 51820 (WireGuard) and TCP 22 (SSH).

On your laptop:

git clone https://github.com/onamfc/exitramp.git
cd exitramp
scp install-server.sh add-peer.sh remove-peer.sh list-peers.sh ubuntu@YOUR_VM_IP:
ssh ubuntu@YOUR_VM_IP
# now on the VM:
chmod +x *.sh
sudo ./install-server.sh          # prints the static egress IP for your vendor
sudo ./add-peer.sh yourname       # creates your personal config + QR code
exit
# back on your laptop:
scp ubuntu@YOUR_VM_IP:yourname.conf .

(Replace ubuntu with your VM's username if different.)

Connecting (developer laptop)

Your yourname.conf is generated by sudo ./add-peer.sh yourname on the gateway — it is not created by the server install. It's a complete WireGuard client profile: your private key, your VPN address, and the gateway's endpoint and public key. The gateway keeps the master copy at /etc/exitramp/clients/yourname.conf (root-only) and drops a copy in your SSH user's home directory so you can fetch it:

scp ubuntu@<gateway-ip>:yourname.conf .

It contains a private key — treat it like an SSH key: never commit or share it, and delete stray copies once imported.

  1. Install the WireGuard app (macOS/Windows/Linux/iOS/Android).
  2. In the app: Import Tunnel(s) from File → pick yourname.conf (on mobile, scan the QR code that add-peer.sh printed instead).
  3. Toggle the tunnel on.
  4. Verify: ./client/check-ip.sh <gateway-ip> from this repo — or just run curl -4 https://checkip.amazonaws.com — and confirm it prints the gateway's IP, not your home IP.

Your daily workflow:

1. Start your local backend
2. Toggle WireGuard on
3. Run your app against localhost
4. Test the vendor integration — vendor sees the static IP
5. Toggle WireGuard off

Testing on a physical phone? The phone hits your Mac's LAN address (http://192.168.1.x:3000); only your Mac needs the tunnel, because the Mac is what calls the vendor.

Managing developers

These commands run on the gateway (SSH in first, or prefix with ssh ubuntu@<gateway-ip>):

sudo ./add-peer.sh alice                          # full tunnel
sudo ./add-peer.sh bob --split 203.0.113.0/24     # only vendor traffic tunnels
sudo ./list-peers.sh                              # who exists, last handshake
sudo ./remove-peer.sh alice                       # revoke — immediate

Full tunnel routes everything through the gateway while connected (simplest, recommended — just disconnect when not testing). Split tunnel routes only chosen vendor CIDRs: see docs/selective-routing.md.

Onboarding a teammate (anywhere in the world)

Teammates never need access to the gateway — only the admin does. To add one:

  1. Create their peer and fetch the config: ssh ubuntu@<gateway-ip> 'sudo ./add-peer.sh alice' then scp ubuntu@<gateway-ip>:alice.conf .
  2. Send them alice.conf over a secure channel (password-manager share, Signal, expiring secret link — not plain email/chat: the file is a credential). One config per person, so departures are a one-line remove-peer.sh revocation.
  3. They install the WireGuard app, import the file, delete it, and toggle the tunnel on while testing. Done — their vendor traffic now leaves from the same allowlisted IP as yours.

Geography doesn't change the setup, but it does change latency: a full tunnel sends all their traffic through your gateway's region while connected. For far-away teammates, either keep the toggle-on-only-while-testing habit or create their peer with --split so only vendor traffic makes the round trip. Peers are isolated from each other by the firewall — one connected laptop cannot reach another.

Security

The install locks the box down by default: WireGuard-key auth per developer, nftables denies everything inbound except SSH + WireGuard, forwarding is strictly vpn → internet (peers can't reach each other, the internet can't relay through you). Details and operational practices: docs/security.md.

Project layout

exitramp/
├── install-server.sh        # one-shot server setup (also used as cloud-init)
├── add-peer.sh              # onboard a developer (full or split tunnel)
├── remove-peer.sh           # revoke a developer
├── list-peers.sh            # list peers + live handshake status
├── client/check-ip.sh       # laptop-side "am I actually egressing?" check
├── terraform/aws/           # EC2 + Elastic IP, fully automated
└── docs/                    # selective routing, security model

Troubleshooting

terraform apply fails with UnauthorizedOperation. Your AWS credentials can't create EC2 resources. Use a profile with EC2 permissions: AWS_PROFILE=admin terraform apply ...

Tunnel is on but nothing loads / WireGuard shows no handshake. The client can't reach the gateway on UDP 51820. Check your cloud provider's firewall/security group allows inbound UDP 51820 from anywhere (Option A sets this up automatically). Some hotel/corporate networks block unknown UDP — try another network.

Handshake works but no internet through the tunnel. Forwarding or NAT isn't active on the gateway. SSH in and re-run sudo ./install-server.sh — it's idempotent and reapplies forwarding + firewall without touching your peers.

ssh ubuntu@<gateway-ip> times out. SSH is restricted to the admin_cidr you set at deploy time, and your IP has probably changed. Re-run terraform apply with the new admin_cidr=$(curl -4 -s https://checkip.amazonaws.com)/32. (Option B: adjust your provider's firewall instead.) The VPN keeps working either way.

add-peer.sh: command not found right after deploying. First-boot setup hasn't finished (takes ~2 minutes). Wait and retry. To watch it: ssh ubuntu@<gateway-ip> 'sudo tail -f /var/log/cloud-init-output.log'

How do I check it's working end to end? With the tunnel on: curl -4 https://checkip.amazonaws.com must print the gateway's IP. On the gateway, sudo ./list-peers.sh should show your peer's last handshake seconds ago.

FAQ

Why WireGuard and not an HTTP proxy (Squid/Tinyproxy)? A proxy needs per-app configuration (proxy agents in Node, env vars that some clients ignore) and is one firewall mistake away from being an open relay. A VPN works with every client unchanged and has no anonymous-access failure mode.

Why not a VPN provider with a dedicated IP? Shared infrastructure, less control, and many vendors refuse to allowlist known VPN ranges. This is your IP on your VM.

Can I use this in production? It's built for development. For production, put your backend somewhere with a static egress IP natively (EC2 + EIP, or NAT Gateway) — the production flow should be app → your API → vendor, with vendor credentials server-side.

What does it cost? The software is free (MIT). A t4g.nano + EIP is roughly $3–5/month on AWS; Hetzner/DigitalOcean equivalents are similar.

Roadmap

  • Terraform modules for Hetzner, DigitalOcean, Linode
  • make targets / single wrapper CLI
  • DNS-based vendor rules for split tunnel (auto-refresh resolved IPs)
  • Temporary (time-boxed) peer access
  • Headscale option for larger teams

License

MIT — see LICENSE.

About

A static exit IP for your local dev traffic - self-hosted WireGuard egress gateway for testing local code against IP-allowlisted APIs

Topics

Resources

Security policy

Stars

Watchers

Forks

Releases

Contributors

Languages