Skip to content

feat(gateway): validate Cloudflare Access edge JWTs instead of trusting the proxy #4813

Description

@mrunalp

User Story

I run OpenShell gateways behind Cloudflare Access. I need the gateway to verify the edge identity on each request itself, so that it accepts only callers the proxy actually authenticated, instead of trusting the network path.

Problem Statement

When a gateway sits behind Cloudflare Access, the CLI and SDKs send the Access JWT on every request as cf-access-jwt-assertion plus a CF_Authorization cookie. The gateway never validates it. No authenticator handles the edge token, and IdentityProvider::CloudflareAccess is never constructed.

The documented setup ("Reverse-Proxy Auth Termination" in docs/kubernetes/access-control.mdx) therefore runs the gateway with disable_tls = true and allow_unauthenticated_users = true. Every request without OIDC or mTLS credentials becomes the unauthenticated-local-dev principal, which holds openshell-admin, openshell-user and openshell:all.

Impact / Why This Matters

  • Bypassing the proxy gives full admin. Any caller that reaches the gateway without going through Access, such as a misconfigured Service or Ingress, a pod in the cluster, or a leaked tunnel route, gets platform-admin access with no credential.
  • Every caller is the same principal. Behind the edge, all users map to one identity, so workspace membership, RBAC and OCSF audit can't tell them apart.
  • The workaround is network isolation alone, which the docs themselves warn is only safe when "the proxy path is fully trusted".

Proposed Design

Add an opt-in edge-authentication mode. When it is configured, the gateway:

  1. Verifies the edge JWT from the cf-access-jwt-assertion header (or the CF_Authorization cookie) on every request against the configured team issuer and its published signing keys. It checks signature, iss, aud (the Access application AUD tag), exp, and nbf when present.
  2. Turns a valid token into an authenticated user principal. The subject is the token's sub, the display name comes from email, and the provider is recorded as Cloudflare Access. Roles can optionally come from a configurable claim, in the same form as roles_claim for OIDC.
  3. Rejects a missing, invalid or expired edge token with Unauthenticated and emits the existing OCSF authentication-failure event. It does not fall back to the local-dev principal.
  4. Lets edge deployments drop allow_unauthenticated_users = true. Workspace membership and RBAC then apply per user.

Startup fails if edge auth is configured with an issuer that isn't HTTPS or with an empty audience, matching OIDC validation.

Suggested UX (if applicable)

[openshell.gateway.edge_auth]
provider   = "cloudflare_access"
issuer     = "https://example-team.cloudflareaccess.com"
audience   = "4714c1358e65fe4b408ad6d432a5f878f08194bdb4752441fd56faefa9b2b6f2"
# optional
roles_claim = "custom.groups"
helm upgrade openshell oci://ghcr.io/nvidia/openshell/helm-chart \
  --set server.disableTls=true \
  --set 'gatewayConfig.openshell\.gateway\.edge_auth.issuer=https://example-team.cloudflareaccess.com' \
  --set 'gatewayConfig.openshell\.gateway\.edge_auth.audience=<aud-tag>'

Acceptance Criteria

  • With edge auth configured, a request carrying a valid Access JWT succeeds as a user principal whose subject is the token's sub, and that subject is checked against workspace membership.
  • A request with no edge token, a token signed by another key, the wrong aud, or an expired token returns Unauthenticated and emits an OCSF authentication-failure event.
  • Edge deployments work without allow_unauthenticated_users = true.
  • Signing-key rotation is handled the same way as OIDC JWKS: refresh on an unknown kid, rate-limited.
  • docs/kubernetes/access-control.mdx and docs/how-it-works/gateways/configuration.mdx describe the new mode and recommend it over allow_unauthenticated_users.

Alternatives Considered

  • Configure the Access application as an OIDC issuer and use [openshell.gateway.oidc]. The CLI's edge flow delivers the token in Cloudflare's header and cookie, not as Authorization: Bearer, so the OIDC authenticator never sees it. Changing every client is more disruptive than accepting the edge token at the gateway.
  • A gateway interceptor (RFC 0010). Interceptors transform requests after authentication and must not bypass gateway authn/authz, so identity establishment belongs in the authenticator chain.
  • Keep relying on network isolation. That is today's state, and it gives admin rights to any caller that bypasses the proxy.

Agent Investigation

  • The client sends the token in crates/openshell-core/src/auth.rs:39-84 and crates/openshell-cli/src/edge_tunnel.rs:157-169.
  • The authenticator chain is built in crates/openshell-server/src/multiplex.rs:998-1021 (peer, compute driver, sandbox JWT, OIDC); there is no edge authenticator.
  • IdentityProvider::CloudflareAccess (crates/openshell-server/src/auth/identity.rs:40-41) is only referenced in name-mapping match arms.
  • JwksCache in crates/openshell-server/src/auth/oidc.rs already handles issuer and JWKS-origin pinning, key-rotation throttling and the algorithm pin, and could be reused.
  • service_routing.rs:810-837 already strips edge headers before forwarding to sandbox services (RFC 0017).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:gatewayGateway server and control-plane workstate:acceptedA maintainer decided OpenShell should pursue this issuetopic:securitySecurity issues

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions