Waygate is an open-source, self-hosted tunnel for securely exposing local services through infrastructure you control.
It sits in the same problem space as ngrok and Cloudflare Tunnel, but is designed for teams that want to run the relay and control plane themselves, issue their own credentials, and control how sessions, URLs, and access policies work.
- Shares
localhostservices from a laptop or workstation through an outbound tunnel - Issues short-lived share sessions with configurable expiry
- Generates unique browser URLs such as
https://calm-harbor-x8k1.tunnel.example.com - Supports browser-friendly HTTP access and SOCKS5 proxy access
- Uses control-plane-issued JWTs and short-lived certificates for session auth
- Lets operators validate real user API keys before a session is created
- Works as a starting point for more advanced policies like device binding, private ingress, or internal-only networking
| Component | Binary | Purpose |
|---|---|---|
| Agent | waygate-agent |
Runs on the developer machine and connects local services to the relay |
| Relay | waygate-relay |
Accepts agent tunnels and serves browser/proxy traffic |
| Control plane | waygate-cp |
Creates sessions, validates API keys, and issues session credentials |
- A developer runs
waygate-agent share --port 3000. - The agent authenticates to the control plane with an API key.
- The control plane creates a session, mints a session JWT, generates a unique share hostname/token, and returns the share URL.
- The agent obtains a short-lived certificate and connects outbound to the relay over WebSocket.
- Browser traffic to the relay is forwarded over the tunnel to the developer's local service.
- You want full control over where traffic flows and how logs are stored.
- You want your own domain, your own Nginx, and your own certificates.
- You need session expiry, custom access logic, or internal/private networking that public tunnel vendors do not match.
- You want a hackable codebase you can extend for your own platform.
Self-hosting Waygate gives you flexibility that is hard to get from managed tunnel products.
- You can plug in your own authentication and authorization logic instead of being limited to a vendor's account model.
- You can issue your own API keys, session tokens, and certificates with policies that match your product or organization.
- You control the relay, control plane, logs, domains, and TLS termination points.
- You can keep traffic inside your own cloud, VPC, VPN, or private network boundary.
- You can expose services on your own wildcard domains and integrate them with your existing DNS and certificate setup.
- You can enforce custom rules such as session expiry, per-user limits, per-tenant routing, or device-bound access.
- You are free to add platform-specific behavior such as internal headers, audit hooks, custom session stores, or approval workflows.
- You avoid vendor lock-in and can evolve the tunnel behavior as your product grows.
- Share flow with
login,share,connect, andlogout - Real API key validation via a control-plane key file
- Wildcard browser URLs through
--browser-base-host - Short-lived URLs with user-configurable expiry from the agent
- Direct relay mode for local development
- Cross-platform builds for macOS, Linux, and Windows
- To build Waygate from source, install Go
1.25or newer. - To run prebuilt Waygate binaries, Go is not required.
- To terminate TLS with Nginx and Certbot, install those separately on your server.
Choose one of these two paths first:
make allOr build every supported target:
make dist-allThat generates binaries like:
bin/waygate-agentbin/waygate-relaybin/waygate-cpbin/waygate-agent-linux-amd64bin/waygate-relay-linux-amd64
If you already have the binaries, place them somewhere convenient, for example:
bin/waygate-agent
bin/waygate-relay
bin/waygate-cp
In this mode, Go is not required. You only need the correct binaries for your operating system and architecture.
If you want to run:
waygate-agent share --port 3000instead of:
./bin/waygate-agent share --port 3000install the binary into a directory that is already on your shell PATH, or use the helper script:
bash ./scripts/install-waygate-agent.sh ./bin/waygate-agentBy default this installs to ~/.local/bin/waygate-agent.
If ~/.local/bin is not already on your PATH, add this to your shell profile such as ~/.zshrc or ~/.bashrc:
export PATH="$HOME/.local/bin:$PATH"Then reload your shell:
source ~/.zshrcAfter that, you can run:
waygate-agent share --port 3000To install system-wide instead, copy the binary to /usr/local/bin with the appropriate privileges.
Use the example in examples/api-keys.json.example.
Minimal example:
{
"keys": [
{
"name": "Example User",
"api_key": "replace-this-with-a-real-secret",
"user_id": "example-user",
"tenant_id": "example-tenant",
"can_create": true
}
]
}For production-style browser URLs, choose a public hostname for your relay, such as relay.example.com, and a wildcard under it such as *.relay.example.com.
Before starting the relay or control plane, generate the JWT signing keypair and session CA:
bash ./scripts/generate-waygate-keys.sh ./certsOn a Linux host you can write them directly to /etc/waygate:
sudo mkdir -p /etc/waygate
sudo bash ./scripts/generate-waygate-keys.sh /etc/waygate./bin/waygate-relay serve \
--addr 127.0.0.1:8080 \
--jwt-key /etc/waygate/jwt-public.pem \
--browser-base-host relay.example.com \
--dev=false \
--verbose./bin/waygate-cp serve \
--addr 127.0.0.1:9090 \
--api-base-url https://api.example.com \
--relay wss://relay.example.com/tunnel/connect \
--api-key-file /etc/waygate/api-keys.json \
--jwt-key /etc/waygate/jwt-private.pem \
--ca-cert /etc/waygate/ca-cert.pem \
--ca-key /etc/waygate/ca-key.pem \
--verbose./bin/waygate-agent login \
--api-key replace-this-with-a-real-secret \
--api-endpoint https://api.example.com./bin/waygate-agent share --port 3000 --expiry 2hExpected output:
Share URL: https://calm-harbor-x8k1.relay.example.com/
Expires At: 2026-04-12T18:30:00Z
If your local app lives at http://localhost:3000/docs/, open:
https://calm-harbor-x8k1.relay.example.com/docs/
To use meaningful per-session subdomains, you need:
- one public hostname for the relay, for example
relay.example.com - a wildcard DNS record for the relay, for example
*.relay.example.com - one public hostname for the control plane, for example
api.example.com - Nginx terminating TLS and proxying both normal HTTP and WebSocket upgrade traffic
Example Nginx layout:
api.example.com->127.0.0.1:9090relay.example.comand*.relay.example.com->127.0.0.1:8080
Minimal relay server block:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name relay.example.com *.relay.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}Then issue certs with Certbot using a DNS challenge for the wildcard name:
sudo certbot certonly \
--manual \
--preferred-challenges dns \
-d relay.example.com \
-d '*.relay.example.com' \
-d api.example.comYou can also use a provider-specific Certbot DNS plugin instead of --manual.
The recommended self-hosted deployment pattern is:
- Provision one cloud VM or container host for the relay and control plane.
- Point your public DNS names at that host.
- Run Nginx in front of both services.
- Issue a TLS certificate for the relay hostname, its wildcard subdomain, and the control-plane hostname.
- Run
waygate-relayon127.0.0.1:8080. - Run
waygate-cpon127.0.0.1:9090. - Give users an API key from your control-plane key file.
- Ask users to run
waygate-agent loginonce, thenwaygate-agent share --port <port>.
Example hostname layout:
- Relay API and browser ingress:
relay.example.com - Wildcard share URLs:
*.relay.example.com - Control plane API:
api.example.com
Example DNS records:
A relay.example.com -> <your-server-ip>A *.relay.example.com -> <your-server-ip>A api.example.com -> <your-server-ip>
If your DNS provider does not support wildcard A records directly, use the equivalent wildcard record type it supports.
Waygate currently uses two layers:
- User API keys for authenticating the agent to the control plane
- Per-session JWTs and short-lived certs for authenticating the agent to the relay
The control plane will reject login and session creation if the API key is not present in the configured key file.
Users can choose expiry directly from the agent:
./bin/waygate-agent share --port 3000 --expiry 30m
./bin/waygate-agent share --port 3000 --expiry 24hCurrent limits:
- Minimum:
5m - Maximum:
168h - Default:
4h
Stores the control-plane endpoint and API key in the local user config directory.
./bin/waygate-agent login \
--api-key replace-this-with-a-real-secret \
--api-endpoint https://api.example.comEnvironment variables are also supported:
WAYGATE_API_KEYWAYGATE_API_ENDPOINT
Legacy TUNNEL_API_KEY and TUNNEL_API_ENDPOINT are still accepted for compatibility.
Creates a new share session and keeps the tunnel alive.
./bin/waygate-agent share --port 3000
./bin/waygate-agent share --port 3000 --local-host localhost --expiry 2hLower-level command for direct relay or explicit control-plane wiring.
./bin/waygate-agent connect \
--relay ws://localhost:8080/tunnel/connect \
--local-ports 3000 \
--insecureRuns the relay and browser/proxy entrypoint.
Important flags:
--browser-base-host--jwt-key--socks5-user--socks5-pass--dev
Runs the control plane.
Important flags:
--api-key-file--api-base-url--relay--jwt-key--ca-cert--ca-key
The repository includes:
The cross-platform build script is:
It removes older generated tunnel binaries and rebuilds fresh binaries for all supported targets.
cmd/
agent/
relay/
controlplane/
internal/
agent/
relay/
controlplane/
shared/
scripts/
examples/
- Stronger multi-tenant policy controls
- Device-bound access enforcement beyond share URL secrecy
- Better response/header rewriting for complex browser apps
- Better operator UX around API key provisioning and revocation
- Production deployment manifests and systemd examples