From a8cedbea76e6e346ab5bd980e9e3a1bee668d241 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Tue, 29 Sep 2026 10:26:05 +0100 Subject: [PATCH 1/2] fix(docker): generate gateway JWT keys in compose quickstart Fixes #2891 Signed-off-by: Eric Curtin --- deploy/docker/docker-compose.yml | 29 ++++++++++++------- deploy/docker/gateway.toml | 26 +++++++++-------- .../gateways/container-deployment.mdx | 4 ++- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml index e491b825c3..6c5681c93a 100644 --- a/deploy/docker/docker-compose.yml +++ b/deploy/docker/docker-compose.yml @@ -9,7 +9,7 @@ # # Quick start: # -# 1. Start the gateway: +# 1. Start the gateway (init first generates JWT keys in /var/lib/openshell/tls): # docker compose up -d # # 2. Register the gateway with the CLI (one-time): @@ -49,19 +49,16 @@ # bind-mount source when sandbox containers are created. Named volumes # cannot be used here because Docker resolves bind-mount sources against the # host filesystem, not the container filesystem. -# -# Linux note: -# host.docker.internal and host.openshell.internal are not automatically -# added on Linux Docker. Add the following under the gateway service: -# extra_hosts: -# - "host.docker.internal:host-gateway" -# - "host.openshell.internal:host-gateway" services: gateway: - image: ghcr.io/nvidia/openshell/gateway:${IMAGE_TAG:-latest} + image: &gateway-image ghcr.io/nvidia/openshell/gateway:${IMAGE_TAG:-latest} restart: unless-stopped + depends_on: + init: + condition: service_completed_successfully + # Clear the default CMD so gateway.toml owns all settings (see note above). command: [] @@ -96,7 +93,8 @@ services: # (e.g. /var/lib/openshell/gateway): the path must match exactly on # both the host and inside the container, and a single gateway per host # is the expected topology. - - type: bind + - &openshell-data + type: bind source: /var/lib/openshell target: /var/lib/openshell bind: @@ -126,3 +124,14 @@ services: # bind-mounted directory so its path is resolvable by the host Docker daemon. XDG_DATA_HOME: /var/lib/openshell HOME: /var/lib/openshell + + # One-shot: generates the sandbox JWT keys (kept if present). + init: + image: *gateway-image + user: "0" + restart: "no" + command: ["generate-certs", "--output-dir", "/var/lib/openshell/tls"] + environment: + HOME: /var/lib/openshell + volumes: + - *openshell-data diff --git a/deploy/docker/gateway.toml b/deploy/docker/gateway.toml index c0cbda4ef6..eea0faa78d 100644 --- a/deploy/docker/gateway.toml +++ b/deploy/docker/gateway.toml @@ -14,12 +14,8 @@ # clearing the CMD first. # # grpc_endpoint note: -# host.docker.internal is automatically resolvable from containers on -# Docker Desktop (Windows / macOS). On Linux, add extra_hosts to the -# gateway service: -# extra_hosts: -# - "host.docker.internal:host-gateway" -# - "host.openshell.internal:host-gateway" +# Omitted on purpose. The supervisor uses host networking and reaches the +# published port at http://127.0.0.1:8080 (keep OPENSHELL_PORT at 8080). [openshell] version = 2 @@ -33,6 +29,18 @@ log_level = "info" compute_driver = "docker" disable_tls = true +# No TLS and loopback only: user calls are unauthenticated. +# Supervisors still authenticate with the JWT keys below. +[openshell.gateway.auth] +allow_unauthenticated_users = true + +# Written by the compose init service. +[openshell.gateway.gateway_jwt] +signing_key_path = "/var/lib/openshell/tls/jwt/signing.pem" +public_key_path = "/var/lib/openshell/tls/jwt/public.pem" +kid_path = "/var/lib/openshell/tls/jwt/kid" +gateway_id = "openshell-docker" + [openshell.drivers.docker] # Default image pulled for `openshell sandbox create` without --from. default_image = "nvcr.io/nvidia/base/ubuntu:24.04" @@ -44,12 +52,6 @@ supervisor_image = "ghcr.io/nvidia/openshell/supervisor:latest" image_pull_policy = "if_not_present" # Value assigned to the openshell.sandbox_namespace label on sandbox containers. sandbox_label = "openshell" -# Address sandbox containers use to call back to the gateway. -# The Docker driver replaces the host with host.openshell.internal and the -# port with the gateway's own bind port (8080). Only the scheme survives. -# The gateway must be published on port 8080 on the Docker host so that -# host.openshell.internal:8080 resolves to the gateway container. -grpc_endpoint = "http://host.openshell.internal:8080" # Explicit supervisor-compatible Docker default. Set RuntimeDefault or # Localhost/ only when the daemon host has AppArmor available. app_armor_profile = "Unconfined" diff --git a/docs/how-it-works/gateways/container-deployment.mdx b/docs/how-it-works/gateways/container-deployment.mdx index e42ede6bc4..d01370b970 100644 --- a/docs/how-it-works/gateways/container-deployment.mdx +++ b/docs/how-it-works/gateways/container-deployment.mdx @@ -156,7 +156,7 @@ the repository contains a production-ready Compose setup with full inline docume | File | Purpose | |---|---| -| `docker-compose.yml` | Gateway service, volumes, and environment variables | +| `docker-compose.yml` | Gateway and init services, volumes, and environment variables | | `gateway.toml` | TOML configuration mounted into the container | Clone or copy those files, then start the gateway: @@ -165,6 +165,8 @@ Clone or copy those files, then start the gateway: docker compose -f deploy/docker/docker-compose.yml up -d ``` +The `init` service runs first and writes the sandbox JWT signing keys to `/var/lib/openshell/tls` on the host. The Docker driver requires them, and existing keys are kept. Do not commit them. This setup disables TLS and user authentication and publishes the port on loopback only. + Register the gateway with the CLI. If registering from the same machine: ```shell From b746323741142a34481c376a5eca47d7ea3b13e4 Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Fri, 2 Oct 2026 01:44:20 +0100 Subject: [PATCH 2/2] docs(docker): add compose mTLS steps Signed-off-by: Eric Curtin --- .../gateways/container-deployment.mdx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/how-it-works/gateways/container-deployment.mdx b/docs/how-it-works/gateways/container-deployment.mdx index d01370b970..660eea5995 100644 --- a/docs/how-it-works/gateways/container-deployment.mdx +++ b/docs/how-it-works/gateways/container-deployment.mdx @@ -180,6 +180,30 @@ machine's LAN address: openshell gateway add http://HOST_IP:8080 --remote --name remote ``` +### Enable mTLS + +The `init` service already writes the server, client, and CA certificates to `/var/lib/openshell/tls`, so enabling mTLS only needs configuration changes: + +1. In `gateway.toml`, remove `disable_tls = true` and the `[openshell.gateway.auth]` table. +2. In `docker-compose.yml`, add `OPENSHELL_LOCAL_TLS_DIR: /var/lib/openshell/tls` under the `gateway` service `environment`. The gateway loads its server certificate, client CA, and sandbox client bundle from that directory, and turns on mTLS user authentication. +3. Apply the change: + + ```shell + docker compose -f deploy/docker/docker-compose.yml up -d + ``` + +Then copy the client bundle to `~/.config/openshell/gateways/local/mtls/`, where the CLI loads it (refer to [Gateway Authentication](/how-it-works/gateways/authentication#mtls)). The files are owned by root, so copy them with `sudo` and hand them to your user. If you registered the plaintext gateway as `local`, run `openshell gateway remove local` first. + +```shell +mtls=~/.config/openshell/gateways/local/mtls +mkdir -p "$mtls" +sudo cp /var/lib/openshell/tls/ca.crt \ + /var/lib/openshell/tls/client/tls.crt \ + /var/lib/openshell/tls/client/tls.key "$mtls"/ +sudo chown "$USER" "$mtls"/* +openshell gateway add https://127.0.0.1:8080 --local --name local +``` + ## Using Podman Replace `docker` with `podman` in the commands above. Mount the Podman socket instead of the Docker socket and set the driver to `podman`: