Files
openclaw/docs/gateway/cloudflare-access.md
T
Peter Steinberger 29f86119e0 docs(gateway): recommend Access service tokens over route exemption (#126536)
* docs(gateway): recommend Access service tokens over route exemption

* docs(gateway): fix Nodes heading link
2026-08-19 21:40:44 -07:00

8.5 KiB

summary, read_when, title
summary read_when title
Publish a loopback Gateway through a Cloudflare Tunnel and authenticate every client with Cloudflare Access
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
Cloudflare Tunnel and Access

Run the Gateway on loopback, publish it through a Cloudflare Tunnel, and let Cloudflare Access authenticate every request before it reaches OpenClaw. The Gateway keeps gateway.bind: "loopback", so no port is exposed and no inbound firewall rule is needed; cloudflared dials out from the host.

This is one supported remote-access topology alongside Tailscale and an SSH tunnel. Choose it when you want 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, and Cloudflare Zero Trust enabled.
  • cloudflared installed on the Gateway host, and on any machine that will use the CLI.
  • A running Gateway on 127.0.0.1:18789 with gateway.bind: "loopback".
  • Familiarity with trusted-proxy auth, which this topology uses.

How the pieces fit

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

Access authenticates the request and injects identity headers. The Gateway does not re-authenticate the person or verify the Access JWT signature; it checks the trusted proxy source and configured header presence, then trusts the user header. Because allowLoopback also lets other local processes present those headers, keep the Gateway port private to the host and run only trusted workloads there.

Step 1: Route the tunnel to loopback

Add an ingress rule mapping your hostname to the Gateway port, then run 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

See Cloudflare's own documentation for creating the tunnel and DNS record.

Step 2: Protect the hostname with Access

Create an Access application for gateway.example with a policy that allows your users. Note the two headers Access adds to authenticated requests, because the Gateway consumes them in the next step:

  • cf-access-authenticated-user-email — the authenticated identity.
  • cf-access-jwt-assertion — Access's signed assertion. OpenClaw checks only that this header is present and non-blank; it does not verify the JWT signature.

Step 3: Trust those headers in the Gateway

Set gateway.auth.mode to trusted-proxy and name the Access headers. allowLoopback is required here: cloudflared connects from 127.0.0.1, and trusted-proxy auth otherwise expects a non-loopback proxy.

{
  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,
      },
    },
  },
}

Requiring cf-access-jwt-assertion adds a second presence check, not cryptographic verification. A local process that can connect to the Gateway can submit both headers, so do not treat this setting as a defense against untrusted local code. The security boundary is the locked-down loopback port plus Cloudflare Access and the tunnel being the only path for external traffic.

Step 4: Decide how nodes and workers get in

Access protects every route on the hostname, including the ones nodes use. A node can authenticate to Access on every leg it needs — the join request, the main Gateway WebSocket, the worker socket, and worker transfers — so the recommended path 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 persists these as env SecretRefs under gateway.cloudflareAccess.clientId / clientSecret; see Node CLI. The only cost is that the node needs those two values before the join command, so a join link is no longer paste-and-go on its own.

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 answers failures with an opaque 404; worker admission carries its own expiring credential. This keeps join links paste-and-go, at the cost of making those two routes publicly reachable. Prefer the service token unless you need that onboarding flow. See Nodes.

If you do neither, openclaw connect fails against the tunnel even though the browser works, because the join request is 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 do not carry 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 choice made in step 4.

Verify

openclaw tui

Expect the TUI to reach wss://gateway.example and show connected. A first connection may report 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 the 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 believe.
  • 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

Symptom Cause and fix
gateway rejected websocket upgrade (HTTP 302) from the CLI or TUI Access intercepted the upgrade. Configure gateway.remote.edgeAuth; see Remote access.
Browser works, openclaw connect fails Node routes are still behind Access. Apply one of the options in step 4.
Exec provider ... exited with code 1 The exec secret provider runs with a scrubbed environment; cloudflared needs passEnv: ["HOME"] to read its cached token.
secrets.providers.*.command must not be a symlink Point command at the resolved binary, not a package-manager symlink.
Gateway starts but every request is anonymous allowLoopback is unset, so headers from the local cloudflared are ignored.