Proxacloud Tunnel is a self-hosted secure tunnel platform for publishing private HTTP and TCP services through outbound agent connections. It lets a private server or Kubernetes cluster expose selected services through a public gateway without opening inbound firewall ports on the private network.
Use it when you want a managed entrypoint for private apps, SSH endpoints, internal dashboards, or Kubernetes services while keeping route configuration, agent tokens, audit logs, and operational visibility under your control.
Internet client
-> public gateway
-> authenticated tunnel session
-> private agent
-> private HTTP or TCP target
The gateway is the public network entrypoint. The agent runs inside the private network and dials out to the gateway over a persistent WebSocket connection. HTTP requests are routed by Host header, serialized across the tunnel, and replayed by the agent against the configured private target. TCP routes use public gateway listener ports and stream bytes over the same authenticated agent connection model.
The repository also includes a control-plane API, React dashboard, PostgreSQL migrations, Helm chart, Linux systemd installer, Docker Compose development stack, and observability starter configuration.
For a complete project description, architecture, workflows, data model, and deployment guidance, read docs/architecture.md.
cmd/gateway: public entrypoint that accepts internet HTTP traffic and agent tunnel connections.cmd/agent: private-network client that dials out to the gateway and forwards HTTP requests plus TCP streams to internal targets.cmd/api: control-plane API for auth, RBAC, tunnel management, route management, tokens, sessions, audit logs, and installer delivery.internal/tunnel: shared protocol, route parsing, and configuration helpers.web/dashboard: React + TypeScript + Vite dashboard.deploy/helm/proxacloud-tunnel: Helm chart for gateway, API, dashboard, and agent.deploy/observability: Prometheus, Grafana, Loki/OpenSearch guidance.
HTTP route:
1. A client sends a request to the public gateway with a route hostname.
2. The gateway checks route policy, limits, WAF rules, and connected agents.
3. The gateway forwards the request through the selected agent session.
4. The agent sends the request to the private target URL.
5. The private service response returns through the agent and gateway.
TCP route:
1. A client connects to a configured public gateway port.
2. The gateway maps that port to a tunnel and private host:port target.
3. The gateway opens a stream over the connected agent session.
4. The agent opens a private TCP connection and relays bytes both ways.
Client IP forwarding:
- The gateway forwards the public client IP to private HTTP targets through
X-Forwarded-ForandX-Real-IP. - Private, loopback, link-local, multicast, and unspecified addresses are filtered from the forwarded client identity.
- Your private web server must be configured to trust and log one of those headers if you want its access log to show the original public client IP. See docs/client-ip-logging.md.
Install Go 1.25+.
go test ./...
go build -buildvcs=false -o bin/gateway ./cmd/gateway
go build -buildvcs=false -o bin/agent ./cmd/agent
go build -buildvcs=false -o bin/api ./cmd/apiWith Docker Compose installed:
docker compose up --build
curl -H "Host: app.local.test" http://localhost:8080Apply the Phase 1 control-plane schema:
make migrate-upOn Windows PowerShell without make:
docker compose exec -T postgres psql -U proxacloud -d proxacloud -f /migrations/000001_control_plane.up.sqlExpected response:
hello from private service through proxacloud tunnel
Local service URLs:
- Gateway:
http://localhost:8080 - API:
http://localhost:8081 - Dashboard:
http://localhost:3000 - Prometheus:
http://localhost:9090 - Grafana:
http://localhost:3001, loginadmin/admin - Loki:
http://localhost:3100
Default seeded admin for local development:
Email: admin@example.com
Password: admin123456
Auth endpoints:
POST /api/v1/auth/login
POST /api/v1/auth/logout
POST /api/v1/auth/refresh
GET /api/v1/auth/me
RBAC-protected endpoints:
GET /api/v1/system requires metrics.read
GET /api/v1/tunnels requires tunnels.read
GET /api/v1/users requires users.read
GET /api/v1/roles requires roles.read
GET /api/v1/audit-logs requires audit.read
Tunnel management endpoints:
GET /api/v1/tunnels
POST /api/v1/tunnels
DELETE /api/v1/tunnels/{id}
GET /api/v1/tunnels/{id}/routes
POST /api/v1/tunnels/{id}/routes
DELETE /api/v1/routes/{id}
GET /api/v1/tunnels/{id}/tokens
POST /api/v1/tunnels/{id}/tokens
DELETE /api/v1/tokens/{id}
GET /api/v1/tunnels/{id}/sessions
GET /api/v1/tunnels/{id}/tcp-routes
POST /api/v1/tunnels/{id}/tcp-routes
DELETE /api/v1/tcp-routes/{id}
GET /api/v1/caddy/domains
POST /api/v1/caddy/domains
DELETE /api/v1/caddy/domains/{id}
Gateway dynamic routing:
- If
DATABASE_URLis set, gateway loads enabled routes from PostgreSQL. - Routes refresh every 10 seconds.
ROUTESenv values still work as fallback defaults.- Dashboard-generated agent tokens can authenticate agents to the gateway.
Managed tunnel test flow:
1. Open dashboard and create a tunnel.
2. Add route app.local.test -> http://demo:9000.
3. Generate an agent token.
4. Start an agent with TUNNEL_ID=<dashboard tunnel UUID> and TUNNEL_TOKEN=<generated token>.
5. Wait up to 10 seconds for gateway route refresh.
6. curl -H "Host: app.local.test" http://localhost:8080
Linux agent systemd install docs are in docs/linux-agent.md.
Hetzner AlmaLinux VPS deployment docs are in docs/hetzner-deployment.md.
Managed HTTP and TCP/SSH routes are configured from the dashboard. Routes can be enabled, disabled, or deleted without editing server files. A typical SSH route is public port 2222 to private target 127.0.0.1:22, then connect with:
ssh -p 2222 user@tunnel.example.comAgent tokens can be created with an expiry window, revoked from the dashboard, and rotated by generating a replacement token before revoking the old one.
The Domains page checks DNS resolution and TLS certificate status. Set PUBLIC_IP in the production environment so the dashboard can detect hostnames that do not point to the expected VPS.
Security hardening notes are in docs/security.md.
Observability docs and Grafana provisioning live in deploy/observability.
Environment variables:
| Variable | Default | Description |
|---|---|---|
GATEWAY_ADDR |
:8080 |
Gateway listen address |
TUNNEL_TOKEN |
dev-token |
Shared bearer token for agent authentication |
ROUTES |
*=dev |
Comma-separated host mapping, such as app.example.com=prod,*=dev |
GEOIP_DB_PATH |
unset | Optional MaxMind-compatible country database path, such as /etc/proxacloud/GeoLite2-Country.mmdb, used by route country blocking when no trusted country header is present |
Environment variables:
| Variable | Default | Description |
|---|---|---|
TUNNEL_ID |
dev |
Tunnel identity registered at the gateway |
TUNNEL_TOKEN |
dev-token |
Shared bearer token |
GATEWAY_URL |
ws://localhost:8080/__agent/connect |
Public gateway WebSocket URL |
TARGET_URL |
http://localhost:9000 |
Private upstream target, such as http://traefik.kube-system.svc.cluster.local |
AGENT_METRICS_ADDR |
:9091 |
Agent metrics and health listen address |
| Area | Choice |
|---|---|
| Language | Go |
| Tunnel v1 | WebSocket, HTTP/2-ready architecture |
| Future tunnel | QUIC |
| Gateway | Go reverse proxy |
| Agent | Go Kubernetes deployment |
| Database | PostgreSQL via DATABASE_URL |
| Cache | Redis via REDIS_URL, optional |
| Frontend | React + TypeScript + Vite |
| K8s packaging | Helm |
| CI/CD | GitHub Actions |
| Registry | GHCR by default, private registry supported through Helm values |
| TLS | Let's Encrypt ACME with cert-manager annotations |
| Metrics | Prometheus + Grafana |
| Logs | Structured stdout, Loki or OpenSearch downstream |
Deploy the gateway in a public environment with ports 80 and 443 exposed by a LoadBalancer or ingress. Deploy the agent inside the private Kubernetes cluster with only outbound network access to the gateway.
Typical private-cluster target:
TARGET_URL=http://traefik.kube-system.svc.cluster.local
Install chart:
helm upgrade --install proxacloud deploy/helm/proxacloud-tunnel \
--set global.imageRegistry=ghcr.io/YOUR_ORG/YOUR_REPO \
--set global.imageTag=YOUR_COMMIT_SHA \
--set gateway.ingress.enabled=true \
--set gateway.ingress.host=tunnel.example.comThe Helm chart requires immutable image tags. Use the commit SHA tag pushed by CI, not latest.
Agent-only Kubernetes install docs are in docs/kubernetes-agent.md.
Future production steps:
- Replace shared token auth with per-agent mTLS credentials.
- Add TLS automation with ACME on the gateway.
- Add PostgreSQL-backed control plane for tunnels and routes.
- Add Helm charts and CRDs for Kubernetes-native route management.
- Add Prometheus metrics and structured audit logging.
- Add HTTP/2 or QUIC transport for higher concurrency and lower latency.
The detailed implementation roadmap lives in docs/phase-roadmap.md.
Phase 1 database migrations are in migrations.