Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions docs/kubernetes/access-control.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,131 @@ Both `adminRole` and `userRole` must be set, or both must be empty. Setting only
| Microsoft Entra ID | `roles` |
| Okta | `groups` |

### Keycloak setup

Keycloak public clients do not include `sub`, `aud`, or realm roles in access tokens by default. Without these claims, the gateway rejects tokens with errors like `missing field 'sub'`, audience mismatch, or `role 'openshell-user' required`.

After creating a realm and a public client (with PKCE S256, redirect URIs `http://localhost:*` and `http://127.0.0.1:*`), add these protocol mappers to the client:

| Mapper name | Mapper type | Key config |
|---|---|---|
| `sub` | Subject (sub) | access.token.claim: true |
| `openshell-audience` | Audience | included.client.audience: `openshell-cli` |
| `realm-roles` | User Realm Role | claim.name: `realm_access.roles`, multivalued: true |

Add these via the Keycloak admin console under **Clients → openshell-cli → Client scopes → Dedicated scope → Add mapper**, or via the CLI:

<Tabs>
<Tab title="kcadm.sh">

```shell
KC_ADM="kcadm.sh --config /tmp/kcadm.config"

$KC_ADM config credentials \
--server http://localhost:8080 \
--realm master \
--user <admin-user> \
--password <admin-password>

$KC_ADM create realms \
-s realm=openshell \
-s enabled=true

$KC_ADM create clients -r openshell \
-s clientId=openshell-cli \
-s enabled=true \
-s publicClient=true \
-s directAccessGrantsEnabled=true \
-s standardFlowEnabled=true \
-s 'redirectUris=["http://localhost:*","http://127.0.0.1:*"]' \
-s 'webOrigins=["http://localhost","http://127.0.0.1"]' \
-s 'attributes={"pkce.code.challenge.method":"S256"}'

CLIENT_UUID=$($KC_ADM get clients -r openshell \
-q clientId=openshell-cli --fields id --format csv --noquotes)

$KC_ADM create clients/$CLIENT_UUID/protocol-mappers/models -r openshell \
-s name=sub \
-s protocol=openid-connect \
-s protocolMapper=oidc-sub-mapper \
-s 'config={"access.token.claim":"true","id.token.claim":"true"}'

$KC_ADM create clients/$CLIENT_UUID/protocol-mappers/models -r openshell \
-s name=openshell-audience \
-s protocol=openid-connect \
-s protocolMapper=oidc-audience-mapper \
-s 'config={"included.client.audience":"openshell-cli","access.token.claim":"true","id.token.claim":"true"}'

$KC_ADM create clients/$CLIENT_UUID/protocol-mappers/models -r openshell \
-s name=realm-roles \
-s protocol=openid-connect \
-s protocolMapper=oidc-usermodel-realm-role-mapper \
-s 'config={"claim.name":"realm_access.roles","jsonType.label":"String","multivalued":"true","access.token.claim":"true","id.token.claim":"true","userinfo.token.claim":"true"}'

$KC_ADM create roles -r openshell -s name=openshell-user
$KC_ADM create roles -r openshell -s name=openshell-admin
```

</Tab>
<Tab title="REST API">

```shell
KC_URL=https://keycloak.example.com

TOKEN=$(curl -sk "${KC_URL}/realms/master/protocol/openid-connect/token" \
-d "client_id=admin-cli" \
-d "username=<admin-user>" \
-d "password=<admin-password>" \
-d "grant_type=password" | jq -r .access_token)

curl -sk -X POST "${KC_URL}/admin/realms" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"realm":"openshell","enabled":true}'

curl -sk -X POST "${KC_URL}/admin/realms/openshell/clients" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"clientId": "openshell-cli",
"enabled": true,
"publicClient": true,
"directAccessGrantsEnabled": true,
"standardFlowEnabled": true,
"redirectUris": ["http://localhost:*", "http://127.0.0.1:*"],
"webOrigins": ["http://localhost", "http://127.0.0.1"],
"attributes": {"pkce.code.challenge.method": "S256"}
}'

CLIENT_UUID=$(curl -sk "${KC_URL}/admin/realms/openshell/clients?clientId=openshell-cli" \
-H "Authorization: Bearer $TOKEN" | jq -r '.[0].id')

for MAPPER in \
'{"name":"sub","protocol":"openid-connect","protocolMapper":"oidc-sub-mapper","config":{"access.token.claim":"true","id.token.claim":"true"}}' \
'{"name":"openshell-audience","protocol":"openid-connect","protocolMapper":"oidc-audience-mapper","config":{"included.client.audience":"openshell-cli","access.token.claim":"true","id.token.claim":"true"}}' \
'{"name":"realm-roles","protocol":"openid-connect","protocolMapper":"oidc-usermodel-realm-role-mapper","config":{"claim.name":"realm_access.roles","jsonType.label":"String","multivalued":"true","access.token.claim":"true","id.token.claim":"true","userinfo.token.claim":"true"}}'; do
curl -sk -X POST "${KC_URL}/admin/realms/openshell/clients/${CLIENT_UUID}/protocol-mappers/models" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d "$MAPPER"
done

curl -sk -X POST "${KC_URL}/admin/realms/openshell/roles" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"openshell-user"}'

curl -sk -X POST "${KC_URL}/admin/realms/openshell/roles" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"openshell-admin"}'
```

</Tab>
</Tabs>

Assign `openshell-user` to users who need sandbox access and `openshell-admin` to administrators.

<Note>
On Kubernetes, run `kcadm.sh` via `kubectl exec` into the Keycloak pod. The `--config /tmp/kcadm.config` flag is required when the container runs as non-root. On OpenShift, see [OIDC on OpenShift](/kubernetes/openshift/oidc-keycloak) for the `oc exec` variant and OpenShift-specific details.
</Note>

