Expose your gateway with a tunnel
Make the gateway reachable from the internet with Cloudflare, Tailscale, ngrok, Pinggy, OpenVPN, or a custom command.
By default the Revka gateway binds to 127.0.0.1 and is reachable only from the machine it runs on. To accept webhooks, pair a phone over the internet, or reach the dashboard from another network, you need a public ingress. Revka ships a built-in tunnel layer that wraps an external tunnel tool — Cloudflare, Tailscale, ngrok, Pinggy, OpenVPN, or any command you supply — and starts and stops it in lockstep with the daemon.
This guide explains the two ways to reach the gateway from outside (allow_public_bind for trusted LANs, a tunnel for the public internet), then walks through configuring each provider. For the gateway server itself see Run the dashboard and revka gateway, daemon & service.
Two ways to be reachable
Section titled “Two ways to be reachable”The gateway never reaches the public internet on its own. You choose one of two exposure models in [gateway] and [tunnel]:
- Direct public bind — bind the listener to a non-loopback address (
0.0.0.0,[::], or a LAN IP). Use this on a trusted LAN or inside Docker, where the network boundary is your firewall. Controlled byallow_public_bind. - Tunnel — keep the gateway on
127.0.0.1and let a tunnel provider forward an internet-facing URL to it. This is the recommended way to expose Revka to the public internet, because the listener itself stays on loopback.
You can combine them, but you rarely need to: a tunnel forwards to localhost just fine.
Bind addresses and allow_public_bind
Section titled “Bind addresses and allow_public_bind”The listen address comes from [gateway].host. Any address other than loopback (127.0.0.1, localhost, ::1) is treated as a public bind.
[gateway]host = "127.0.0.1" # default — localhost onlyport = 42617allow_public_bind = false # guard against accidental exposure| Key | Type | Default | Meaning |
|---|---|---|---|
host | string | "127.0.0.1" | Bind address. 0.0.0.0 = all IPv4 interfaces, [::] = all IPv6 interfaces. Env: REVKA_GATEWAY_HOST |
port | int | 42617 | Listen port. Env: REVKA_GATEWAY_PORT |
allow_public_bind | bool | false | Acknowledge a non-loopback bind. Env: REVKA_ALLOW_PUBLIC_BIND |
On the CLI you can override the host and port per run:
revka daemon --host 127.0.0.1 # localhost only (default)revka daemon --host 0.0.0.0 # all interfaces, LAN accessrevka daemon --port 9090 # override the gateway portWhen you start the gateway on a public address without a configured tunnel and without allow_public_bind = true, Revka logs a warning at startup pointing you at the three safe options: stay on 127.0.0.1, configure a tunnel, or set allow_public_bind = true to acknowledge the exposure and silence the warning. Configuring a tunnel suppresses the warning automatically.
The [tunnel] section
Section titled “The [tunnel] section”A tunnel wraps an external tool. Revka spawns the binary (or, for Pinggy, an SSH session), watches its output for the public URL, prints it, and terminates it cleanly when the daemon shuts down. Select a provider with tunnel.provider and supply that provider’s sub-section.
[tunnel]provider = "cloudflare" # none | cloudflare | tailscale | ngrok | openvpn | pinggy | custom
[tunnel.cloudflare]token = "eyJhIjoiMTI..."| Field | Type | Default | Meaning |
|---|---|---|---|
tunnel.provider | string | "none" | Which provider to start. Empty string is the same as "none". |
The provider value is matched case-insensitively, so legacy PascalCase configs such as "CloudFlare" still work — but use the canonical lowercase form in new configs. If provider is anything other than "none", the matching sub-section must be present and its required fields set, or the daemon refuses to start with an error such as tunnel.provider = "ngrok" but [tunnel.ngrok] section is missing.
You can configure the common providers interactively in step 5 of revka onboard (Cloudflare, Tailscale, ngrok, and Custom). OpenVPN and Pinggy are configured by editing config.toml.
Providers
Section titled “Providers”Wraps the cloudflared binary and a Cloudflare Zero Trust tunnel token. On start Revka runs cloudflared tunnel --no-autoupdate run against your local port and reads the public URL from stderr (up to a 30-second wait).
[tunnel]provider = "cloudflare"
[tunnel.cloudflare]token = "eyJhIjoiMTI..." # required — from the Zero Trust dashboard| Field | Type | Required | Meaning |
|---|---|---|---|
token | string | yes | Cloudflare tunnel token from the Zero Trust dashboard |
Requires cloudflared installed and on PATH. Note that cloudflared prints its URL to stderr, not stdout.
Wraps the tailscale CLI. The default mode is tailscale serve, which exposes the gateway only to devices on your tailnet. Set funnel = true for tailscale funnel, which publishes to the public internet. The hostname is auto-detected from tailscale status --json when omitted.
[tunnel]provider = "tailscale"
[tunnel.tailscale]funnel = false # false = serve (tailnet only); true = funnel (public)hostname = "" # auto-detected if empty| Field | Type | Default | Meaning |
|---|---|---|---|
funnel | bool | false | false = tailnet-only serve, true = public funnel |
hostname | string | auto | Tailnet hostname; detected from tailscale status if empty |
Requires tailscale installed and authenticated (tailscale up). Funnel mode also needs Tailscale Funnel enabled on the account. The [tunnel.tailscale] section is optional; omitting it uses the defaults above.
Wraps the ngrok CLI. Revka registers the auth token with ngrok config add-authtoken, starts the tunnel, and parses the public URL from ngrok’s logfmt output (15-second wait).
[tunnel]provider = "ngrok"
[tunnel.ngrok]auth_token = "2abc..." # requireddomain = "my-custom.ngrok.app" # optional, paid plan| Field | Type | Required | Meaning |
|---|---|---|---|
auth_token | string | yes | Your ngrok auth token |
domain | string | no | Reserved custom domain (requires a paid ngrok plan) |
Requires ngrok installed. On the free tier the URL is an auto-generated *.ngrok-free.app address that changes on every restart — reserve a domain for a stable URL.
Uses SSH reverse port forwarding through the system ssh command — no extra binary to install. The free tier connects to free.pinggy.io; supplying a Pro token switches to pro.pinggy.io. Revka extracts the *.pinggy.link URL from the SSH output (15-second wait).
[tunnel]provider = "pinggy"
[tunnel.pinggy]token = "your-pro-token" # optional — omit for the free tierregion = "us" # optional regional prefix, e.g. "us", "eu"| Field | Type | Required | Meaning |
|---|---|---|---|
token | string | no | Pinggy Pro token; omit to use the free tier |
region | string | no | Regional endpoint prefix |
Requires only ssh. The session connects over port 443, which passes through most firewalls, and accepts the host key on first connect (StrictHostKeyChecking=accept-new). Free-tier tunnels are ephemeral.
Wraps the openvpn binary against a client .ovpn profile. Revka waits for OpenVPN’s Initialization Sequence Completed marker, then reports the address you configure in advertise_address as the public URL — OpenVPN does not auto-detect a hostname the way the HTTP tunnels do.
[tunnel]provider = "openvpn"
[tunnel.openvpn]config_file = "/etc/openvpn/client.ovpn" # requiredauth_file = "/etc/openvpn/auth.txt" # optional, for --auth-user-passadvertise_address = "10.8.0.2:42617" # optional, reported as the public URLconnect_timeout_secs = 30 # default 30extra_args = ["--verb", "3"] # optional extra openvpn flags| Field | Type | Required | Meaning |
|---|---|---|---|
config_file | string | yes | Path to the .ovpn client profile |
auth_file | string | no | Credentials file for --auth-user-pass |
advertise_address | string | no | Address reported as the public URL; falls back to the local gateway address |
connect_timeout_secs | int | no (30) | How long to wait for the connection to come up |
extra_args | array | no | Extra flags forwarded to openvpn |
Requires openvpn installed and usually root/administrator privileges to create the tun/tap device.
Bring your own tunnel. Revka spawns an arbitrary command, substituting {host} and {port} placeholders, and optionally extracts the public URL from stdout. If no URL is found it falls back to http://{host}:{port}.
[tunnel]provider = "custom"
[tunnel.custom]start_command = "bore local {port} --to bore.pub" # requiredhealth_url = "https://bore.pub/health" # optional liveness probeurl_pattern = "bore.pub" # optional URL-extraction hintOther examples:
start_command = "ssh -R 80:localhost:{port} serveo.net"start_command = "frp -c /etc/frp/frpc.ini"| Field | Type | Required | Meaning |
|---|---|---|---|
start_command | string | yes | Command to run; {host} and {port} are substituted |
health_url | string | no | HTTP URL polled (5s timeout) for the health check |
url_pattern | string | no | Substring used to find the public-URL line in stdout |
The command is split on whitespace — there is no shell expansion. URL extraction looks for http:// / https:// on the line that matches url_pattern. If health_url is unset, the health check falls back to a process-liveness check.
The default. No tunnel is started; the gateway is reachable only at http://{host}:{port}. Set this (or omit [tunnel]) when you expose the gateway some other way or keep it local.
[tunnel]provider = "none" # or omit the [tunnel] section entirelySet up a tunnel
Section titled “Set up a tunnel”-
Install the provider’s tool. Cloudflare needs
cloudflared; Tailscale needstailscale; ngrok needsngrok; OpenVPN needsopenvpn. Pinggy and Custom (with an SSH-based command) only needssh. -
Add the
[tunnel]section to~/.revka/config.tomlwith your provider and its credentials, using the examples above. -
Keep the gateway on localhost. Leave
[gateway].host = "127.0.0.1"— the tunnel forwards to the local port, so you do not needallow_public_bind. -
Start the daemon. Revka launches the tunnel alongside the gateway and prints the public URL.
Terminal window revka daemon# 🔗 Starting cloudflare tunnel...# 🌐 Public URL: https://your-tunnel.example.com -
Pair over the public URL. Open the printed URL and complete pairing. The 6-digit code is shown in your terminal (not in a remote browser) — see Run the dashboard for the remote pairing flow.
Verify and troubleshoot
Section titled “Verify and troubleshoot”Check that the gateway answers locally first — the health endpoint needs no auth and leaks no secrets:
curl http://127.0.0.1:42617/health# {"status":"ok","paired":true,"require_pairing":true,...}Then hit the same path through the tunnel URL. If the local check passes but the tunnel does not:
- The daemon exits with a missing-section error.
provideris set but the matching[tunnel.<provider>]sub-section (or a required field liketoken/auth_token/config_file) is absent. Add it. - No public URL is printed. The provider tool is not on
PATH, or it did not emit a URL inside the timeout (30s Cloudflare, 15s ngrok/Pinggy). Run the tool by hand to confirm it works, then check the daemon logs. - The dashboard says it can’t reach the gateway from another machine. You are binding loopback with no tunnel. Add a
[tunnel]section, or setallow_public_bind = truefor a trusted LAN.
Run revka doctor for structured diagnostics. For network-deployment specifics (LAN binding, reverse proxies, Raspberry Pi) see Network deployment, Raspberry Pi & proxy.