> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/openshell/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/openshell/_mcp/server.

# Gateway Authentication

> Gateway resolution, authentication modes, connection flow, OIDC support, and credential file layout.

This page describes how the CLI resolves a gateway, authenticates with it, and where credentials are stored. For how to deploy or register gateways, refer to [Gateways](/sandboxes/manage-gateways).

## Gateway Resolution

When any CLI command needs to talk to the gateway, it resolves the target through a priority chain:

1. `--gateway-endpoint <URL>` flag (direct URL).
2. `-g <NAME>` flag.
3. `OPENSHELL_GATEWAY` environment variable.
4. Active gateway from `~/.config/openshell/active_gateway`, falling back to `/etc/openshell/active_gateway` when no user selection exists.

The CLI loads gateway metadata from `~/.config/openshell/gateways/<name>/metadata.json` first and falls back to `/etc/openshell/gateways/<name>/metadata.json` when no user entry exists.

## Authentication Status

`openshell status` reports gateway reachability and authentication separately. The public health RPC determines whether the gateway is connected and supplies its version. The CLI then calls the existing protected gateway-info capability query to verify the configured credentials. An authorization denial still confirms that the gateway authenticated the caller.

For example, an expired bearer token can produce `Status: Connected` and `Authentication: Failed`. This means the gateway is healthy but protected commands cannot use the current credentials. Run `openshell gateway login <name>` to re-authenticate. When a gateway predates the gateway-info capability query, the CLI reports `Authentication: Unverified` rather than treating a successful public health check as proof of authentication.

## Authentication Modes

The CLI uses one of these authentication modes depending on the gateway's configuration.

### mTLS

The default mode for local Docker, Podman, and VM gateways without OIDC. The CLI presents a client certificate during the TLS handshake, and the gateway can map the verified certificate subject to a local user principal when mTLS user authentication is enabled.

mTLS user authentication is for local single-user gateways. Kubernetes deployments must use OIDC or a trusted access proxy for user authentication; the Helm chart does not render `mtls_auth`.

Set these environment variables before starting the gateway:

| Environment variable         | Purpose                                                                                                                                                 |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `OPENSHELL_TLS_CERT`         | Path to the gateway server certificate.                                                                                                                 |
| `OPENSHELL_TLS_KEY`          | Path to the gateway server private key.                                                                                                                 |
| `OPENSHELL_TLS_CLIENT_CA`    | Path to the CA certificate that verifies CLI client certificates.                                                                                       |
| `OPENSHELL_ENABLE_MTLS_AUTH` | Set to `true` to authenticate CLI callers from verified client certificates. Defaults on for local Docker, Podman, and VM gateways with no OIDC issuer. |

For local access, the server certificate must be valid for the endpoint the CLI uses. Include `localhost`, `127.0.0.1`, and `::1` in the certificate SANs when users connect to a local gateway through loopback.

Package-managed local gateways generate this bundle automatically for the `openshell` gateway name. Homebrew registers `https://localhost:17670`; Debian and RPM use `https://127.0.0.1:17670`.
When you register a package-managed local gateway with `openshell gateway add <endpoint> --local --name openshell`, the CLI refreshes its mTLS bundle from the package-managed TLS directory.
On Homebrew, the gateway service also mirrors the Docker sandbox client bundle into `$HOME/.local/state/openshell/homebrew/tls` before startup so Docker Desktop can bind-mount the files into sandbox containers.

The CLI loads its mTLS bundle from `~/.config/openshell/gateways/<name>/mtls/`:

| File      | Purpose                                                         |
| --------- | --------------------------------------------------------------- |
| `ca.crt`  | CA certificate that verifies the gateway server certificate.    |
| `tls.crt` | Client certificate. It must chain to `OPENSHELL_TLS_CLIENT_CA`. |
| `tls.key` | Client private key for `tls.crt`.                               |

The connection flow:

1. The CLI loads the certificate files.
2. Opens a TCP connection to the gateway endpoint.
3. Performs a TLS handshake, presenting the client certificate.
4. The gateway verifies the client certificate against its CA.
5. When mTLS user authentication is enabled, the gateway maps the verified certificate subject to a user principal.
6. The gateway authorizes the gRPC method.

### OIDC

