Skip to content

Infrastructure: Full API Isolation via Cloudflare Tunnel

Problem with the current setup

All services share a VPS with published Docker ports. Three weaknesses compound each other:

  1. IP bypass risk — even with CF Proxy (orange cloud), the VPS still listens on public ports. If the real IP leaks (DNS history, old A records, misconfigured service), an attacker can bypass CF WAF, rate limits, and Access policies entirely:
    curl --resolve api.verifluence.io:443:<VPS_IP> https://api.verifluence.io/api/...
  2. Admin secret in the bundleVITE_ADMIN_SECRET is baked into the admin panel JS at build time. Anyone who inspects the deployed bundle obtains the secret.
  3. No process boundary — admin routes and public API routes share one process and one port. A single exploit in any route potentially exposes all routes.

Proposed architecture

Cloudflare EdgeVPS — Docker Composeapp.verifluence.io(CF Pages)admin.verifluence.io(CF Pages + CF Access)CF Tunnel(4 outbound connectionsfrom VPS to CF PoPs)cloudflared(tunnel daemon)api:3000(public API)admin-api:3002(admin API)kick-server:3000(webhook receiver)postgres:5432(loopback only)HTTPSHTTPS + CF Access(@verifluence.io only)encrypted tunnel(outbound from VPS)http://api:3000http://admin-api:3002http://kick-server:3000

Key property

cloudflared makes outbound connections to Cloudflare's PoPs. The VPS has no inbound ports — there is no socket to connect to, even if the IP is known. CF WAF, rate limits, and Access policies cannot be bypassed.

Traffic flow

OriginRouteAuth at edge
app.verifluence.ioCF Pages → CF Tunnel → api:3000None (IS-9 session inside API)
admin.verifluence.ioCF Pages → CF Access → CF Tunnel → admin-api:3002Google SSO — @verifluence.io only
Kick webhookkick.verifluence.io → CF Tunnel → kick-server:3000Kick HMAC signature in handler
Postgres (ops)SSH tunnel → 127.0.0.1:5432SSH key

What changes

1. docker-compose.yml

All ports: replaced with expose: — nothing published to the host. Postgres bound to 127.0.0.1 for SSH-tunnel maintenance access only.

yaml
api:
  expose: ["3000"]       # was ports: ["3000:3000"]

kick-server:
  expose: ["3000"]       # was ports: ["3001:3000"]

db:
  ports:
    - "127.0.0.1:5432:5432"   # loopback only — SSH tunnel for maintenance

admin-api:               # new service
  expose: ["3002"]

cloudflared:             # new service
  image: cloudflare/cloudflared:latest
  restart: unless-stopped
  command: tunnel --no-autoupdate run
  environment:
    TUNNEL_TOKEN: ${CF_TUNNEL_TOKEN}

2. Tunnel ingress (CF dashboard or config.yml)

yaml
ingress:
  - hostname: api.verifluence.io
    service: http://api:3000
  - hostname: admin-api.verifluence.io
    service: http://admin-api:3002
  - hostname: kick.verifluence.io
    service: http://kick-server:3000
  - service: http_status:404

3. New admin-server.ts entry point

A second Hono server sharing the same codebase. Moves these routes out of server.ts:

  • POST /api/auth/impersonate — create one-time impersonation token
  • GET /api/streams + GET /api/streams/:id/probes — admin stream inspection
  • Admin-write paths on /api/operators and /api/invitations

Replaces X-Admin-Secret with CF Access JWT validation:

ts
// CF Access injects this header on every proxied request.
// Verified against the CF Access public key — no shared secret needed.
const jwt = request.headers.get("Cf-Access-Jwt-Assertion");
const { payload } = await verifyCloudflareAccessJwt(jwt, env.CF_ACCESS_TEAM_DOMAIN);
if (!payload?.email?.endsWith("@verifluence.io")) return errorResponse("Forbidden", 403);

4. Dockerfile — new admin-api target

dockerfile
FROM base AS admin-api
COPY migrations/ ./migrations/
CMD ["node", "dist/admin-server.js"]

5. CF Zero Trust setup (one-time, dashboard)

  1. Tunnel — create a named tunnel → copy TUNNEL_TOKEN → add to VPS .env
  2. DNS — CF dashboard creates CNAME records pointing each hostname at the tunnel automatically
  3. Access Application on admin-api.verifluence.io
    • Policy: Allow — email ends in @verifluence.io (Google Identity)
    • CF injects Cf-Access-Jwt-Assertion on every request that passes the gate
  4. Service token (optional) — for CI/CD calls that can't do browser SSO

6. Admin panel

VITE_API_URL        → VITE_ADMIN_API_URL=https://admin-api.verifluence.io
VITE_ADMIN_SECRET   → (removed entirely)

Reliability: cloudflared is not a SPOF

By default cloudflared opens 4 parallel connections to different CF PoPs. A single connection drop is invisible to clients — CF retries on the remaining connections. The only hard failure is the cloudflared process itself, which Docker restarts instantly (restart: unless-stopped).

For additional resilience, run two cloudflared containers pointing at the same tunnel token:

yaml
cloudflared-1:
  image: cloudflare/cloudflared:latest
  restart: unless-stopped
  command: tunnel --no-autoupdate run
  environment:
    TUNNEL_TOKEN: ${CF_TUNNEL_TOKEN}

cloudflared-2:
  image: cloudflare/cloudflared:latest
  restart: unless-stopped
  command: tunnel --no-autoupdate run
  environment:
    TUNNEL_TOKEN: ${CF_TUNNEL_TOKEN}

Both connect independently to the same tunnel. CF load-balances across all 8 connections (4 per daemon). A full daemon crash causes at most a few seconds of elevated error rate while Docker restarts it.


Security comparison

BeforeAfter
VPS inbound ports3000, 3001 publicnone
IP bypass possible✅ yesno
Admin secret exposureShared secret in JS bundleCF Access JWT — no secret
Admin auth boundaryX-Admin-Secret header checkCF Zero Trust (Google SSO)
Process isolationSingle process for all routesSeparate process for admin
DB reachable from internet✅ yes (port 5432 published)no (loopback only)
WAF bypass possible✅ via direct IPno

Environment variables

VariableServicePurpose
CF_TUNNEL_TOKENVPS .envAuthenticates the cloudflared daemon to CF
CF_ACCESS_TEAM_DOMAINadmin-api containere.g. verifluence.cloudflareaccess.com — fetches Access public key for JWT verification
ADMIN_SECRET(removed)Replaced by CF Access JWT
VITE_ADMIN_SECRET(removed)Replaced by CF Access JWT

Effort estimate

TaskEstimate
docker-compose.yml — remove ports, add admin-api + cloudflared30 min
CF Tunnel + DNS + Access policy (dashboard)30 min
admin-server.ts — extract admin routes + CF Access JWT verification2 h
Dockerfile admin-api target30 min
Admin panel — VITE_ADMIN_API_URL, remove secret30 min
CI/CD — add admin-api image build + deploy1 h
Total~5 h

Verifluence Documentation