Cloudflare Tunnel and Access for Remote Gateway

Learn how to publish a loopback Gateway through a Cloudflare Tunnel and authenticate every client with Cloudflare Access. This setup is ideal for teams needing a stable public HTTPS URL and SSO in front of the Control UI.

Read this when

  • You want a public HTTPS Gateway URL without opening a port
  • You want Cloudflare Access (SSO) to authenticate the Control UI
  • Your CLI, TUI, or nodes get HTTP 302 from a Cloudflare-fronted Gateway

Run the Gateway on loopback, expose it through a Cloudflare Tunnel, and have Cloudflare Access authenticate every request before it reaches OpenClaw. The Gateway holds gateway.bind: "loopback", which means no port is open and no inbound firewall rule is required; cloudflared initiates the outbound connection from the host.

This represents one supported remote-access setup, alongside Tailscale and an SSH tunnel. Pick it when you need a stable public HTTPS URL and identity-provider SSO in front of the Control UI.

Before you begin

  • A Cloudflare account with the zone for your hostname, plus Cloudflare Zero Trust enabled.
  • cloudflared set up on the Gateway host and on any machine that will use the CLI.
  • A Gateway running on 127.0.0.1:18789 with gateway.bind: "loopback".
  • Knowledge of trusted-proxy auth, which this topology relies on.

How the pieces fit

browser / CLI / node  ->  Cloudflare Access (identity)  ->  Tunnel  ->  127.0.0.1:18789

Access handles authentication and adds identity headers. The Gateway does not re-verify the user or check the Access JWT signature; it only confirms the trusted proxy source and that the configured headers exist, then accepts the user header. Since allowLoopback allows other local processes to send those headers too, keep the Gateway port restricted to the host and run only trusted workloads there.

Step 1: Route the tunnel to loopback

Set up an ingress rule that maps your hostname to the Gateway port, then start cloudflared as a service on the Gateway host:

tunnel: <tunnel-id>
credentials-file: /root/.cloudflared/<tunnel-id>.json
ingress:
  - hostname: gateway.example
    service: http://localhost:18789
  - service: http_status:404

Refer to Cloudflare's own docs for creating the tunnel and DNS record.

Step 2: Protect the hostname with Access

Create an Access application for gateway.example with a policy permitting your users. Remember the two headers Access attaches to authenticated requests, since the Gateway uses them in the following step:

  • cf-access-authenticated-user-email, the authenticated identity.
  • cf-access-jwt-assertion, Access's signed assertion. OpenClaw only verifies that this header is present and non-empty; it does not validate the JWT signature.

Step 3: Trust those headers in the Gateway

Assign gateway.auth.mode the value trusted-proxy and specify the Access headers. allowLoopback is mandatory in this context: cloudflared connects from 127.0.0.1, and trusted-proxy auth otherwise expects a proxy that is not on loopback.

{
  gateway: {
    bind: "loopback",
    trustedProxies: ["127.0.0.1", "::1"],
    auth: {
      mode: "trusted-proxy",
      trustedProxy: {
        userHeader: "cf-access-authenticated-user-email",
        requiredHeaders: ["cf-access-jwt-assertion"],
        allowLoopback: true,
      },
    },
  },
}

Enforcing cf-access-jwt-assertion adds a second presence check, not cryptographic validation. Any local process with access to the Gateway can supply both headers, so do not view this setting as protection against untrusted local code. The real security boundary is the restricted loopback port combined with Cloudflare Access and the tunnel being the sole route for external traffic.

Step 4: Decide how nodes and workers get in

Access secures every route on the hostname, including those used by nodes. A node can authenticate to Access on each leg it needs: the join request, the main Gateway WebSocket, the worker socket, and worker transfers. The recommended path therefore exposes nothing publicly.

Recommended: give the node an Access service token. Add a Service Auth policy to the application, then on the node host:

export CF_ACCESS_CLIENT_ID="<client-id>"
export CF_ACCESS_CLIENT_SECRET="<client-secret>"
openclaw connect https://gateway.example/j/<code> --service

openclaw connect stores these as env SecretRefs under gateway.cloudflareAccess.clientId / clientSecret; see Node CLI. The only downside is that the node needs those two values before the join command, so a join link is no longer paste-and-go by itself.

Alternative: exempt the self-authenticating routes. Allow /j/* and /__openclaw__/worker without Access identity, keeping WebSocket upgrade enabled on the worker route. Both enforce their own short-lived credentials: a join code is single-use with a TTL, rate-limited per IP, and returns an opaque 404 on failure; worker admission carries its own expiring credential. This preserves paste-and-go join links, but those two routes become publicly reachable. Choose the service token unless you require that onboarding flow. See Nodes.

If you do neither, openclaw connect fails against the tunnel even though the browser works, because the join request gets redirected to the Access login page.

Step 5: Connect each client

Control UI. Open https://gateway.example and sign in through Access. With trusted-proxy auth, the Gateway maps your Access identity to an operator session.

CLI and TUI. These lack browser cookies, so they present an Access token on the WebSocket upgrade. Configure gateway.remote.edgeAuth as described in Remote access, then run cloudflared access login https://gateway.example once to cache a token.

Nodes. Follow the decision made in step 4.

Verify

openclaw tui

Expect the TUI to reach wss://gateway.example and display connected. A first connection may show device pairing required; approve it in the Control UI under Settings → Devices, or run openclaw devices approve --latest on the Gateway host.

Reaching the Gateway's own pairing prompt is itself proof that Access was satisfied; an unauthenticated request never gets that far.

Production readiness

  • Keep gateway.bind: "loopback". Binding wider re-exposes the Gateway beside the tunnel and bypasses Access entirely.
  • Keep trustedProxies limited to loopback. It is the list of addresses whose identity headers the Gateway will trust.
  • trustedProxy.deviceAutoApprove can pair devices automatically for Access-authenticated identities. It removes a manual approval step; enable it only when you accept that anyone who passes Access gets a paired device with the scopes you list.
  • Access tokens expire on the application's session duration. Expect CLI users to re-run cloudflared access login when their token lapses.

Troubleshooting

SymptomCause and fix
gateway rejected websocket upgrade (HTTP 302) from the CLI or TUIThe upgrade was blocked by Access. Set gateway.remote.edgeAuth as described in Remote access.
Works in the browser, but openclaw connect does notNode routes remain behind Access. Pick any option from step 4.
Exec provider ... exited with code 1The exec secret provider operates with a stripped-down environment; cloudflared requires passEnv: ["HOME"] to access its stored token.
secrets.providers.*.command must not be a symlinkDirect command to the actual binary rather than a package-manager symlink.
Gateway starts, yet every request is unauthenticatedSince allowLoopback is not configured, headers coming from the local cloudflared get disregarded.
1,065 words · updated Aug 22, 2026