Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .agents/skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ openshell logs <sandbox-name>
| CLI TLS error | Local mTLS bundle does not match server cert/CA | Check `~/.config/openshell/gateways/<name>/mtls/` |
| Image pull failure | Gateway or sandbox image cannot be pulled | Runtime events and image pull credentials |
| `K8s namespace not ready` with `envoy-gateway-openshell.yaml: the server could not find the requested resource` | Optional Gateway API manifest was applied without Envoy Gateway CRDs, or k3s Helm controller startup exceeded the namespace wait | Apply `deploy/kube/manifests/envoy-gateway-openshell.yaml` manually only after Envoy Gateway is installed and `grpcRoute` is enabled |
| HTTPS ingress (`grpcRoute.gateway.listener.protocol=HTTPS`) connection resets or TLS handshake hangs | Envoy terminates TLS but the gateway pod still expects TLS, so the plaintext backend hop fails | Set `server.disableTls=true` so Envoy forwards plaintext to the pod; verify the listener `certificateRefs` Secret exists in the release namespace and `openshell status` over `https://<host>` |
| HTTPS ingress returns `Unauthenticated` after connecting | TLS terminates at Envoy, so the gateway never sees a client cert; no OIDC issuer is configured for identity | Configure `server.oidc.issuer` and register with `openshell gateway add https://<host> --oidc-issuer <url>`, or set `server.auth.allowUnauthenticatedUsers=true` for a trusted-proxy/dev cluster |

## Reporting

Expand Down
5 changes: 3 additions & 2 deletions deploy/helm/openshell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ add `ci/values-spire.yaml` to the OpenShell release values files.
| grpcRoute.gateway.className | string | `"eg"` | GatewayClass to reference. Envoy Gateway installs one named "eg". |
| grpcRoute.gateway.create | bool | `false` | When true, a Gateway resource is created in the release namespace. Set to false and provide name/namespace to attach to a pre-existing Gateway. |
| grpcRoute.gateway.listener.allowedRoutes | string | `"Same"` | "Same" restricts attached routes to the release namespace; "All" allows any namespace. |
| grpcRoute.gateway.listener.port | int | `80` | Listener port for the generated Gateway resource. |
| grpcRoute.gateway.listener.protocol | string | `"HTTP"` | Listener protocol for the generated Gateway resource. |
| grpcRoute.gateway.listener.port | int | `80` | Listener port for the generated Gateway resource. Use 443 with protocol HTTPS. |
| grpcRoute.gateway.listener.protocol | string | `"HTTP"` | Listener protocol for the generated Gateway resource: HTTP or HTTPS. HTTPS terminates TLS at the Envoy Gateway listener; pair it with server.disableTls=true so Envoy forwards plaintext to the gateway pod, and use OIDC for client identity (the gateway never sees the client cert). |
| grpcRoute.gateway.listener.tls.certificateRefs | list | `[]` | certificateRefs for the HTTPS listener. Required when protocol is HTTPS. Each entry needs a `name` pointing at a kubernetes.io/tls Secret in the Gateway's namespace. May reference a cert-manager-issued Secret or the existing openshell-server-tls Secret (its SANs must include the external hostname). |
| grpcRoute.gateway.name | string | `""` | Name of the Gateway resource. Defaults to the chart fullname. |
| grpcRoute.gateway.namespace | string | `""` | Namespace of the Gateway referenced by the GRPCRoute parentRef. Defaults to the release namespace. |
| grpcRoute.hostnames | list | `[]` | Hostnames the GRPCRoute matches on. Leave empty to match all hosts. |
Expand Down
33 changes: 33 additions & 0 deletions deploy/helm/openshell/ci/values-gateway-tls.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Gateway API overlay with TLS termination at the Envoy Gateway listener.
#
# Exercises the HTTPS listener branch of templates/gateway.yaml for
# `helm template`/lint coverage. Envoy Gateway terminates TLS using the
# referenced kubernetes.io/tls Secret and forwards plaintext to the gateway pod,
# so the gateway runs with TLS disabled and uses OIDC for client identity.
#
# The certificate Secret (openshell-ingress-tls) and the OIDC issuer below are
# placeholders for render coverage; a real deployment must provide a valid TLS
# Secret in the release namespace and a reachable OIDC issuer.

grpcRoute:
enabled: true
gateway:
create: true
className: "eg"
listener:
port: 443
protocol: HTTPS
tls:
certificateRefs:
- name: openshell-ingress-tls
hostnames: []

server:
# Envoy terminates TLS at the edge; the gateway listens plaintext behind it.
disableTls: true
oidc:
issuer: "https://keycloak.example.com/realms/openshell"
audience: "openshell-cli"
14 changes: 13 additions & 1 deletion deploy/helm/openshell/templates/gateway.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,21 @@ metadata:
spec:
gatewayClassName: {{ .Values.grpcRoute.gateway.className }}
listeners:
- name: http
- name: {{ ternary "https" "http" (eq .Values.grpcRoute.gateway.listener.protocol "HTTPS") }}
port: {{ .Values.grpcRoute.gateway.listener.port }}
protocol: {{ .Values.grpcRoute.gateway.listener.protocol }}
{{- if eq .Values.grpcRoute.gateway.listener.protocol "HTTPS" }}
{{- if not .Values.grpcRoute.gateway.listener.tls.certificateRefs }}
{{- fail "grpcRoute.gateway.listener.tls.certificateRefs is required when grpcRoute.gateway.listener.protocol is HTTPS" }}
{{- end }}
{{- if not .Values.server.disableTls }}
{{- fail "grpcRoute.gateway.listener.protocol=HTTPS terminates TLS at Envoy Gateway, which forwards plaintext gRPC to the gateway pod; set server.disableTls=true so the pod listens plaintext (this chart does not render a BackendTLSPolicy for re-encryption to a TLS backend)" }}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we document a way to enable this? Possibly as a follow-up; it would be great to support end-to-end TLS, but I think this PR is a good first step.

