Skip to main content

Tunnel Webhooks (Reverse Tunnel)

This guide explains how to receive third-party webhooks (Stripe, GitHub, etc.) on a machine that is not directly reachable from the public internet.

cert-ctrl provides a minimal HTTPS-only reverse tunnel:

  • A cert-ctrl client (agent) opens a WebSocket connection to the server: wss://<server>/api/tunnel.
  • The server exposes a public HTTPS endpoint: https://<server>/hooks/<tunnel_id>.
  • Incoming HTTP requests are forwarded over the WebSocket tunnel to your client, then forwarded to your local HTTP service.

Prerequisites

  • You have a cert-ctrl device/client already installed and logged in.
  • You have a local HTTP service to handle the webhook (for example http://127.0.0.1:9000).
  • Your cert-ctrl client has the tunnel feature enabled (see next section).

Enable the cert-ctrl client tunnel

The tunnel feature is gated by a config flag.

Example config (initial revision):

{
"enabled": false,
"remote_endpoint": "wss://api.cjj365.cc/api/tunnel",
"webhook_base_url": "https://hook.cjj365.cc/hooks",
"local_base_url": "http://127.0.0.1:9000",
"verify_tls": true,
"request_timeout_seconds": 45,
"ping_interval_seconds": 20,
"max_concurrent_requests": 12,
"max_payload_bytes": 5242880,
"header_allowlist": [
"content-type",
"user-agent"
]
}

To enable:

  • Set enabled to true in your tunnel config, or
  • If you use the agent CLI, toggle it via cert-ctrl conf set tunnel.enabled true.

Then restart the agent (or reload config, depending on your deployment).

Find your tunnel id

A tunnel id (tunnel_id) is assigned by the server during WebSocket handshake.

  • Open the Web UI page: Tunnel Webhook History.
  • If a client is connected, the page shows Connected tunnel id(s).
  • If you see No connected client, start/restart the agent and refresh the page.

Send webhook requests

Use the connected tunnel_id as part of the public callback URL:

  • POST https://<server>/hooks/<tunnel_id>

Example (test locally with curl):

curl -X POST "https://cjj365.cc/hooks/<tunnel_id>" \
-H "Content-Type: application/json" \
-d '{"hello":"world"}'

The server includes a x-tunnel-request-id response header. You can use this request id to locate the entry in the Web UI.

Common failure modes

  • No connected client: there is no active WebSocket tunnel session. Start/restart the agent and confirm it can reach wss://<server>/api/tunnel.
  • 502 Tunnel not connected: the request hit the server, but the specified tunnel_id has no active session.
  • 504 Tunnel request timed out: the client did not respond within request_timeout_seconds.
  • 429 Tunnel at capacity: too many concurrent in-flight requests; increase max_concurrent_requests or reduce upstream webhook concurrency.