On this page

On this page

Remote access

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

text
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:

yaml
tunnel: <tunnel-id>credentials-file: /root/.cloudflared/<tunnel-id>.jsoningress:  - 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.

OIDC sign-in and existing people

For OIDC sign-in, configure an Access policy that admits the intended users through that identity provider. For example, use a signed role claim maintained by the provider. A GitHub organization policy applies to the GitHub identity provider; it does not grant access through a separate OIDC provider.

OpenClaw verifies the OIDC identity through Cloudflare Access's identity endpoint and requires its email to match the authenticated user header. It then resolves that email through the existing person profile. Using the same email retains the person's profile and role; a different email requires an existing linked alias to resolve to that person. GitHub sign-in continues to verify the immutable GitHub account ID. Failed identity verification does not fall back to email matching.

Keep the identity provider responsible for verifying email ownership. Creating an OpenClaw person profile does not grant access through Cloudflare Access.

Verified GitHub credit through OIDC

An OIDC provider can supply a verified GitHub account without changing the sign-in email or the account used to publish pull requests. This is optional and disabled until you explicitly trust one Access issuer, identity-provider ID, and claim name:

json5
{  gateway: {    auth: {      mode: "trusted-proxy",      trustedProxy: {        userHeader: "cf-access-authenticated-user-email",        requiredHeaders: ["cf-access-jwt-assertion"],        cloudflareAccessOidc: {          issuer: "https://example.cloudflareaccess.com",          providerId: "your-access-identity-provider-id",          githubAccountIdClaim: "https://openclaw.ai/github-account-id",        },      },    },  },}

The issuer is the Access team origin without a trailing slash. providerId is the selected integration's ID from Access, not its name or an OIDC user subject. The provider must verify ownership of the GitHub account and bind it to the verified sign-in email. Its ID token must contain a canonical positive decimal-string account ID, such as "12345", within JavaScript's safe-integer range. Configure Access to forward that exact custom OIDC claim. OpenClaw reads it from oidc_fields in the authenticated /cdn-cgi/access/get-identity response, or from custom when oidc_fields is absent, then verifies the numeric account through GitHub to obtain its current public login. A present oidc_fields container takes precedence: OpenClaw does not retry custom if the preferred container lacks the claim or its value is invalid. The identity-provider Test preview uses oidc_fields; inspect the authenticated identity endpoint after a fresh sign-in to verify the response OpenClaw consumes.

A missing or malformed optional claim, or an unselected issuer/provider, keeps ordinary email-only resolution. Malformed claims are never sent to GitHub or used to create an association or public credit. If the optional GitHub account lookup fails, OpenClaw resolves the authenticated email through the normal profile and access-policy checks. An existing authorized profile or valid email invitation can still sign in. A GitHub-only access requirement is not satisfied by an email fallback.

Lookup failure does not create a GitHub identity or public credit. An existing verified matching association and saved co-author preference remain intact; a later successful lookup can complete or refresh the link. This behavior applies to both oidc_fields and custom. Invalid Access authentication, mismatched principals, conflicting profile bindings for valid account claims, and expired or revoked access still fail their normal checks.

Existing email profiles retain their identity, role, and saved co-author preference. A conflicting GitHub account does not automatically merge profiles or move the email; an administrator must resolve it through the existing users.linkEmail operation. This also applies to a first-time email claiming an account that already belongs to another profile: link that email explicitly before it can inherit the profile's role. Explicitly linked secondary accounts retain the profile's primary account for public credit. See Gateway profiles and GitHub credit.

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.

json5
{  gateway: {    bind: "loopback",    publicOrigin: "https://gateway.example",    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.

publicOrigin supplies external session links, agent link instructions, and the browser-origin default when controlUi.allowedOrigins is omitted. You only need an explicit allowlist for a different browser policy; that list replaces the default rather than adding to it. For profile, role, GitHub, and operations setup, see Deploy a team server.

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:

bash
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.

If Access expires while a chat is open, the chat connection can remain active while new image and file requests require renewed website access. The Control UI first attempts automatic renewal through a hidden, sandboxed browser navigation. If your global Cloudflare Access session is still valid and your browser permits its cookies, Access can issue a fresh application cookie without another login. OpenClaw verifies access before retrying failed attachments, keeping your conversation and unsent draft open.

If renewal still requires sign-in, the Control UI opens one Sign in to continue loading content dialog. An expired global session, an identity-provider challenge, or blocked third-party cookies can require this manual step. Automatic renewal does not extend the session durations configured in Cloudflare Access. Choose Sign in, finish authentication in the new tab, and return to the conversation. Visible failed attachments retry after access is verified; the original conversation and unsent draft stay open. Check again repeats the access check, and Not now dismisses the prompt without interrupting the chat. Ordinary network failures and missing files do not trigger this dialog.

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

bash
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 to preview the request, then rerun the approval command it prints.

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.
Was this useful?