{{- end }}
tls:
mode: Terminate
certificateRefs:
{{- toYaml .Values.grpcRoute.gateway.listener.tls.certificateRefs | nindent 10 }}
{{- end }}
allowedRoutes:
namespaces:
from: {{ .Values.grpcRoute.gateway.listener.allowedRoutes }}
Expand Down
16 changes: 14 additions & 2 deletions deploy/helm/openshell/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,21 @@ grpcRoute:
namespace: ""
# Listener settings (only used when gateway.create is true).
listener:
# -- Listener port for the generated Gateway resource.
# -- Listener port for the generated Gateway resource. Use 443 with protocol HTTPS.
port: 80
# -- Listener protocol for the generated Gateway resource.
# -- Listener protocol for the generated Gateway resource: HTTP or HTTPS.
# HTTPS terminates TLS at the Envoy Gateway listener; pair it with
# server.disableTls=true so Envoy forwards plaintext to the gateway pod,
# and use OIDC for client identity (the gateway never sees the client cert).
protocol: HTTP
# -- "Same" restricts attached routes to the release namespace; "All" allows any namespace.
allowedRoutes: Same
# TLS settings for the listener. Used only when protocol is HTTPS
# (mode is always Terminate).
tls:
# -- certificateRefs for the HTTPS listener. Required when protocol is
# HTTPS. Each entry needs a `name` pointing at a kubernetes.io/tls Secret
# in the Gateway's namespace. May reference a cert-manager-issued Secret
# or the existing openshell-server-tls Secret (its SANs must include the
# external hostname).
certificateRefs: []

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a certificate is provided by e.g. cert-manager, would that be supported automatically?

58 changes: 58 additions & 0 deletions docs/kubernetes/ingress.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,64 @@ openshell gateway add http://<external-ip> --name production
openshell status
```

This setup is plaintext end-to-end and is intended for development. For external access, terminate TLS at the gateway as shown below.

## HTTPS (TLS termination)

Envoy Gateway can terminate TLS at the listener and forward plaintext to the OpenShell gateway pod:

```text
client → HTTPS → Envoy Gateway (terminate TLS) → plaintext → openshell gateway pod
```

Envoy Gateway only terminates TLS here — it does not perform OIDC. Do not enable an Envoy Gateway OIDC `SecurityPolicy` in front of the gateway: that flow relies on browser redirects and cookies and cannot work with the OpenShell CLI or headless agents. Instead, the OpenShell gateway validates an OIDC bearer token that the client sends in the gRPC `authorization` metadata, which Envoy forwards untouched.

Because Envoy terminates TLS, the OpenShell gateway never sees a client certificate, so client mTLS cannot provide identity on this path. Use OIDC bearer tokens for client identity instead.

For headless agents and CI, the CLI obtains the token via the OAuth2 client-credentials grant (no browser): set `OPENSHELL_OIDC_CLIENT_SECRET` to the OAuth client secret before running `openshell gateway add`. The client id comes from `--oidc-client-id` (default `openshell-cli`); pass it explicitly if your IdP client uses a different id. Interactive human users get the browser-based Authorization Code + PKCE flow by default.

### Provide a TLS certificate

Create a `kubernetes.io/tls` Secret in the `openshell` namespace with the certificate for your external hostname:

```shell
kubectl -n openshell create secret tls openshell-ingress-tls \
--cert=tls.crt --key=tls.key
```

The Secret may also be issued by cert-manager, or you can reference the chart's existing `openshell-server-tls` Secret if its SANs include the external hostname.

### Install with HTTPS termination

Enable an HTTPS listener, point it at the Secret, disable gateway-pod TLS so Envoy forwards plaintext, and configure an OIDC issuer for client identity:

```shell
helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell \
--set grpcRoute.enabled=true \
--set grpcRoute.gateway.create=true \
--set grpcRoute.gateway.className=eg \
--set grpcRoute.gateway.listener.protocol=HTTPS \
--set grpcRoute.gateway.listener.port=443 \
--set 'grpcRoute.gateway.listener.tls.certificateRefs[0].name=openshell-ingress-tls' \
--set server.disableTls=true \
--set server.oidc.issuer=https://<issuer> \
--set 'grpcRoute.hostnames[0]=<external-hostname>'
```

Keep the certificate Secret in the release namespace. Referencing a Secret in another namespace requires a `ReferenceGrant`.

### Register over HTTPS

```shell
openshell gateway add https://<external-hostname> --name production --oidc-issuer https://<issuer>
openshell status
```

See [Authentication](/kubernetes/setup) for OIDC issuer, audience, and roles configuration.

## SSH Relay

Sandbox SSH uses the gateway endpoint registered with the CLI. No separate Helm SSH host or port values are required.
Expand Down
Loading