## Reverse-Proxy Auth Termination

When an access proxy, such as Cloudflare Access, ngrok, or a corporate SSO gateway, handles authentication in front of the OpenShell gateway, you can explicitly allow unauthenticated user calls at the gateway:
Expand Down
95 changes: 0 additions & 95 deletions docs/kubernetes/openshift.mdx

This file was deleted.

169 changes: 169 additions & 0 deletions docs/kubernetes/openshift/gateway-connection.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
title: "Gateway Connection"
sidebar-title: "Gateway Connection"
slug: "kubernetes/openshift/gateway-connection"
description: "Connect the OpenShell CLI to a gateway running on OpenShift using port forwarding, a reencrypt Route, or the Kubernetes Gateway API."
keywords: "Generative AI, Cybersecurity, Kubernetes, OpenShift, Route, Gateway API, Istio, gRPC, Ingress, CLI"
position: 2
---

After the gateway is installed, register it with the CLI so you can create and manage sandboxes. The gateway is only reachable inside the cluster by default. How you connect depends on whether you need local-only access or external access for your team.

| Method | Use case |
|---|---|
| [Port forward](#local-access) | Local evaluation from your workstation |
| [Route](#remote-access-with-routes) | Shared or production access using OpenShift-native routing |
| [Gateway API](#remote-access-with-gateway-api) | Shared or production access using the standard Kubernetes Gateway API |

## Local access

For quick evaluation, forward the gateway port to your workstation:

```shell
oc -n openshell port-forward svc/openshell 8080:8080
```

Register the gateway with the CLI:

```shell
openshell gateway add http://127.0.0.1:8080 --local --name openshift
openshell status
```

## Remote access

In shared and production environments, the gateway is exposed externally so that team members and CI systems can connect from outside the cluster. On OpenShift, there are two options: native Routes or the Kubernetes Gateway API.

The gateway multiplexes gRPC and HTTP on a single port. Both options must preserve HTTP/2 for gRPC to work.

### Remote access with Routes

OpenShift Routes are the simplest way to expose the gateway externally. However, not all route types support gRPC:

| Route type | gRPC support | Issue |
|---|---|---|
| Edge | Broken | Terminates TLS and forces HTTP/1.1 to the backend, which drops gRPC frames. |
| Passthrough | Broken | Preserves HTTP/2 but exposes the gateway's mTLS client certificate requirement to browsers, causing `ERR_BAD_SSL_CLIENT_AUTH_CERT`. |
| **Reencrypt** | **Works** | Terminates external TLS at the router, re-establishes TLS to the backend, and supports HTTP/2 with the `backend-protocol=h2` annotation. |

Create a reencrypt route using the gateway's CA certificate for the backend TLS connection:

```shell
DEST_CA=$(oc get secret openshell-server-tls -n openshell \
-o jsonpath='{.data.ca\.crt}' | base64 -d)

oc create route reencrypt openshell \
--service=openshell --port=8080 \
--dest-ca-cert=<(echo "$DEST_CA") \
-n openshell

oc annotate route openshell -n openshell \
haproxy.router.openshift.io/backend-protocol=h2 --overwrite
```

Register the gateway with the CLI using the route hostname:

```shell
oc get route openshell -n openshell -o jsonpath='{.spec.host}'
openshell gateway add https://<route-hostname> --gateway-insecure --name openshift
```

### Remote access with Gateway API

The Kubernetes [Gateway API](https://gateway-api.sigs.k8s.io) provides native gRPC routing via `GRPCRoute` resources, without needing HTTP/2 annotations.

On OpenShift, the Ingress Operator manages Gateway API CRDs. The standard [Envoy Gateway](/kubernetes/ingress) cannot be installed because the Ingress Operator rejects third-party CRD installations. Instead, use the Istio-based Gateway controller that OpenShift provides.

<Steps>

## Identify the GatewayClass

```shell
oc get gatewayclass
```

Look for a class with an Istio-based controller (e.g. `istio` or `data-science-gateway-class`). The class must show `ACCEPTED: True`.

## Create the Gateway and GRPCRoute

```shell
kubectl apply -f - <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: openshell-gateway
namespace: openshell
spec:
gatewayClassName: istio
listeners:
- name: grpc
protocol: HTTPS
port: 443
tls:
mode: Terminate
certificateRefs:
- name: openshell-server-tls
kind: Secret
allowedRoutes:
namespaces:
from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
name: openshell
namespace: openshell
spec:
parentRefs:
- name: openshell-gateway
namespace: openshell
rules:
- backendRefs:
- name: openshell
port: 8080
EOF
```

## Configure TLS to the backend

The Istio envoy proxy terminates external TLS at the Gateway, then connects to the backend in plaintext by default. Since the OpenShell gateway expects TLS, create a `DestinationRule` for TLS origination:

```shell
kubectl apply -f - <<'EOF'
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: openshell-tls-origination
namespace: openshell
spec:
host: openshell.openshell.svc.cluster.local
trafficPolicy:
tls:
mode: SIMPLE
insecureSkipVerify: true
EOF
```

## Get the external address

The Gateway controller creates a LoadBalancer service. Wait for the external address:

```shell
oc get gateway openshell-gateway -n openshell \
-o jsonpath='{.status.addresses[0].value}'
```


## Register the gateway

```shell
openshell gateway add https://<external-address> --gateway-insecure --name openshift
```

</Steps>

## Next steps

- [OIDC with Keycloak](/kubernetes/openshift/oidc-keycloak) — add `--oidc-issuer` to the `gateway add` command for authenticated access.
Loading
Loading