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:
- Go to Management → Connectivity
- Click Create Tunnel
- Enter a label (e.g., your company name)
- Copy the generated token (
ztun_...)
| Item | Example |
|---|---|
| Agent token | ztun_a1b2c3d4e5f6... |
| Tunnel URL | wss://tunnel.zihin.ai (default) |
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
| Field | Required | Description |
|---|---|---|
token_env | Yes | Name of the environment variable containing the token (ZIHIN_TOKEN) |
tunnel_url | No | WebSocket URL of the cloud server (default: wss://tunnel.zihin.ai) |
log_level | No | Log level: debug, info, warn, error (default: info) |
pool_size | No | Number of WebSocket connections to the cloud (1–10, default: 4). Don't exceed your tunnel's limit — extra connections wait and retry automatically |
services[].name | Yes | Service identifier — must match the name configured in Zihin Cloud |
services[].url | Yes | Internal service URL (host and port only, no path) |
services[].inject_headers | No | Headers added to every request. Supports ${VAR} for environment variables |
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.
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.
Accept for MCP servicesMCP / 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
Option A: Docker (recommended)
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)
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 (
index0 through 3) - No authentication errors in logs (
code: 4001or4003) - Container/process is running (
docker psorsystemctl 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
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:
- A new token is generated and displayed once — copy it immediately
- The previous token continues working for 24 hours (grace period) — both tokens are accepted during this window
- Update the
ZIHIN_TOKENvariable 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
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.
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.