One small doorman in front of your private network. A self-hosted VPN for developers, QA and CI, shipped as a single Go binary: one open port, one token per person, and every app on your laptop reaches private databases and services as if it were sitting next to them.
Your team's Postgres lives on a cloud VM, or inside a Kubernetes cluster, with no public IP. That's exactly how the security reviewer wants it. Then someone needs to run a migration from their laptop, and CI needs to seed test data.
The usual answers all hurt:
| Option | Like… | Pain |
|---|---|---|
| Give the database a public IP | leaving the front door open | security says no |
| VNet peering / VPN gateway / bastion | building a new road between two cities | slow, cloud-specific, needs network admins |
| OpenVPN / WireGuard | hiring a security firm | keys, configs, per-device setup |
auth-vpn's answer is a doorman. One small program stands next to your private services, and it's the only thing exposed. Anyone with a valid wristband (a token) is walked through a private, encrypted hallway (a TLS 1.3 tunnel) to what they need. Everything else keeps its private IP.
Tip
Recap
- The server is the doorman, the tunnel is the hallway, and tokens are the wristbands.
- Only one port is open on the internet (
7777by default). - It's one binary with no cloud service behind it. You host it.
curl -fsSL https://github.com/adishM98/auth-vpn/releases/latest/download/install.sh \
| sudo bash -s -- --serverNo Go, no Git, nothing to install first. The installer downloads the right binary, generates a self-signed TLS cert, creates a first access token and API key, and registers a systemd service. At the end you'll see:
─────────────────────────────────────────────
Connect with:
auth-vpn connect <server-ip>:7777 --token abc123xyz
Web dashboard: https://<server-ip>:9100/ui
API key: <generated-key>
─────────────────────────────────────────────
That token is shown once. Store it in a password manager. Then create one token per person (and one for CI) from the dashboard, so you can revoke anyone without affecting the others.
curl -fsSL https://github.com/adishM98/auth-vpn/releases/latest/download/install.sh | sudo bash
sudo auth-vpn connect <server-ip>:7777 --token <token> # foreground
sudo auth-vpn connect <server-ip>:7777 --token <token> --background --reconnect # keep it running
auth-vpn status # is the tunnel up?
auth-vpn disconnectImportant
Tunnel mode needs sudo: creating the virtual network card is a root-only operation. Without it you'll see create tun: … operation not permitted. No root available? Use proxy mode instead.
The first time, you're asked to trust the server's certificate fingerprint ("trust on first use"). After that, auth-vpn remembers it and refuses impostors. Save a profile so you never retype the token:
auth-vpn profile save staging --host <server-ip>:7777 --token <token>
sudo auth-vpn connect staging --background --reconnectEvery service on the server machine is now at 10.8.0.1:
psql -h 10.8.0.1 -p 5432
redis-cli -h 10.8.0.1 -p 6379
curl http://10.8.0.1:8080/healthOnly 10.8.0.0/24, plus any routes you or the server add, goes through the tunnel. Your normal internet traffic is untouched.
Sharing with your team? docs/dev-qa-guide.md is a standalone onboarding page for developers and QA. Send it to anyone who needs access.
Read it left to right: outside → the server's rooms → the private network. The server has one front door (:7777, TLS 1.3) plus a few side rooms: an SSH window, side doors for whitelisted IPs, and a control room (the dashboard and API on :9100).
- Knock: TLS handshake, then
AUTH {token}. - Get a seat: the server hands out the lowest free address in
10.8.0.2–254. - Build the hallway: your laptop creates a virtual network card (TUN) and adds routes. In Kubernetes it also gets the cluster's routes and DNS automatically.
- Walk through: every packet to
10.8.0.x(or a pushed route) is wrapped in a frame and sent down the tunnel. Replies come back the same way.
The order matters, like airport security: there's no point reading a boarding pass if the person is on the no-fly list. Five wrong tokens in a minute earns a one-minute timeout.
Tip
Recap
- One TLS port in, a
/24of tunnel addresses, routes only for what you need. - Front-door checks: banned? → AUTH? → whitelisted? → token? → TUN or proxy?
- Deep dive: docs/architecture.md.
| Door | How | Root on your side? | Reaches |
|---|---|---|---|
| TUN (default) | virtual network card; every app just works | yes (sudo) |
the tunnel subnet, --route CIDRs, server-pushed routes |
| Proxy | --forward 5432:10.8.0.1:5432: local port ↔ remote host:port |
no | specific host:port pairs (the server dials them) |
| SSH | built-in SSH server on :2222, ssh -L only |
no (any SSH client) | 127.0.0.0/8 and 10.8.0.0/24 on the server |
| Direct forward | the server pipes a port to a fixed target | no client at all | what the admin configured, from whitelisted IPs only |
# Proxy mode — no sudo, great for containers and CI
auth-vpn connect <server-ip>:7777 --token <token> --forward 5432:10.8.0.1:5432
psql -h 127.0.0.1 -p 5432Run auth-vpn as a pod. It already lives inside the cluster network, so your laptop can reach ClusterIP services by name, with no VNet peering, no --route flags, and no public IP per service.
kubectl apply -k "github.com/adishM98/auth-vpn/k8s?ref=main" # namespace, read-only RBAC, auth-vpn
kubectl logs -n auth-vpn deploy/auth-vpn | grep "auth-vpn connect" # first-boot token (shown once)
kubectl get svc -n auth-vpn auth-vpn # LoadBalancer IP
sudo auth-vpn connect <LB-IP>:7777 --token <token>
psql -h postgres.myns.svc.cluster.local # cluster DNS works through the tunnelOn connect, the pod pushes two things it detects for you: a route for the cluster's service range, and split DNS for *.cluster.local. Your laptop checks what it's handed before using it. It never accepts a default route, anything covering the server's own IP, or a bare TLD, and you can switch it off with --no-push-routes / --no-push-dns.
Want only Grafana behind the VPN, with everything else exactly as it is?
kubectl set env -n auth-vpn deploy/auth-vpn AUTH_VPN_EXPOSE=labeled
kubectl label svc grafana -n monitoring auth-vpn.io/expose=trueauth-vpn then routes and allows only labelled Services. It enforces that itself, per packet and per proxy dial, so it works even on clusters with no NetworkPolicy engine. It also fails closed: if the API is unreachable, it keeps the last list.
Tip
Recap
- In a cluster, auth-vpn speaks for you (MASQUERADE), so you reach what the pod can reach.
- Routes and DNS are pushed automatically. Clients need v2.6.0+.
- Labeled mode = an allow-list you manage with
kubectl label.
Full guide, including the recommended overlay, upgrades and troubleshooting: docs/k8s-deployment.md.
Live clients, traffic, tokens, IP whitelist, SSH keys and direct forwards, all on port 9100, which speaks both http:// and https://:
| From | Open |
|---|---|
| a connected VPN client | http://10.8.0.1:9100/ui |
the server, ssh -L 9100:localhost:9100, or kubectl port-forward |
http://localhost:9100/ui |
| anywhere else | https://<server-ip>:9100/ui (self-signed cert) |
Plain http:// from the internet is redirected to https://, so the API key never crosses the internet unencrypted. Paste the key into the page rather than into ?key= URLs. Prometheus metrics are at /metrics:
auth_vpn_uptime_seconds auth_vpn_active_connections
auth_vpn_connections_total auth_vpn_auth_failures_total
auth_vpn_bytes_in_total auth_vpn_bytes_out_total
auth_vpn_dropped_packets_total
Two steps per job. Every job gets its own short-lived token, created at the start and revoked at the end, so parallel matrix jobs never share credentials.
steps:
- name: Connect to VPN
uses: adishM98/auth-vpn@v2
with:
server: ${{ secrets.VPN_SERVER }} # <server-ip>:7777
api-key: ${{ secrets.VPN_API_KEY }} # api_key from /etc/auth-vpn/server.yaml
api-url: https://<server-ip>:9100
# services on the server are now at 10.8.0.1 (or by name, if the server runs in Kubernetes)
- name: Disconnect VPN
if: always()
uses: adishM98/auth-vpn/disconnect@v2More: docs/github-actions.md (matrix jobs, proxy mode, inputs, non-GitHub CI).
auth-vpn hub serve gives you one local dashboard over every server (prod, staging, eu-west…), using each server's API key. See docs/hub.md.
- TLS 1.3 tunnel, with the server's self-signed cert pinned on first use (TOFU). A changed cert is refused.
- Tokens: only a SHA-256 hash is stored on the server. Tokens can be permanent, expiring or one-time. Revoking one blocks new connections immediately, and Kick (dashboard) ends a live session.
- Rate limiting: 5 failed auths in a minute → 60-second ban for that IP.
- Ephemeral CI tokens: one per job, revoked on exit, with a 2-hour expiry as a backstop.
- ACL rules: per-device allow/deny by protocol and port (optional).
- Kubernetes labeled mode: only labelled Services are reachable. It's enforced by the server and fails closed.
- Pushed settings are filtered client-side: no default route, nothing covering the server IP, no bare TLDs.
- API key for the dashboard and API. It's never sent unencrypted over the internet.
- IP whitelist, direct forwards and SSH tunnel are there for machines that can't run the client.
URL=https://github.com/adishM98/auth-vpn/releases/latest/download/install.sh
curl -fsSL $URL | sudo bash # client
curl -fsSL $URL | sudo bash -s -- --server # server, port 7777
curl -fsSL $URL | sudo bash -s -- --server --port=8888 # server, another port
curl -fsSL $URL | sudo bash -s -- --version=v2.9.0 # pin a release (add to any line)Piped installs never prompt, so the port is 7777 unless you pass --port=<n> or set TJ_VPN_PORT. To be asked for the port, run ./install.sh --server from a clone.
Deploy from your Mac (the VM needs nothing): make deploy VM=azureuser@<vm-ip> for a server, make deploy-client VM=… for a client.
Docker Compose: containers must publish on 0.0.0.0 ("5432:5432"), not 127.0.0.1:5432:5432, or the tunnel can't reach them.
Requires Go 1.25+.
git clone https://github.com/adishM98/auth-vpn && cd auth-vpn
go build -o auth-vpn ./cmd # current platform
make build-all # linux/amd64, darwin/amd64, darwin/arm64 → dist/| Make target | What it does |
|---|---|
make build-linux / build-mac-arm / build-mac-intel / build-all |
build into dist/ |
make install / make install-server |
install client / server on this machine |
make deploy VM=user@host / make deploy-client VM=user@host |
build and install on a remote VM |
make release |
build all + publish a GitHub release |
make clean |
remove dist/ |
sudo auth-vpn update # latest release; restarts the systemd service if active
auth-vpn version
sudo auth-vpn server change-port # move the tunnel port (update firewalls + client profiles too)Re-running the server installer is safe: it keeps server.yaml, the TLS cert and all tokens.
# client
auth-vpn disconnect && sudo rm /usr/local/bin/auth-vpn && rm -rf ~/.auth-vpn
# server
sudo systemctl disable --now auth-vpn
sudo rm -f /usr/local/bin/auth-vpn /etc/systemd/system/auth-vpn.service /var/run/auth-vpn.sock
sudo rm -rf /etc/auth-vpn && sudo systemctl daemon-reload| If you want to… | Read |
|---|---|
| understand how it works inside (frames, front door, packet path) | docs/architecture.md |
| run it in Kubernetes, with labeled mode | docs/k8s-deployment.md |
| use the dashboard, API, whitelist, direct forwards, SSH | docs/server-features.md |
| look up a command or setting | docs/cli-reference.md |
| wire up CI | docs/github-actions.md |
| manage many servers | docs/hub.md |
| onboard a developer / QA | docs/dev-qa-guide.md |
| step-by-step VM install | INSTALL.md |
All diagrams are in docs/diagrams/: an .svg for reading and an .excalidraw for editing. To redraw them: cd docs/diagrams/_src && npm install && npm run build.