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
enabledtotruein 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_idhas 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_requestsor reduce upstream webhook concurrency.