Gateways can validate OpenID Connect access tokens on gRPC requests. Configure OIDC when you want users, operators, or automation to authenticate with an identity provider such as Keycloak, Entra ID, or Okta.

OIDC is application-layer authentication. TLS still controls the transport. If TLS client certificates remain required, the CLI must also have an mTLS bundle for the gateway.

Configure the gateway with an issuer and audience:

```shell
openshell-gateway \
  --oidc-issuer https://idp.example.com/realms/openshell \
  --oidc-audience openshell-cli \
  --oidc-roles-claim realm_access.roles \
  --oidc-admin-role openshell-admin \
  --oidc-user-role openshell-user
```

The same settings are available through environment variables:

| Environment variable          | Purpose                                                                                                | Default              |
| ----------------------------- | ------------------------------------------------------------------------------------------------------ | -------------------- |
| `OPENSHELL_OIDC_ISSUER`       | OIDC issuer URL. The gateway discovers `/.well-known/openid-configuration` from this URL.              | None                 |
| `OPENSHELL_OIDC_AUDIENCE`     | Expected JWT `aud` claim.                                                                              | `openshell-cli`      |
| `OPENSHELL_OIDC_JWKS_TTL`     | JWKS cache TTL in seconds. The gateway also refreshes on an unknown key ID. Must be greater than zero. | `3600`               |
| `OPENSHELL_OIDC_ROLES_CLAIM`  | Dot-separated claim path containing roles.                                                             | `realm_access.roles` |
| `OPENSHELL_OIDC_ADMIN_ROLE`   | Role required for admin operations.                                                                    | `openshell-admin`    |
| `OPENSHELL_OIDC_USER_ROLE`    | Role required for standard user operations.                                                            | `openshell-user`     |
| `OPENSHELL_OIDC_SCOPES_CLAIM` | Dot-separated claim path containing scopes. Empty disables scope enforcement.                          | Empty                |

For Helm deployments, set the same values under `server.oidc`:

```yaml
server:
  oidc:
    issuer: https://idp.example.com/realms/openshell
    audience: openshell-cli
    rolesClaim: realm_access.roles
    adminRole: openshell-admin
    userRole: openshell-user
    scopesClaim: ""
```

Register an OIDC gateway with the CLI:

```shell
openshell gateway add https://gateway.example.com \
  --name production \
  --oidc-issuer https://idp.example.com/realms/openshell \
  --oidc-client-id openshell-cli \
  --oidc-audience openshell-cli
```

When you register or log in to an OIDC gateway, the CLI uses the Authorization Code flow with PKCE. It opens a browser, receives the authorization code on a localhost callback, exchanges the code for tokens, and stores the token bundle under the gateway credential directory. After `openshell gateway logout`, the next browser login asks the identity provider for a fresh login prompt so you can choose a different browser user instead of silently reusing the previous session. If `OPENSHELL_OIDC_CLIENT_SECRET` is set, the CLI uses the client credentials flow instead. Use that mode for CI and other non-interactive automation.

Official Python, TypeScript, and Go SDKs can perform renewable client-credentials
authentication directly. Configure the service account at the identity provider
with the audience, roles, scopes, and workspace membership required by the
gateway. The SDKs discover the token endpoint, attach the bearer token to each
RPC, and repeat the grant before expiry. They keep the client secret and access
token in memory and do not update the CLI's `oidc_token.json`. SDK clients
require TLS when sending these credentials to a non-loopback gateway.

#### Python

```python
from openshell import ClientCredentialsAuth, SandboxClient

auth = ClientCredentialsAuth(
    client_secret=lambda: load_secret(),
    # Omit issuer/client_id/scopes/audience to use active gateway metadata.
)
client = SandboxClient.from_active_cluster(client_credentials=auth)
```

#### TypeScript

```ts
import { clientCredentials, OpenShellClient } from '@nvidia/openshell-sdk'

const client = await OpenShellClient.connect({
  gateway: 'https://gateway.example.com',
  oidcTokenProvider: clientCredentials({
    issuer: 'https://idp.example.com/realms/openshell',
    clientId: 'openshell-service',
    clientSecret: () => loadSecret(),
    audience: 'openshell-gateway',
    scopes: ['sandbox:read', 'sandbox:write'],
  }),
})
```

#### Go

