OpenShell Deployment
NVIDIA OpenShell is an open-source runtime environment that securely executes autonomous AI agents using sandboxed environments and kernel-level isolation. It allows agents to access files, credentials, and external networks without the risk of system compromise or data exfiltration.
Astonish integrates OpenShell as a Helm subchart — a single helm install deploys the Astonish control plane and the OpenShell gateway together. Agent tool calls execute inside individually isolated sandboxes with granular policy enforcement and full audit trails.
Prerequisites
1. Kubernetes Cluster
- Kubernetes 1.28+
- A RWX StorageClass (NFS, CephFS, EFS, Azure Files, or similar)
- Optional: Istio service mesh (simplifies TLS between control plane and gateway)
2. PostgreSQL
- PostgreSQL 15+ with the
pgvectorextension - A database user with CREATE DATABASE permissions
3. Install the Agent Sandbox CRD
The Agent Sandbox CRD and controller must be installed cluster-wide before deploying Astonish with OpenShell:
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yamlVerify the CRD is installed:
kubectl get crd sandboxes.agents.x-k8s.io4. Install Kyverno (Recommended for Production)
Kyverno is a Kubernetes-native policy engine that Astonish uses to eliminate workspace PVC provisioning latency. Without it, every sandbox pod waits for a 2Gi PVC to provision before starting — adding ~35 seconds on Cinder/OpenStack clusters and 5-15s on EBS gp2/gp3.
With Kyverno installed and ephemeralWorkspace: true set, a ClusterPolicy mutates sandbox pods at admission time to replace the PVC-backed workspace volume with a fast emptyDir. Sandbox startup drops to 3-5 seconds regardless of StorageClass speed.
When to install:
- Required if you set
sandbox.openshell.ephemeralWorkspace: true(recommended for production) - Not needed if your cluster has fast block storage provisioning (local-path, NVMe-backed CSI < 2s) or if you need persistent workspace data across sandbox restarts (rare)
Install Kyverno:
# Add the Kyverno Helm repository
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
# Install Kyverno (v1.12+ required; v1.18+ recommended)
helm install kyverno kyverno/kyverno \
--namespace kyverno \
--create-namespace \
--set admissionController.replicas=3 \
--set backgroundController.replicas=2 \
--waitFor high-availability production clusters, use 3 admission controller replicas. For dev/test, a single replica is fine:
# Minimal install for dev/test
helm install kyverno kyverno/kyverno \
--namespace kyverno \
--create-namespace \
--waitVerify Kyverno is running:
# All pods should be Running
kubectl -n kyverno get pods
# The admission webhook should be registered
kubectl get mutatingwebhookconfigurations | grep kyvernoExpected output:
kyverno-resource-mutating-webhook-cfg ... ...Important notes:
- Kyverno is a cluster-wide tool. If it's already installed in your cluster (by another team or platform), skip this step — the Astonish ClusterPolicy will work with any existing Kyverno 1.12+ installation.
- Kyverno is not bundled as a subchart dependency. It must be installed separately before deploying Astonish with
ephemeralWorkspace: true. - The Astonish ClusterPolicy is disposable — it works around a limitation in OpenShell v0.0.63 (issues #967, #971). Once upstream fixes workspace PVC injection, remove the policy by setting
ephemeralWorkspace: false.
Platform Initialization
1. Initialize the Database
Run from a machine with network access to your PostgreSQL instance:
./astonish platform init \
--host <postgres-host> \
--port 5432 \
--password <postgres-admin-password>This creates the platform database and prints the connection DSN. Save this DSN for the next steps.
2. Generate Secrets
Generate the master encryption key and JWT signing secret:
./astonish platform gen-secret # → use as MASTER_KEY
./astonish platform gen-secret # → use as JWT_SECRET3. Create the Kubernetes Secret
Create the namespace and store the secrets:
kubectl create namespace astonish
kubectl create secret generic astonish-secrets \
-n astonish \
--from-literal=master-key="<master-key-from-step-2>" \
--from-literal=jwt-secret="<jwt-secret-from-step-2>" \
--from-literal=platform-dsn="<dsn-from-step-1>"Helm Values
Create a values-openshell.yaml file for your deployment. Below is a complete production-ready example.
With Istio (Recommended)
When your cluster has Istio, the mesh handles encryption between the control plane and the OpenShell gateway. This is the recommended setup:
# values-openshell.yaml (with Istio service mesh)
# ------------------------------------------------------------------
# Control-plane image
# ------------------------------------------------------------------
image:
repository: ghcr.io/sap/astonish
tag: latest
pullPolicy: IfNotPresent
# ------------------------------------------------------------------
# Namespace layout
# "astonish" prefix yields:
# control plane: astonish
# sandbox: astonish-sandbox
# ------------------------------------------------------------------
namespaces:
prefix: astonish
# ------------------------------------------------------------------
# Secrets — reference the pre-created K8s Secret
# ------------------------------------------------------------------
secrets:
existingSecret: "astonish-secrets"
# ------------------------------------------------------------------
# Platform database
# ------------------------------------------------------------------
config:
storage:
backend: postgres
postgres:
instanceSuffix: "" # Optional: appended to database names
# ------------------------------------------------------------------
# Ingress
# ------------------------------------------------------------------
ingress:
enabled: true
className: nginx
hosts:
- host: astonish.example.com
paths:
- path: /
pathType: Prefix
# ------------------------------------------------------------------
# Service Mesh — Istio
# ------------------------------------------------------------------
mesh:
provider: istio
# ------------------------------------------------------------------
# Sandbox — OpenShell backend
# ------------------------------------------------------------------
sandbox:
enabled: true
backend: openshell
podSecurity: privileged # OpenShell supervisor needs SYS_ADMIN, NET_ADMIN, SYS_PTRACE
openshell:
enabled: true
# Eliminate workspace PVC latency via Kyverno pod mutation (requires Kyverno)
ephemeralWorkspace: true
# Gateway address — leave empty for auto-derived:
# "{release}-openshell.{namespace}.svc.cluster.local:8080"
gateway:
addr: ""
# Sandbox container image (Astonish agent tooling + OpenShell supervisor)
image:
repository: ghcr.io/sap/astonish-sandbox-openshell
tag: latest
# Network egress policy for sandboxes
# Available presets: "default", "code_hosting", "package_registries",
# "llm_apis", "tools", "system", "search", "cdn"
# "default" enables all presets.
networkPolicy:
presets:
- "default"
# Add internal services your agents need to reach:
extraEndpoints: []
# extraEndpoints:
# - host: "internal-api.company.com"
# port: 443
# Storage — required by chart validation (PVCs are created but unused by OpenShell)
storage:
storageClassName: "<your-rwx-storage-class>" # e.g., nfs-client, efs-sc, cephfs
# Seed job — disabled for OpenShell (no overlay layer to seed)
seed:
enabled: false
# Resource limits per sandbox session
limits:
cpu: 2
memory: "2Gi"
processes: 500
requests:
cpuMillis: 100
memoryMiB: 256
# ------------------------------------------------------------------
# OpenShell subchart values (forwarded to NVIDIA OpenShell Helm chart)
# ------------------------------------------------------------------
openshell:
nameOverride: openshell
server:
# Istio handles encryption — gateway listens plaintext
disableTls: true
# Must match the computed sandbox namespace
sandboxNamespace: "astonish-sandbox"
# Must match sandbox.openshell.image above
sandboxImage: "ghcr.io/sap/astonish-sandbox-openshell:latest"
auth:
# Istio AuthorizationPolicy handles identity
allowUnauthenticatedUsers: true
tls:
clientCaSecretName: ""
# PKI init job — MUST remain enabled (generates JWT signing keypair
# for sandbox token issuance)
pkiInitJob:
enabled: true
# Astonish manages its own NetworkPolicies
networkPolicy:
enabled: falseWithout Istio
If you don't have a service mesh, the OpenShell gateway handles mTLS itself:
# Differences from the Istio version:
# No mesh provider
mesh:
provider: ""
# OpenShell subchart — gateway handles its own TLS
openshell:
nameOverride: openshell
server:
# Gateway terminates mTLS itself
disableTls: false
sandboxNamespace: "astonish-sandbox"
sandboxImage: "ghcr.io/sap/astonish-sandbox-openshell:latest"
auth:
# Without mesh identity, consider restricting access
allowUnauthenticatedUsers: false
tls:
# PKI init job auto-generates certificates
clientCaSecretName: ""
pkiInitJob:
enabled: true
networkPolicy:
enabled: falseAll other values remain the same as the Istio version.
Install
Deploy with Helm:
helm upgrade --install astonish deploy/helm/astonish \
-n astonish --create-namespace \
-f values-openshell.yamlWait for all pods to become ready:
kubectl -n astonish rollout status deployment/astonish-api
kubectl -n astonish rollout status deployment/astonish-workerVerify
Check that all components are running:
# Control-plane pods (api, worker, openshell gateway)
kubectl -n astonish get pods
# Sandbox namespace exists (empty until first agent session)
kubectl get ns astonish-sandbox
# Agent Sandbox CRD is installed
kubectl get crd sandboxes.agents.x-k8s.io
# OpenShell gateway logs (should show "listening on :8080")
kubectl -n astonish logs -l app.kubernetes.io/name=openshell --tail=20Access Studio
Via Ingress
If you configured ingress, open https://astonish.example.com in your browser.
Via Port-Forward
For local access without ingress:
kubectl -n astonish port-forward svc/astonish-api 9393:9393 &
open http://localhost:9393The first user to register will become the organization owner with full admin access. A default organization and team are created automatically on first signup.
Notes
TLS and Service Mesh
| Setup | mesh.provider | openshell.server.disableTls | How encryption works |
|---|---|---|---|
| With Istio | "istio" | true | Mesh mTLS encrypts all traffic |
| Without mesh | "" | false | Gateway terminates mTLS (auto-generated certs) |
| Dev/testing only | "" | true | Plaintext (not for production) |
Pod Security
OpenShell supervisor containers require elevated privileges (SYS_ADMIN, NET_ADMIN, SYS_PTRACE) for kernel-level isolation. Set sandbox.podSecurity: privileged to configure the sandbox namespace's Pod Security Admission accordingly.
Storage Class
The chart requires a storageClassName for validation even though OpenShell does not use the overlay PVC system. The PVCs are created but remain unused. Set this to any available RWX StorageClass in your cluster.
Note: The workspace PVC that OpenShell creates per sandbox is separate from this storage class — it uses the cluster's default StorageClass. To eliminate workspace PVC latency, see Workspace Storage (Ephemeral Mode) below.
Network Policy Presets
By default, sandboxes can reach common external services (code hosting, package registries, LLM APIs, CDNs). The OpenShell supervisor enforces these rules at the network namespace level — the Kubernetes NetworkPolicy is permissive, and fine-grained enforcement happens inside the sandbox.
Available presets: default (all below), code_hosting, package_registries, llm_apis, tools, system, search, cdn.
Add internal services with extraEndpoints:
sandbox:
openshell:
networkPolicy:
presets:
- "default"
extraEndpoints:
- host: "internal-api.company.com"
port: 443
- host: "*.internal.company.com"
port: 443For dynamic per-team/org/platform network rules managed through the Studio UI (rather than static config), see Network Policy.
Landlock Filesystem Policy
The sandbox filesystem is controlled by Linux Landlock LSM. Astonish configures the policy to allow:
- Read-only:
/usr,/bin,/sbin,/lib,/lib64,/etc,/opt,/dev/null,/dev/urandom - Read-write:
/sandbox,/tmp,/var/tmp,/home,/run,/dev/pts
The /dev/pts path is required for PTY allocation (interactive shell support). On kernel 6.10+, Landlock ABI v5 restricts ioctl on device files — without this path explicitly listed, the shell_command tool's PTY allocation would fail with Permission denied.
Note:
/dev/ptmxis typically a symlink and cannot be listed directly (the supervisor refuses to chown symlinks). PTY allocation works through/dev/pts/ptmxwhich is accessible via the/dev/ptsdirectory entry.
Extending the filesystem policy:
sandbox:
openshell:
filesystemPolicy:
extraReadOnly:
- /data/shared-models
extraReadWrite:
- /mnt/scratchLandlock enforcement mode:
sandbox:
openshell:
# "best_effort" (default) — degrades gracefully on kernels without Landlock
# "hard_requirement" — fails sandbox startup if Landlock can't be enforced
landlockCompatibility: "best_effort"Use hard_requirement when debugging Landlock issues — it provides fast, clear failure instead of silent degradation.
:::note Supervisor Version OpenShell supervisor ≥ 0.0.70 is recommended for reliable PTY device handling on kernel 6.10+. Earlier versions may have issues with device path pre-opening during prepare_filesystem() (see OpenShell Issue #749). Upgrade by bumping the subchart version in Chart.yaml and running helm dependency update. :::
Namespace Convention
The namespaces.prefix value drives all namespace names:
| Setting | Result |
|---|---|
prefix: astonish | Control plane: astonish, Sandboxes: astonish-sandbox |
prefix: astonish-prod | Control plane: astonish-prod, Sandboxes: astonish-prod-sandbox |
The openshell.server.sandboxNamespace must match the computed sandbox namespace.
Workspace Storage (Ephemeral Mode)
By default, the OpenShell gateway injects a 2Gi ReadWriteOnce PersistentVolumeClaim into every sandbox pod for the /sandbox workspace directory. On clusters with slow block storage provisioning (Cinder/OpenStack, EBS gp2), this PVC provisioning dominates sandbox startup time — adding 15-35+ seconds before the pod can start.
How ephemeral mode works:
When sandbox.openshell.ephemeralWorkspace: true is set and Kyverno is installed, the Astonish Helm chart deploys a ClusterPolicy that:
- Intercepts sandbox Pod creation at admission time (after the Sandbox controller assembles the pod spec)
- Finds the
workspacevolume (which references a PVC) - Replaces it with
emptyDir: { sizeLimit: "2Gi" }
The pod starts immediately — no PVC provisioning wait. The /sandbox directory is backed by the node's filesystem (tmpfs or disk, depending on kubelet configuration).
Trade-offs:
| Aspect | With PVC (default) | With emptyDir (ephemeral) |
|---|---|---|
| Startup latency | 15-35s (Cinder) | 3-5s |
| Data persistence | Survives pod restart | Lost on pod restart |
| Storage accounting | Per-PVC billing | Part of node ephemeral storage |
| Cleanup | PVC deleted with Sandbox | Automatic (pod termination) |
| Capacity | Fixed 2Gi PVC | Shared node ephemeral storage (2Gi soft limit) |
For most use cases, ephemeral mode is preferred: chat sessions are short-lived, sandboxes are destroyed after idle timeout, and workspace data does not need to survive restarts.
Enable ephemeral mode:
sandbox:
openshell:
ephemeralWorkspace: true # Requires Kyverno (see Prerequisites §4)Verify the policy is active:
# The ClusterPolicy should exist and report "Ready"
kubectl get clusterpolicy -l app.kubernetes.io/component=openshell-workspace
# Expected output:
# NAME ADMISSION BACKGROUND ... MESSAGE
# astonish-sandbox-emptydir true true ... ReadyVerify a sandbox pod uses emptyDir:
# After creating a sandbox (or starting a chat), inspect the pod:
kubectl -n <sandbox-namespace> get pods
kubectl -n <sandbox-namespace> get pod <sandbox-pod> -o jsonpath='{.spec.volumes[?(@.name=="workspace")]}'
# Expected: {"emptyDir":{"sizeLimit":"2Gi"},"name":"workspace"}
# NOT: {"name":"workspace","persistentVolumeClaim":{"claimName":"..."}}Troubleshooting:
| Symptom | Cause | Fix |
|---|---|---|
Sandbox pods stuck Pending with volume error | Kyverno not installed or policy not deployed | Install Kyverno, run helm upgrade |
ClusterPolicy shows Ready: false | Kyverno webhook not registered | Check kubectl -n kyverno get pods, restart if needed |
Pod has PVC volume despite ephemeralWorkspace: true | Policy not matching (wrong namespace) | Verify openshell.server.sandboxNamespace matches namespaces.prefix + -sandbox |
| Kyverno logs show "failed to apply rule" | Kyverno version too old (< 1.12) | Upgrade Kyverno to 1.12+ |
Pod starts but /sandbox writes fail with ENOSPC | Node ephemeral storage full | Increase node disk or reduce sizeLimit |
Disabling ephemeral mode:
To revert to PVC-backed workspace storage (e.g., if you need persistent workspace data):
sandbox:
openshell:
ephemeralWorkspace: false # Default — Kyverno policy not deployedRun helm upgrade — the ClusterPolicy will be removed and new sandboxes will use PVC storage.
Note on orphaned PVCs: When ephemeral mode is active, the Sandbox controller still creates a PVC (from volumeClaimTemplates in the Sandbox CRD). This PVC remains Pending (never bound, since no pod references it) and is automatically cascade-deleted when the Sandbox is destroyed via its ownerReference. No manual cleanup is required.
See Also
- Sandboxes — security model and isolation architecture
- Kubernetes Deployment — standard K8s deployment (without OpenShell)
- Deployment Overview — choosing between deployment models