Skip to main content

Getting Started

This guide walks you through installing and configuring the Tunnel Agent to connect your on-premise services to Zihin Cloud.

1. Create the tunnel

An admin or owner of your organization creates the tunnel from the Zihin Console:

  1. Go to Management → Connectivity
  2. Click Create Tunnel
  3. Enter a label (e.g., your company name)
  4. Copy the generated token (ztun_...)
ItemExample
Agent tokenztun_a1b2c3d4e5f6...
Tunnel URLwss://tunnel.zihin.ai (default)
caution

The token is displayed only once at creation time. Copy it immediately and store it in a secure location (vault, secret manager, or .env file with restricted permissions).

2. Create the configuration file

Create a file named zihin-agent.yml:

token_env: ZIHIN_TOKEN
tunnel_url: wss://tunnel.zihin.ai
log_level: info

services:
- name: my-service
url: http://192.168.1.100:8080
inject_headers:
Authorization: "Bearer ${SERVICE_API_KEY}"

- name: another-service
url: https://internal.example.com
inject_headers:
X-API-Key: "${ANOTHER_KEY}"

Configuration reference

FieldRequiredDescription
token_envYesName of the environment variable containing the token (ZIHIN_TOKEN)
tunnel_urlNoWebSocket URL of the cloud server (default: wss://tunnel.zihin.ai)
log_levelNoLog level: debug, info, warn, error (default: info)
pool_sizeNoNumber of WebSocket connections to the cloud (1–10, default: 4). Don't exceed your tunnel's limit — extra connections wait and retry automatically
services[].nameYesService identifier — must match the name configured in Zihin Cloud
services[].urlYesInternal service URL (host and port only, no path)
services[].inject_headersNoHeaders added to every request. Supports ${VAR} for environment variables
Important

The services[].url field should contain only the host and port (e.g., https://192.168.1.100:8080). The request path is forwarded automatically by the tunnel — do not include it in the URL.

Security

Values in inject_headers are resolved locally and never sent to the cloud. Zihin Cloud only sends the service name — the agent resolves the URL and injects headers on your network.

Don't inject Accept for MCP services

MCP / streamable-HTTP servers require a specific Accept header — and the MCP client already sends it. Adding Accept to inject_headers overrides the correct value and causes 406 Not Acceptable. Use inject_headers for credentials only (tokens, API keys). The agent logs a warning at startup if an Accept header is configured.

3. Start the agent

Create a docker-compose.yml:

services:
zihin-tunnel:
image: ghcr.io/zihin-ai/tunnel-agent:1.2
container_name: zihin-tunnel
restart: unless-stopped
environment:
- ZIHIN_TOKEN=ztun_your_token_here
- SERVICE_API_KEY=your-api-key
- ANOTHER_KEY=another-key
volumes:
- ./zihin-agent.yml:/app/zihin-agent.yml:ro
# Start
docker compose up -d

# View logs
docker compose logs -f zihin-tunnel

Option B: Docker run

docker run -d \
--name zihin-tunnel \
--restart unless-stopped \
-e ZIHIN_TOKEN="ztun_your_token_here" \
-e SERVICE_API_KEY="your-api-key" \
-v $(pwd)/zihin-agent.yml:/app/zihin-agent.yml:ro \
ghcr.io/zihin-ai/tunnel-agent:1.2

Option C: Node.js (coming soon)

Coming Soon

The @zihin/tunnel-agent npm package is not yet published. Use Docker (Option A or B) for now.

4. Verify the connection

After starting the agent, the logs should show:

{"level":"info","message":"Zihin Tunnel Agent starting","services":["my-service","another-service"]}
{"level":"info","message":"WebSocket connected","index":0}
{"level":"info","message":"WebSocket connected","index":1}
{"level":"info","message":"WebSocket connected","index":2}
{"level":"info","message":"WebSocket connected","index":3}

The 4 connections (or your configured pool_size) indicate the pool is active and ready to handle requests.

Verification checklist

  • All 4 WebSocket connections established (index 0 through 3)
  • No authentication errors in logs (code: 4001 or 4003)
  • Container/process is running (docker ps or systemctl status)

5. Self-signed certificates

If your internal service uses HTTPS with a self-signed certificate or a private CA, mount the CA certificate and point NODE_EXTRA_CA_CERTS at it:

# docker-compose.yml
services:
zihin-tunnel:
image: ghcr.io/zihin-ai/tunnel-agent:1.2
environment:
- NODE_EXTRA_CA_CERTS=/app/certs/ca-cert.pem
volumes:
- ./zihin-agent.yml:/app/zihin-agent.yml:ro
- ./certs/ca-cert.pem:/app/certs/ca-cert.pem:ro
note

ssl_verify: false is deprecated and has no effect since agent 1.1.0 — TLS verification is always on. Mounting the CA is the supported path.

6. Token rotation

An admin can rotate the tunnel token from the Console at any time (Management → Connectivity → Rotate Token). When a rotation happens:

  1. A new token is generated and displayed once — copy it immediately
  2. The previous token continues working for 24 hours (grace period) — both tokens are accepted during this window
  3. Update the ZIHIN_TOKEN variable and restart the agent before the 24-hour grace period expires
# Update the token in your .env or docker-compose.yml, then:
docker compose down && docker compose up -d
warning

After the 24-hour grace period, only the new token is accepted. Make sure to update the agent configuration within this window to avoid disconnection.

7. Updating the agent

docker compose pull
docker compose up -d

The agent reconnects automatically after restart, and the pool re-establishes its connections in seconds.

tip

Agent 1.2+ adds native support for real-time (SSE) responses — required for MCP streamable HTTP. Just update the image; no configuration changes needed.

Environment-only configuration

For environments where a YAML file is impractical (Kubernetes, ECS), configure everything via environment variables:

export ZIHIN_TOKEN="ztun_your_token_here"
export ZIHIN_TUNNEL_URL="wss://tunnel.zihin.ai"
export ZIHIN_LOG_LEVEL="info"
export ZIHIN_POOL_SIZE="4" # optional, 1-10
export ZIHIN_SERVICES='[
{"name":"my-service","url":"http://192.168.1.100:8080","headers":{"Authorization":"Bearer key"}},
{"name":"another","url":"http://192.168.1.50:3000","headers":{"X-API-Key":"token"}}
]'

The agent automatically detects this: if no zihin-agent.yml is found, it uses environment variables.