```go
auth, err := oidc.NewClientCredentialsAuth(
    oidc.WithGateway("production"),
    oidc.WithClientSecretProvider(loadSecret),
)
client, err := v1.NewClient(v1.Config{Address: address, Auth: auth, TLS: tlsConfig})
```

The connection flow:

For a headless environment, set `OPENSHELL_NO_BROWSER=1` before registering or logging in to the gateway. When this variable is set and `OPENSHELL_OIDC_CLIENT_SECRET` is not configured, the CLI uses the Device Authorization Grant (RFC 8628) with S256 PKCE. This flow prompts the user to visit a verification URL on any device with a browser and enter a displayed code. The CLI polls the token endpoint until the user completes authorization. This requires the OIDC client to have the device authorization grant enabled on the identity provider.

1. The CLI loads the stored OIDC token bundle.
2. If the access token is expired or near expiry, the CLI refreshes it with the OIDC scopes saved in the gateway metadata. If refresh fails, the command stops before sending a protected request and directs you to run `openshell gateway login <name>`.
3. The CLI connects to the gateway and attaches `authorization: Bearer <token>` metadata to each gRPC request.
4. The gateway validates the JWT signature, issuer, audience, expiration, subject, and key ID against the issuer's JWKS.
5. The gateway extracts roles and optional scopes from the configured claim paths.
6. The gateway authorizes the gRPC method. Platform-scoped methods require the configured admin role. Workspace-scoped methods require the configured user role and a sufficient membership in the target workspace. Admin role holders satisfy user-role checks and bypass workspace membership checks.

For the Platform Admin, Workspace Admin, and Workspace User permissions, refer
to [Manage Workspaces and Access](/sandboxes/manage-workspaces).

#### JWT validation

The gateway validates OIDC access tokens against the issuer's JWKS endpoint. It derives the signing algorithm from each JWK's `kty` and `crv` fields, not from the JWT header:

| JWK `kty` | `crv`   | Algorithm                                   |
| --------- | ------- | ------------------------------------------- |
| RSA       | —       | RS256, RS384, RS512, PS256, PS384, or PS512 |
| EC        | P-256   | ES256                                       |
| EC        | P-384   | ES384                                       |
| OKP       | Ed25519 | EdDSA (Ed25519)                             |

For EC and OKP keys, `crv` fully determines the algorithm. For RSA keys, the algorithm is selected from the JWK's declared `alg` field when it names one of the six RSA algorithms above; if `alg` is absent (as with Microsoft Entra ID, which omits it entirely), the gateway defaults to RS256. If `alg` names a non-RSA algorithm, the key is treated as contradictory and skipped (see below) rather than falling back to RS256.

The gateway rejects tokens when the JWT header `alg` does not match the algorithm pinned from the JWK. Tokens must include `iss`, `aud`, `exp`, and `sub` claims. The `iss` and `aud` values must match the configured issuer and audience.

JWKs with `use: enc`, an `alg` that genuinely contradicts the derived algorithm (for example an RSA key declaring `ES256`), or `key_ops` that excludes `verify` are skipped. The gateway refreshes the JWKS cache when it encounters an unknown key ID, throttled to at most once per second to bound how much an unknown-`kid` lookup can amplify requests against the issuer. If a refresh yields zero usable keys, the gateway keeps serving the previous key set rather than dropping all signing keys and failing every request, but only for up to three times the configured TTL. If the JWKS is still empty after that grace period, the cached keys are evicted and all tokens are rejected until the issuer recovers.

Common identity providers such as Keycloak (RS256), Microsoft Entra ID (RSA), and Okta (often ES256) publish compatible signing keys.

If `OPENSHELL_OIDC_SCOPES_CLAIM` is set, the gateway also enforces scopes. It accepts space-delimited scope strings such as `scope: "openid sandbox:read"` and JSON arrays such as `scp: ["sandbox:read"]`. Standard OIDC scopes such as `openid`, `profile`, `email`, and `offline_access` are ignored for authorization. `openshell:all` grants access to all scoped methods.

