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.
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.
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.
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 configureonce, 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/*.pubshows 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 theubuntuuser.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 sameterraform applywith 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).
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.)
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.
- Install the WireGuard app (macOS/Windows/Linux/iOS/Android).
- In the app: Import Tunnel(s) from File → pick
yourname.conf(on mobile, scan the QR code thatadd-peer.shprinted instead). - Toggle the tunnel on.
- Verify:
./client/check-ip.sh <gateway-ip>from this repo — or just runcurl -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.
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 — immediateFull 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.
Teammates never need access to the gateway — only the admin does. To add one:
- Create their peer and fetch the config:
ssh ubuntu@<gateway-ip> 'sudo ./add-peer.sh alice'thenscp ubuntu@<gateway-ip>:alice.conf . - Send them
alice.confover 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-lineremove-peer.shrevocation. - 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.
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.
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
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.
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.
- Terraform modules for Hetzner, DigitalOcean, Linode
-
maketargets / single wrapper CLI - DNS-based vendor rules for split tunnel (auto-refresh resolved IPs)
- Temporary (time-boxed) peer access
- Headscale option for larger teams
MIT — see LICENSE.