Skip to main content

OpenShell MVP: Gateway-per-Tenant on Kind + OpenShift

Date: 2026-04-24 Status: Draft Supersedes: openshell-k8s-integration.md (directions only; gap analysis remains valid reference) Background: openshell-driver-architecture.md (driver internals, OIDC analysis, multi-tenancy options)


1. Goal

Deploy OpenShell on Kind and OpenShift with multi-tenant isolation, OIDC authentication, session persistence, and headless agent execution. Two tenants (team1, team2) prove isolation. No upstream OpenShell code changes required.

In scope:

CapabilityApproach
Multi-tenancyOne gateway per tenant (process isolation)
AuthenticationOIDC via Keycloak (PR #935) + browser PKCE
Compute driverFork of openshell-driver-openshift (out-of-process, Go)
Credential resolutionKeycloak credentials driver (OAuth2 client_credentials via driver subsystem)
Session persistencedtach via init container
Headless modeRossoctl AgentTask CRD + controller
PlatformsKind (Istio Gateway API) + OpenShift (passthrough Routes)
StoreSQLite (one file per gateway instance)

Out of scope (see Section 10): shared-gateway multi-tenancy, SPIFFE/SPIRE identity, supervisor extensions, transparent proxy-level token exchange (AuthBridge parity), OTel/MLFlow instrumentation (integration points documented in Section 8), gateway HA, CI client credentials flow.


2. Architecture

Each tenant gets an isolated OpenShell stack in its own namespace. A shared Keycloak instance provides authentication. The agent-sandbox-controller is shared across the cluster.

Cluster
├── keycloak/ Shared OIDC provider
├── agent-sandbox-system/ Shared CRD controller
├── team1/ Gateway + driver + sandboxes
│ ├── gateway (StatefulSet)
│ ├── compute-driver (sidecar)
│ ├── SQLite (emptyDir or PVC)
│ ├── Service (ClusterIP)
│ ├── Ingress (TCPRoute or Route)
│ └── sandbox pods
└── team2/ Same structure, isolated

3. Components

3.1 Gateway Pod

Each tenant's gateway runs as a StatefulSet with the compute driver as a sidecar container. They communicate over a Unix domain socket on a shared emptyDir volume.

ContainerImageRole
gatewayForked openshell-server (adds --compute-driver-socket flag, ~20 lines Rust)Control plane, gRPC API, SSH bridge, OIDC validation
compute-driverBuilt from forked openshell-driver-openshift (Go)Sandbox lifecycle via agents.x-k8s.io/v1alpha1/Sandbox CRD
credentials-driveropenshell-credentials-keycloak (Go)OAuth2 token resolution via Keycloak

The gateway fork is minimal. The upstream VM driver already uses RemoteComputeDriver over UDS; the fork adds a generic External variant. Upstreaming should be proposed in parallel.

Gateway environment (per tenant):

Env VarValuePurpose
OPENSHELL_OIDC_ISSUERhttps://keycloak.<domain>/realms/openshellJWT issuer validation
OPENSHELL_OIDC_AUDIENCEteam1 or team2Tenant access scoping
OPENSHELL_OIDC_ENABLEDtrueEnable OIDC auth
OPENSHELL_DB_URLsqlite:///data/openshell.dbPer-tenant SQLite
OPENSHELL_COMPUTE_DRIVER_SOCKET/run/drivers/compute.sockUDS to compute driver
OPENSHELL_CREDENTIALS_DRIVER_SOCKET/run/drivers/credentials.sockUDS to credentials driver

3.2 Compute Driver

Forked from zanetworker/openshell-driver-openshift. Already implements the full ComputeDriver gRPC contract (9 RPCs), uses the agents.x-k8s.io/v1alpha1/Sandbox CRD, and solves supervisor side-loading with init container + emptyDir.

Changes needed for MVP:

ChangeSizeDescription
Namespace from configSmallRead --namespace from flag/env. Each tenant's driver targets one namespace.
Tenant labelsSmallAdd openshell.ai/tenant: <team> and rossoctl.io/team: <team> to sandbox pods
Scoped RBACSmallReplace demo cluster-admin with namespace-scoped Role (sandboxes, pods, secrets)
dtach in init containerSmallAdd dtach binary to the supervisor-loader init container image

What works as-is: gRPC server, Sandbox CRD CRUD, supervisor init container injection, watch/reconciliation, GPU pre-flight, env var injection, endpoint resolution.

3.3 Credentials Driver (Keycloak)

A new out-of-process credentials driver that implements OpenShell's CredentialsDriver gRPC contract (ResolveCredential, ListCredentials). Runs as a third sidecar container in the gateway pod, communicating over UDS.

How it works:

Agent Process
│ HTTP request with placeholder credential

Supervisor Egress Proxy
│ resolves placeholder → asks gateway

Gateway
│ calls credentials driver via gRPC

Credentials Driver (openshell-credentials-keycloak)
│ OAuth2 client_credentials grant

Keycloak → returns access_token

▼ (back through the chain)
Supervisor injects Bearer token into outbound request

The key insight is that this fits cleanly into OpenShell's existing credential resolution flow. The supervisor already resolves placeholder credentials through the gateway. The gateway already delegates to a credentials driver. The only new piece is a driver that calls Keycloak instead of reading static secrets.

What the driver does:

OperationBehavior
ResolveCredential(name)Looks up the named credential config (client_id, client_secret, token endpoint, scopes). Calls Keycloak's token endpoint with grant_type=client_credentials. Returns the access token. Caches tokens until near-expiry.
ListCredentialsReturns all configured credential names (e.g., github-api, rossoctl-backend).

Configuration per tenant:

Each tenant's credentials driver has its own config file mapping credential names to Keycloak clients:

credentials:
github-api:
client_id: team1-github
client_secret_ref: team1-github-secret # K8s Secret reference
token_endpoint: https://keycloak.example.com/realms/openshell/protocol/openid-connect/token
scopes: ["repo", "read:org"]
rossoctl-backend:
client_id: team1-rossoctl
client_secret_ref: team1-rossoctl-secret
token_endpoint: https://keycloak.example.com/realms/openshell/protocol/openid-connect/token
scopes: ["rossoctl:read"]

Client secrets are stored in Kubernetes Secrets and mounted into the driver container. The driver never persists tokens to disk.

What this provides vs. what it doesn't:

CapabilityProvided?
OAuth2 client_credentials for outbound callsYes
Token caching and auto-refreshYes
Per-tenant credential isolationYes (separate driver instance per tenant)
Static API key injection (LLM providers)No — handled by OpenShell's native provider store
Transparent proxy-level exchange (AuthBridge-style)No — agent must use credential placeholders
SPIFFE SVID-based token exchange (RFC 8693)No — beyond MVP

Implementation: Go, matching the compute driver. Estimated ~500 lines. The gRPC contract is defined by OpenShell's proto/credentials_driver.proto.

Open question: The exact target services for OAuth2 credentials are TBD. The driver is designed to be generic — any Keycloak client can be configured. Initial targets will be determined during implementation.

3.4 Sandbox Pod

Created by the agent-sandbox-controller from the Sandbox CRD. The driver configures the pod spec.

initContainers:
- name: supervisor-loader
image: ghcr.io/nvidia/openshell-community/openshell-sandbox:latest
command: ["sh", "-c", "cp /usr/local/bin/openshell-sandbox /opt/bin/ && cp /usr/local/bin/dtach /opt/bin/"]
volumeMounts:
- name: supervisor-bin
mountPath: /opt/bin
containers:
- name: sandbox
image: <agent-image>
command: ["/opt/bin/openshell-sandbox"]
securityContext:
runAsUser: 0
capabilities:
add: [NET_ADMIN, SYS_ADMIN, SYS_PTRACE]
volumeMounts:
- name: supervisor-bin
mountPath: /opt/bin
readOnly: true
- name: workspace
mountPath: /sandbox
volumes:
- name: supervisor-bin
emptyDir: {}
- name: workspace
persistentVolumeClaim:
claimName: <sandbox-pvc>

The supervisor starts as root, sets up Landlock, Seccomp, and network namespace isolation, then drops privileges before exec'ing the agent process.

PodSecurity implication: The sandbox pod's runAsUser: 0 and SYS_ADMIN/SYS_PTRACE/NET_ADMIN capabilities require the agent namespace to use the privileged Pod Security Standard (or a namespace-scoped exemption policy). This is a deliberate tradeoff — the supervisor needs these privileges to establish kernel-level isolation (Landlock, seccomp, netns) before dropping to unprivileged execution. On OpenShift, this means the namespace needs a privileged SCC bound to the sandbox service account.

3.5 agent-sandbox-controller

Upstream Kubernetes SIG controller (registry.k8s.io/agent-sandbox/agent-sandbox-controller). Reconciles agents.x-k8s.io/v1alpha1/Sandbox CRDs into pods and PVCs. Deployed once per cluster in agent-sandbox-system.

3.6 Keycloak Configuration

Shared Keycloak instance (can be Rossoctl's existing Keycloak or a dedicated one).

ResourceValue
Realmopenshell
Clientopenshell-cli (public, PKCE)
Rolesopenshell-admin, openshell-user
Usersalice (team1, user), bob (team2, user), admin (both, admin)
Groups/team1, /team2

Audience scoping: Each gateway validates the aud claim. Keycloak includes the audience based on client scope or group membership. Alice gets tokens with aud=team1; Bob gets aud=team2. A token for team1 is rejected by team2's gateway.


4. Platform-Specific Configuration

4.1 Kind

Ingress: Shared TLS Gateway + per-tenant TLSRoute

The gateway requires L4 passthrough (mTLS between CLI and gateway, SSH tunneling inside gRPC). A single shared Istio Gateway in rossoctl-system handles TLS passthrough for all tenants. Each tenant deploys only a TLSRoute that uses SNI (hostname) to route to its backend.

Shared Gateway (deployed once by deploy-shared.sh):

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: tls-passthrough
namespace: rossoctl-system
annotations:
networking.istio.io/service-type: NodePort
spec:
gatewayClassName: istio
listeners:
- name: tls-passthrough
port: 443
protocol: TLS
tls:
mode: Passthrough
allowedRoutes:
namespaces:
from: All

Per-tenant TLSRoute (deployed by charts/openshell/ per namespace):

apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TLSRoute
metadata:
name: openshell
namespace: team1
spec:
hostnames:
- "openshell-team1.localtest.me"
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: tls-passthrough
namespace: rossoctl-system
sectionName: tls-passthrough
rules:
- backendRefs:
- name: openshell-server
port: 8080

All tenants share the single Gateway (one Envoy pod in rossoctl-system, NodePort 30443). SNI hostname distinguishes tenants — no extra Envoy pods per namespace, no port-per-tenant allocation.

Prerequisites:

  • Kind cluster with Istio installed (Rossoctl's kind-full-test.sh includes this)
  • agents.x-k8s.io CRD + controller deployed

4.2 OpenShift

Ingress: Passthrough Route

apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: openshell
namespace: team1
annotations:
haproxy.router.openshift.io/timeout: 24h
spec:
host: openshell-team1.apps.<cluster>.<domain>
port:
targetPort: grpc
tls:
termination: passthrough
to:
kind: Service
name: openshell-server
  • termination: passthrough is mandatory (mTLS, SSH tunneling)
  • timeout: 24h is mandatory (SSH sessions idle-timeout at 30s default)
  • Automatic DNS via *.apps.<cluster>.<domain> wildcard

SCC for sandbox pods:

apiVersion: security.openshift.io/v1
kind: SecurityContextConstraints
metadata:
name: openshell-sandbox
allowPrivilegedContainer: false
allowHostNetwork: false
allowHostPorts: false
allowHostDirVolumePlugin: false
requiredDropCapabilities:
- ALL
allowedCapabilities:
- NET_ADMIN
- SYS_ADMIN
- SYS_PTRACE
runAsUser:
type: RunAsAny
seLinuxContext:
type: MustRunAs
volumes:
- emptyDir
- persistentVolumeClaim
- secret
- configMap

The SCC is bound to the sandbox service account in each tenant namespace. The gateway and driver containers do not need elevated privileges.

RBAC (namespace-scoped Role, not ClusterRole):

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: openshell-gateway
namespace: team1
rules:
- apiGroups: ["agents.x-k8s.io"]
resources: ["sandboxes"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
- apiGroups: [""]
resources: ["pods", "events", "persistentvolumeclaims"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: openshell-gateway
namespace: team1
roleRef:
kind: Role
name: openshell-gateway
subjects:
- kind: ServiceAccount
name: openshell-gateway
namespace: team1

The gateway can only see resources in its own namespace. Even if compromised, it has zero access to other tenants.

ResourceQuota per tenant:

apiVersion: v1
kind: ResourceQuota
metadata:
name: sandbox-quota
namespace: team1
spec:
hard:
pods: "10"
requests.cpu: "20"
requests.memory: "40Gi"
persistentvolumeclaims: "10"

5. Authentication Flow

5.1 CLI Login (Browser PKCE)

User CLI Keycloak Gateway
│ │ │ │
│ openshell login │ │ │
│─────────────────────>│ │ │
│ │ Authorization Code │ │
│ │ + PKCE (browser) │ │
│ │───────────────────────>│ │
│ Browser: login form │ │ │
│<─────────────────────│────────────────────────│ │
│ Credentials │ │ │
│─────────────────────>│───────────────────────>│ │
│ │ Auth code callback │ │
│ │<───────────────────────│ │
│ │ Exchange for tokens │ │
│ │───────────────────────>│ │
│ │ access_token (JWT) │ │
│ │ + refresh_token │ │
│ │<───────────────────────│ │
│ │ │ │
│ openshell sandbox │ │ │
│ create -- claude │ │ │
│─────────────────────>│ gRPC + Bearer JWT │ │
│ │───────────────────────────────────────────────>│
│ │ │ Validate JWT: │
│ │ │ iss, aud=team1, │
│ │ │ roles, scopes │
│ │ │<─────────────────────│
│ │ │ │

Token auto-refresh is handled by the CLI. The refresh token is stored locally. Each gateway validates aud matches its tenant name.

5.2 RBAC Within a Tenant

PR #935 provides two roles:

OperationRequired Role
Sandbox CRUD, exec, SSHopenshell-user
Provider CRUD, config mutationsopenshell-admin

Optional scope enforcement (sandbox:read, sandbox:write, etc.) layers on top.

Limitation: Within a tenant, all openshell-user members can access all sandboxes. There is no per-user sandbox ownership. This is acceptable for the MVP (teams of 2-5 people sharing a workspace).


6. Session Persistence (dtach)

Problem

When the CLI disconnects (network drop, terminal close), the SSH session ends and the sandbox is destroyed (default behavior). With --keep, the sandbox pod survives but the agent process is killed because it was running inside the SSH session.

Solution

dtach is a minimal (~20KB) statically-compiled process detacher. It is delivered via the same init container as the supervisor binary. Every sandbox has detach/reattach capability without custom images or network egress.

Usage

Create a persistent sandbox with dtach:

openshell sandbox create --keep -- dtach -c /tmp/session -z claude

Disconnect: Close the terminal or press Ctrl-\. The sandbox pod and agent process continue running.

Reconnect:

openshell sandbox connect <sandbox-id>
dtach -a /tmp/session

Limitations

LimitationImpactAcceptable for MVP?
Pod death kills everythingPVCs survive, session state does notYes (ephemeral sessions are expected)
User must remember --keep + dtachWithout both, disconnect kills the sessionYes (document the pattern)
Gateway restart may lose sandbox indexsandbox connect may fail until reconciliationYes (rare, self-healing)

7. Headless Mode (AgentTask CRD)

Problem

Interactive sandboxes require an SSH connection. For fire-and-forget tasks (refactoring, code review, batch analysis), agents should run unattended.

Solution

A Rossoctl-owned AgentTask CRD and controller that creates sandbox pods with the agent command as the entrypoint instead of sshd.

apiVersion: rossoctl.io/v1alpha1
kind: AgentTask
metadata:
name: refactor-auth
namespace: team1
spec:
image: ghcr.io/nvidia/openshell-community/sandbox:latest
command: ["claude", "--task", "Refactor the auth module to use JWT"]
workspace:
pvc: team1-workspace
subPath: refactor-auth
timeout: 2h
resources:
limits:
cpu: "4"
memory: 8Gi
status:
phase: Running # Pending | Running | Succeeded | Failed
startTime: "2026-04-24T10:00:00Z"
completionTime: null

Controller Behavior

  1. Creates a pod with the same security setup as interactive sandboxes (supervisor init container, capabilities, SCC)
  2. Sets the agent command as entrypoint instead of sshd
  3. Mounts the PVC workspace for persistent artifacts
  4. Sets AgentTask.status from the pod's exit state
  5. Respects the timeout by setting activeDeadlineSeconds on the pod

Credential Injection

In the MVP, headless sandboxes use Kubernetes Secrets mounted into the pod. This is less integrated than the gateway's credential store but functional. Gateway-based credential injection is a beyond-MVP enhancement.

Debugging

kubectl exec -it <pod> -n team1 -- /bin/bash

Direct access to running headless sandboxes. No SSH tunnel needed.


8. Observability Integration Points

Observability (OTel/MLFlow) is not implemented in the MVP but the following integration points are identified for future work:

ComponentWhat to instrumentSink
GatewaySandbox lifecycle events (create, delete, connect, disconnect)OTel traces
GatewayProvider CRUD operationsOTel traces
Supervisor egress proxyHTTP CONNECT requests (target, method, status, latency)OTel spans
Supervisor egress proxyLLM API calls (model, tokens in/out, latency, cost)MLFlow
Compute driverCRD reconciliation events, watch latencyOTel metrics
AgentTask controllerTask lifecycle (pending, running, succeeded, failed, duration)OTel traces

The supervisor's egress proxy is the critical instrumentation point for LLM observability. Every agent-to-LLM call passes through it, making it the natural place to capture token counts, latency, and cost per sandbox/tenant.


9. Deployment and Validation

9.1 Deployment Steps

  1. Cluster setup

    • Kind: kind-full-test.sh (includes Istio)
    • OpenShift: existing cluster with Istio ambient or standard ingress
  2. Shared infrastructure (scripts/openshell/deploy-shared.sh)

    • Deploy agents.x-k8s.io Sandbox CRD + agent-sandbox-controller
    • Gateway API experimental CRDs (TLSRoute/TCPRoute, Kind only)
    • Shared TLS passthrough Gateway in rossoctl-system (Kind only)
    • cert-manager CA chain (ClusterIssuer + CA Certificate)
    • Deploy Keycloak with openshell realm, PKCE client, users (alice/team1, bob/team2, admin/both)
  3. Per-tenant deployment (scripts/openshell/deploy-tenant.sh, repeat for team1 and team2)

    scripts/openshell/deploy-tenant.sh team1
    scripts/openshell/deploy-tenant.sh team2

    Under the hood this runs:

    helm upgrade openshell-team1 charts/openshell/ --install \
    --namespace team1 \
    --set oidc.issuer="http://keycloak-service.keycloak.svc.cluster.local:8080/realms/openshell" \
    --set oidc.audience="team1" \
    --set driver.namespace="team1" \
    --set ingress.type="istio" \
    --set ingress.host="openshell-team1.localtest.me"
  4. CLI configuration

    # Alice (team1) — port 30443 is the shared gateway NodePort
    openshell gateway set --url https://openshell-team1.localtest.me:30443
    openshell login # Browser PKCE flow

9.2 Validation Criteria

#TestPass Condition
1Sandbox lifecyclesandbox create -- claude creates pod, sandbox delete removes it
2SSH connectivityopenshell term opens interactive session inside sandbox
3OPA policy enforcementBlocked egress target returns connection refused
4Provider credential injection (static)Agent can call LLM API via injected API key credentials
5OAuth2 credential resolutionCredentials driver resolves a Keycloak client_credentials grant and the token is injected into an outbound request
6Tenant isolation (auth)Alice's JWT (aud=team1) is rejected by team2's gateway
7Tenant isolation (RBAC)team1's gateway SA cannot list sandboxes in team2 namespace
8Credential isolationProviders created on team1's gateway are invisible from team2
9Session persistenceDisconnect + reconnect with dtach restores session
10Headless executionAgentTask CRD creates pod, runs task, reports status
11OpenShift SCCSandbox pod runs with custom SCC, SELinux enforcing (OpenShift only)

10. Beyond MVP

These items are explicitly deferred. Each has a documented path in openshell-driver-architecture.md.

ItemWhy DeferredReference
Shared-gateway multi-tenancyRequires 5 upstream changes (thread Identity, tenant labels, list filtering, ownership checks, provider scoping)driver-architecture.md Section 3.7
SPIFFE/SPIRE sandbox identityReplaces shared mTLS certs; requires trust domain design and supervisor SPIRE clientdriver-architecture.md Section 7.2
Transparent proxy-level token exchangeFull AuthBridge parity: supervisor egress proxy detects OAuth2 targets and injects tokens automatically. Requires upstream supervisor changes. MVP uses credential placeholders instead.driver-architecture.md Section 2 (Approach B)
SVID-based token exchange (RFC 8693)Exchange SPIFFE SVID for OAuth2 token. Requires SPIRE integration (beyond MVP) + Keycloak token exchange grant.driver-architecture.md Section 7.3
Supervisor extensions (SPIRE, mTLS origination)General-purpose upstream contributions; not needed for process-isolated MVPdriver-architecture.md Section 7.3
OTel/MLFlow instrumentationIntegration points documented in Section 8; implementation requires upstream proxy instrumentationdriver-architecture.md Section 7.6
Gateway HASingle-instance per tenant is acceptable for MVP; externalized state needed for replicask8s-integration.md Section 5.3
CI client credentials flowPR #935 supports it; MVP focuses on browser PKCE for human usersdriver-architecture.md Section 4
Full Rossoctl driver suite (4 drivers)MVP builds compute + credentials drivers; CP identity and sandbox identity drivers are futuredriver-architecture.md Section 7.4
Policy hierarchy and templatesEach tenant manages policies independently for nowk8s-integration.md Section 5.4
Sandbox TTL / hibernationCronJob cleanup as workaround; upstream feature needed for proper lifecyclek8s-integration.md Section 5.7
Upstream gateway fork eliminationPropose --compute-driver-socket flag upstream in parallel with MVPdriver-architecture.md Section 6
Gateway-level session managementAuto-wrap in dtach, session broker, session listing APIk8s-integration.md Section 6
AgentTask gateway integrationGateway handles credential injection for headless pods, sandbox attach debuggingk8s-integration.md Section 6

11. Prerequisites and Dependencies

DependencyStatusRisk
PR #935 (OIDC auth)In reviewLow — active PR, can run from branch
openshell-driver-openshiftPublishedLow — fork and adapt
Gateway fork (--compute-driver-socket, --credentials-driver-socket)~40 lines RustLow — uses existing RemoteComputeDriver pattern
Credentials driver proto (proto/credentials_driver.proto)Defined by OpenShellLow — implement against existing contract
agent-sandbox-controllerPublishedLow — upstream K8s SIG project
KeycloakRossoctl ships with KeycloakNone
Istio (Kind)Rossoctl ships with IstioNone
Supervisor binary imagePublished by NVIDIANone

12. Open Items

#QuestionImpactStatus
1Supervisor + SELinux on OpenShiftMay need custom SELinux policy moduleNeeds testing in Phase 1
2Supervisor binary version pinningMust match gateway version? Compatibility matrix?Check upstream docs
3Keycloak audience claim mechanismClient scope vs group mapper for per-tenant audDesign during Keycloak setup
4dtach licensingRedistribution in init container imageVerify (GPL v2)
5Kind Istio Gateway port allocationOne port per tenant vs SNI-based routingDesign during Kind setup

13. Implementation Plan and PoC Alignment

This section maps the MVP architecture to concrete implementation steps and reconciles with the existing PoC work in PR #1300 (code) and PR #1319 (architecture docs).

13.1 Repos to Fork

The MVP requires 3 upstream repos forked into the rossoctl org, plus 1 new repo:

UpstreamFork toChangesSize
NVIDIA/OpenShellrossoctl/openshellAdd --compute-driver-socket + --credentials-driver-socket flags (~40 lines Rust), cherry-pick PR #935 (OIDC)Small
zanetworker/openshell-driver-openshiftrossoctl/openshell-driver-openshiftNamespace from config flag, tenant labels, scoped RBAC, dtach in init containerSmall
kubernetes-sigs/agent-sandboxNo fork neededDeploy upstream image as-isNone
New repoPurpose
rossoctl/openshell-credentials-keycloakGo credentials driver (~500 lines), implements CredentialsDriver gRPC contract for OAuth2 client_credentials via Keycloak

13.2 Script Architecture

The MVP replaces the two divergent deployment paths (PR #1300's kustomize-based openshell-full-test.sh and the Phase 0 Helm-based scripts/kind/openshell/setup.sh) with a layered script architecture that works on Kind, OpenShift, and CI.

openshell-full-test.sh (orchestrator — thin, calls the others)

├── scripts/openshell/build-images.sh Build forked images (Kind or OCP)
├── scripts/openshell/deploy-shared.sh Shared infra (cert-manager CA, sandbox controller, Keycloak realm)
├── scripts/openshell/deploy-tenant.sh Per-tenant gateway stack (helm install)
└── scripts/openshell/configure-cli.sh Extract certs for local dev (optional)

scripts/openshell/build-images.sh

Builds the 3 forked/new images for the target platform:

  • Gateway fork (rossoctl/openshell)
  • Compute driver (rossoctl/openshell-driver-openshift)
  • Credentials driver (rossoctl/openshell-credentials-keycloak)

On Kind: docker build + kind load docker-image. On OpenShift: Shipwright Build or oc start-build.

scripts/openshell/deploy-shared.sh

Deploys cluster-wide components (idempotent):

  1. agent-sandbox-controller — upstream image, agent-sandbox-system namespace
  2. Gateway API experimental CRDs — for TLSRoute/TCPRoute (Kind only, OCP has Routes)
  3. Shared TLS passthrough Gateway — single Istio Gateway in rossoctl-system with allowedRoutes.namespaces.from: All, NodePort 30443 (Kind only, OCP uses Routes)
  4. cert-manager CA chainClusterIssuer + CA Certificate (see Section 13.3)
  5. Keycloak realmopenshell realm, openshell-cli PKCE client, users (alice/team1, bob/team2, admin/both), audience mappers

scripts/openshell/deploy-tenant.sh <team>

Deploys one tenant's gateway stack via the charts/openshell/ Helm chart:

  • Namespace creation + labels (shared-gateway-access=true, openshell.ai/tenant)
  • cert-manager Certificate CRs for server TLS + client mTLS (signed by shared CA)
  • Gateway StatefulSet with compute-driver + credentials-driver sidecars
  • Service (ClusterIP), namespace-scoped Role/RoleBinding
  • Platform-specific ingress: Istio TLSRoute referencing shared gateway (Kind) or passthrough Route (OpenShift)
  • ResourceQuota
  • Auto-detects Kind vs OCP, constructs OIDC issuer URL from Keycloak service discovery

Invocation: deploy-tenant.sh team1 and deploy-tenant.sh team2.

scripts/openshell/configure-cli.sh <team>

Dev-only script that extracts cert-manager-generated client certs from K8s secrets and writes ~/.config/openshell/gateways/<context>/. Not needed in CI.

Updated openshell-full-test.sh

Becomes a thin orchestrator:

Phase 1: Cluster create (existing kind/hypershift scripts)
Phase 2: Rossoctl platform install (existing Helm installer) [Note: Ansible installer removed]
Phase 3: build-images.sh (forked gateway + drivers)
Phase 4: deploy-shared.sh (sandbox controller, cert-manager CA, Keycloak realm)
Phase 5: deploy-tenant.sh team1 (gateway + driver + ingress)
deploy-tenant.sh team2
Phase 6: E2E tests (adapted from PR #1300 test suite)
Phase 7: Cluster destroy (existing scripts)

charts/openshell/ Helm Chart

New Helm chart for per-tenant deployment. Templated values:

ValueExamplePurpose
oidc.issuerhttps://keycloak.../realms/openshellJWT issuer for gateway validation
oidc.audienceteam1Tenant-scoped audience claim
driver.namespaceteam1Compute driver target namespace
ingress.typeistio or routePlatform-specific ingress
ingress.hostopenshell-team1.localtest.meIngress hostname (SNI routing key)
ingress.gatewayNametls-passthroughShared Istio Gateway name (Kind)
ingress.gatewayNamespacerossoctl-systemNamespace of shared Gateway (Kind)
tls.issuerRefopenshell-ca-issuercert-manager issuer for leaf certs

13.3 TLS via cert-manager

All three existing TLS approaches are replaced by cert-manager Certificate CRs, following the pattern already established by Rossoctl's Shipwright integration (deployments/ansible/roles/rossoctl_installer/tasks/04_install_shipwright.yaml) [historical reference — Ansible installer removed].

What this replaces:

Previous approachWhereProblem
Helm genCA/genSignedCert template patchscripts/kind/openshell/setup.shPatches upstream Helm chart, Helm-only, no auto-renewal
Empty placeholder secrets + --disable-tlsPR #1300 deployments/openshell/tls-secrets.yamlTLS disabled in PoC, manual openssl commands for production
Manual openssl commandsDocumented in PR #1300 tls-secrets.yaml commentsManual, error-prone, no renewal

Shared CA (deployed by deploy-shared.sh):

# Step 1: Bootstrap issuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: openshell-selfsigned
spec:
selfSigned: {}
---
# Step 2: Root CA certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: openshell-ca
namespace: cert-manager
spec:
isCA: true
commonName: openshell-ca
secretName: openshell-ca-secret
duration: 87600h # 10 years
renewBefore: 720h # 30 days
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: openshell-selfsigned
kind: ClusterIssuer
---
# Step 3: CA issuer (signs tenant leaf certs)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: openshell-ca-issuer
spec:
ca:
secretName: openshell-ca-secret

Per-tenant leaf certs (templated in charts/openshell/):

# Server TLS certificate (gateway)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: openshell-server-tls
namespace: {{ .Values.tenant }}
spec:
secretName: openshell-server-tls
duration: 8760h # 1 year
renewBefore: 720h # 30 days
dnsNames:
- openshell-server
- openshell-server.{{ .Values.tenant }}
- openshell-server.{{ .Values.tenant }}.svc
- openshell-server.{{ .Values.tenant }}.svc.cluster.local
- localhost
issuerRef:
name: openshell-ca-issuer
kind: ClusterIssuer
---
# Client mTLS certificate (CLI authentication)
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: openshell-client-tls
namespace: {{ .Values.tenant }}
spec:
secretName: openshell-client-tls
duration: 8760h
renewBefore: 720h
commonName: openshell-client
usages:
- client auth
issuerRef:
name: openshell-ca-issuer
kind: ClusterIssuer

Advantages over previous approaches:

  • Auto-renewal (cert-manager watches expiry, rotates before renewBefore)
  • Works on both Kind and OpenShift (cert-manager is a Rossoctl platform dependency)
  • No upstream chart patching, no manual openssl, no placeholder secrets
  • Follows existing Rossoctl convention (Shipwright, Istio shared trust)
  • Kustomize and Helm compatible (Certificate CRs are standard K8s resources)

13.4 What Gets Retired

Current artifactReplaced byNotes
scripts/kind/openshell/setup.shdeploy-shared.sh + deploy-tenant.sh + configure-cli.shKind-only Phase 0 script, Helm-based
deployments/openshell/ kustomize (PR #1300)charts/openshell/ Helm chartKustomize manifests with --disable-tls
deployments/openshell/tls-secrets.yamlcert-manager Certificate CRsEmpty placeholder secrets
Helm genCA/genSignedCert template patchcert-manager Certificate CRsInjected into upstream chart at runtime
openshell-full-test.sh Phase 3 inline kustomizedeploy-shared.sh + deploy-tenant.sh callsMonolithic deploy logic

13.5 Alignment with PR #1319 Architecture Docs

PR #1319 documents the PoC architecture (single gateway, --disable-tls, 7 agent types, 117 E2E tests). The MVP diverges from the PoC in several areas. This table maps where the architecture docs remain valid and where they need updates:

PR #1319 sectionMVP statusAction needed
§2 Current PoC ArchitectureSuperseded — single gateway replaced by gateway-per-tenantUpdate diagram to show per-tenant stacks
§3 Agent Deployment TiersValid — 3-tier model (Tier 1/2/3) still appliesNo change
§4 Sandboxing LayersValid — supervisor, Landlock, seccomp, netns unchangedNo change
§5 Credential IsolationExtended — MVP adds Keycloak credentials driver alongside existing provider storeAdd credentials driver to the flow
§6 Target Architecture (Phase 2)Partially implemented — compute driver is MVP, backend/UI integration is beyond MVPClarify which parts land in MVP
§7 OpenShell RFC 0001Valid — driver subsystem mapping unchangedNo change
§8 Security: Init Container PatternImplemented — compute driver fork uses init container for supervisor deliveryMark as done
§9 LLM Compatibility MatrixValid — same agent/CLI compatibilityNo change
§10 Egress Policy EnforcementValid — same supervisor enforcement modelNo change
§11 Phase 1 PoC ResultsValid — historical results, still applicable as baselineNo change
§12 Phase 2 Backend/UI IntegrationDeferred — beyond MVP scopeAnnotate as post-MVP
§13 TODO: Production ReadinessPartially addressed — TLS via cert-manager (HIGH), init container (HIGH), scoped RBAC (MEDIUM)Update status column

13.6 Implementation Order

StepDeliverableDepends on
1Fork NVIDIA/OpenShellrossoctl/openshell
2Fork zanetworker/openshell-driver-openshiftrossoctl/openshell-driver-openshift
3Gateway fork: --compute-driver-socket + --credentials-driver-socket, cherry-pick PR #935Step 1
4Compute driver: namespace flag, tenant labels, scoped RBAC, dtach in init containerStep 2
5Create rossoctl/openshell-credentials-keycloak — implement CredentialsDriver gRPC
6scripts/openshell/build-images.shSteps 3, 4, 5
7scripts/openshell/deploy-shared.sh (cert-manager CA, sandbox controller, Keycloak realm)
8charts/openshell/ Helm chart (per-tenant gateway stack with cert-manager leaf certs)Step 7
9scripts/openshell/deploy-tenant.sh (wraps Helm install with platform detection)Step 8
10scripts/openshell/configure-cli.sh (extract certs for local dev)Step 7
11Update openshell-full-test.sh as thin orchestratorSteps 6–9
12E2E tests — adapt PR #1300 test suite for multi-tenant validation criteria (Section 9.2)Step 11

Steps 1–2, 5, and 7 have no dependencies and can start in parallel.