Supervisor-to-gateway RPCs do not use user OIDC tokens or mTLS user identity. Each sandbox supervisor presents a gateway-minted `Authorization: Bearer` token scoped to its sandbox ID. On Kubernetes, the Kubernetes compute driver validates the projected ServiceAccount token with TokenReview, verifies the live pod UID and controlling `Sandbox` ownerReference, and returns the authenticated sandbox ID to the gateway. The gateway verifies that sandbox still exists before minting its JWT. Log upload, policy status, provider environment lookup, and sandbox config sync run with sandbox-restricted scope, while CLI users authenticate with OIDC, edge auth, local mTLS user authentication, or an explicitly enabled unauthenticated local developer mode. Provider environment responses expose only the credentials and configuration attached to that sandbox, subject to endpoint binding and credential expiry checks.

Re-authenticate an OIDC gateway with:

```shell
openshell gateway login production
```

Inspect the identity the gateway validated:

```shell
openshell whoami
openshell whoami --output json
```

The output includes the stable subject used for workspace membership, the
display name when available, identity provider, roles, and scopes. The gateway
returns its validated identity; the CLI does not infer these values from an
unverified local token payload. Use the `subject` value when adding the user to
a workspace. For membership commands, refer to
[Manage Workspaces and Access](/sandboxes/manage-workspaces).

### Edge JWT (cloud gateways)

For gateways behind a reverse proxy that handles authentication (e.g. Cloudflare Access), the CLI uses a browser-based login flow and routes traffic through a WebSocket tunnel.

**Registration flow** (`openshell gateway add https://gateway.example.com`):

1. The CLI stores gateway metadata with the edge authentication mode.
2. Opens your browser to the gateway's authentication endpoint.
3. The reverse proxy handles login (SSO, identity provider, etc.).
4. After authentication, the browser relays the authorization token back to the CLI through a localhost callback.
5. The CLI stores the token and sets the gateway as active.

**Connection flow** (subsequent commands):

1. The CLI starts a local proxy that listens on an ephemeral port.
2. The proxy opens a WebSocket connection (`wss://`) to the gateway, attaching the stored bearer token in the upgrade headers.
3. The reverse proxy authenticates the WebSocket upgrade request.
4. The gateway bridges the WebSocket into the same service that handles direct mTLS connections.
5. CLI commands send requests through the local proxy as plaintext HTTP/2 over the tunnel.

This is transparent to the user. All CLI commands work the same regardless of whether the gateway uses mTLS, OIDC, or edge authentication.

**Re-authentication**: If the token expires, run `openshell gateway login` to open the browser flow again and update the stored token.

### Plaintext

When a gateway is deployed with `server.disableTls=true`, TLS is disabled entirely. The CLI connects over plain HTTP/2. This mode is intended for local port-forwarding or gateways behind a trusted reverse proxy or tunnel that handles TLS termination externally.

Register a plaintext gateway with an explicit `http://` endpoint:

```shell
openshell gateway add http://127.0.0.1:17670 --local
```

For Kubernetes local development, the Helm Skaffold overlay enables `[openshell.gateway.auth] allow_unauthenticated_users = true` so a port-forwarded plaintext gateway works without OIDC or mTLS user credentials. Leave this disabled for shared and production clusters.

This stores the gateway with `auth_mode = plaintext`, skips mTLS client certificate lookup, and does not open the browser login flow.

## File Layout

User-managed gateway credentials and metadata are stored under `~/.config/openshell/`:

```text
openshell/
  active_gateway                    # Plain text: active gateway name
  gateways/
    <name>/
      metadata.json                 # Gateway metadata (endpoint, auth mode, type)
      mtls/                         # mTLS bundle (local and remote gateways)
        ca.crt                      # CA certificate
        tls.crt                     # Client certificate
        tls.key                     # Client private key
      edge_token                    # Edge auth JWT (cloud gateways)
      oidc_token.json               # OIDC access token, refresh token, and expiry metadata
      last_sandbox                  # Last-used sandbox for this gateway
```

Installers can also seed read-only defaults under `/etc/openshell/` (or a non-empty absolute `OPENSHELL_SYSTEM_GATEWAY_DIR` override) using the same active-gateway and metadata layout:

```text
/etc/openshell/
  active_gateway
  gateways/
    <name>/
      metadata.json
```

Only `active_gateway` and `metadata.json` fall back to the system layout. mTLS bundles, OIDC tokens, edge tokens, and `last_sandbox` remain per-user state.

For OIDC gateways, `metadata.json` also stores the issuer, CLI client ID, optional audience, and requested scopes. Treat `oidc_token.json` as a credential. OpenShell writes it with owner-only file permissions.