Wednesday, November 4, 2026 Edition No. 228
Search
Review · Infrastructure · 21 min read

OpenShell on Amazon EKS: Hands-On with a Self-Hosted Install

By Fabio Douek

Jump to section
Explain (TLDR) like I am...
What is this?

Imagine building a playground where every robot gets its own fenced corner and a grown-up standing next to it. I built that playground on Amazon's big computer, using a recipe book so I can build it again exactly the same way.

Most of it worked once I fixed a few pieces. The fences really stop robots from wandering, but a new fence can take about a second to close, and one kind of extra-strong room I wanted to add just kept falling down. I don't know why yet.

This is an installation record for OpenShell, an open source agent runtime, on Amazon EKS in the author's own AWS account. The project does not test EKS, so every finding here is the operator's own evidence, not a vendor commitment.

The material points for due diligence: the network fence depends on the cluster's network plugin and can have a short opening when a sandbox starts; telemetry must be switched off explicitly; and the stronger virtual machine isolation option did not work in this version. Document these before relying on the boundary.

The procedure was transplanting OpenShell onto Amazon EKS and checking three vital signs before going further: a kernel feature called Landlock, the network path between each sandbox's two halves, and VM isolation. Two were healthy on the first reading; the third failed reproducibly.

Side effects observed: each sandbox needs its own disk, so it takes about ten seconds to start and thirty to remove, and the network fence can close only about a second after a new pod appears. Contraindicated for anyone who needs VM isolation from this version.

There is a particular relief in watching a careful install go green, check after check, and a particular sinking feeling when the last check you cared about turns red. This build had both, and the red one was the option that promised the most peace of mind.

The honest part is naming what still needs trust: a fence that opens for a second at birth, a plugin setting you have to remember to switch on, a default that sends usage data home. None of it is alarming, but each one is a conversation a team should have before it relaxes.

Think of this as setting up the stage before the band plays. The rig is two nodes, a mixer that only routes approved channels, and every instrument miked through a supervisor. Once it was patched, the soundcheck ran clean: each new sandbox came up in about ten seconds.

The off notes were in the timing and one missing instrument. A new channel can stay open for about a beat before the fence closes, and the VM isolation pedal, which sounded great on its own, cut out every time OpenShell plugged into it.

The story is a repeatable, auditable install of an agent runtime on infrastructure a platform team already owns. Terraform, Helm and nine scripts take you from an empty account to a working, mTLS-protected gateway in well under an hour.

The positioning for buyers is candid: the core works on EKS today, the network fence needs a plugin setting that is off by default, and the VM isolation option is not ready in this release. That honesty is the differentiator, not a weakness to hide.

GitHub See the source code here: openshell-eks

Overview

OpenShell, accepted as a CNCF Sandbox project on 2026-09-26, runs AI agents in sandboxes split into an untrusted workload and a trusted supervisor. Part 1 explained the pieces and ran them on a Mac. This post, part 2 of 6, installs OpenShell 0.1.6 on Amazon EKS 1.37, which is what part 3, part 4, part 5 and part 6 run on.

Upfront: OpenShell doesn’t test EKS. Its CI runs pull requests on kind with Kubernetes 1.35 and releases on k3s 1.29, and its release policy’s build matrix names GKE and OpenShift as supported targets. So everything below, including each workaround, is my own evidence, not a vendor statement.

Before building anything on top of the install, I set three go/no-go checks:

  1. Landlock active in the node kernel. OpenShell’s sandbox runtime refuses to start without it.
  2. The control path: each sandbox’s workload Pod has to reach its supervisor Pod through the VPC CNI’s NetworkPolicy enforcement.
  3. VM isolation: an OpenShell sandbox running in a Kata Containers VM on ordinary EC2 instances with nested virtualization.

The first two passed. The third didn’t, and that shaped the rest of the series.

Architecture

LAPTOP Terraform scripts 01–09 openshell CLI mTLS client cert port-forward 127.0.0.1:17680 EKS 1.37 · 2 × m7i.2xlarge · AL2023 · kernel 6.18 · nested virtualization NAMESPACE openshell · PSA restricted gateway StatefulSet (1 replica) SQLite on a 1 GiB PVC · mTLS on 8080 · telemetry off per sandbox supervisor Pod open egress Service :5500 workload Pod egress: supervisor only UID 10001 2 GiB gp3 workspace volume NetworkPolicy · 2 Services · 2 Secrets · Sandbox CR namespace fences: workloads none, supervisors open egress AGENT-SANDBOX-SYSTEM Agent Sandbox v1.0.3 Sandbox CR → workload Pod KUBE-SYSTEM VPC CNI + network policy agent EKS Pod Identity agent EBS CSI (gp3 default) LB Controller kata-deploy 3.32.0 TEST 5 ONLY (DELETED AFTERWARDS) internet-facing NLB · TCP passthrough · port 8080 · one source /32 our own Service; certificate SAN set at install AMAZON ECR openshell-eks/* gateway · supervisor sandbox · controller ubuntu base · workload AMAZON EBS one volume per sandbox gRPC
As built. Every image comes from ECR; the gateway is reached through a port-forward; each sandbox is two Pods and an EBS volume.

What EKS needs that the defaults don’t give it:

WhatDefaultWhat I did
NetworkPolicy enforcementVPC CNI accepts NetworkPolicies but doesn’t enforce themenableNetworkPolicy: "true" in the vpc-cni add-on
Agent Sandbox controllerNot installed; the chart’s preflight fails without itsandbox.yaml from release v1.0.3 (the version OpenShell’s pull-request CI tests; the docs say “latest”, v1.0.9 on 2026-11-01)
ImagesGHCR, nvcr.io and registry.k8s.io, through one NAT gatewayAll six in ECR (five copied, one built); global.image.registry and sandbox.image in the values
A default StorageClassEKS has noneThe EBS CSI add-on’s defaultStorageClass (gp3)
Gateway resourcesresources: {} (BestEffort)250m CPU, 512 MiB requested
TelemetryOnserver.telemetryEnabled: false
Exposing the gatewayThe chart’s Service takes no annotations or source ranges, and also publishes the unauthenticated metrics port 9090Port-forward by default; our own NLB Service for one test
Certificate namesThe PKI hook never re-issues; SANs cover only in-cluster, loopback and Docker-host namesCreate the NLB first, put its hostname in pkiInitJob.serverDnsNames
CLI registrationopenshell gateway add --local copies a local gateway’s certificates, if the laptop has oneRegister first, then write the cluster’s client bundle

Setup

The companion directory builds everything with Terraform and nine numbered scripts. Each one is meant to be safe to run again; I re-ran 01 to 06, and each converged instead of failing.

Step 1: Tools

Terraform 1.9 or later, the AWS CLI v2, kubectl, Helm (I used 4.3.1; the docs say 3.x, and the chart installed fine), Docker with buildx, jq, envsubst and uv, plus the OpenShell CLI from part 1. No secrets: the gateway’s PKI is generated inside the cluster.

Step 2: The cluster

01_cluster.sh runs Terraform (one VPC with one NAT gateway, EKS 1.37, two m7i.2xlarge nodes), installs the AWS Load Balancer Controller, and checks the result. Two settings matter for OpenShell:

vpc-cni = {
  addon_version        = "v1.23.2-eksbuild.8"
  before_compute       = true
  configuration_values = jsonencode({ enableNetworkPolicy = "true" })
}
cpu_options = {
  nested_virtualization = "enabled"   # /dev/kvm on M7i, for Kata
}
metadata_options = {
  http_tokens                 = "required"
  http_put_response_hop_limit = 1     # no node credentials for pods
}

The whole thing took 13 minutes 30 seconds, 12:48 of it Terraform.

Step 3: Images to ECR

03_images.sh copies five images with their multi-architecture indexes and builds a sixth. It then compares every digest in ECR with its source:

ImageSource
openshell/gateway:0.1.6, supervisor:0.1.6, sandbox:0.1.6ghcr.io/nvidia/openshell/* (tags have no v)
agent-sandbox/agent-sandbox-controller:v1.0.3registry.k8s.io
base/ubuntu:24.04nvcr.io/nvidia/base/ubuntu, pinned by digest
workload:0.1.6built here: the base plus the AWS CLI 2.39.3, boto3 1.43.123, git, gh, curl, jq

It took 2 minutes 34 seconds. When I deleted one tag, --verify-only exited 1 and named the missing image. A re-run copied only that image, and the workload image rebuilt from cache to the same digest.

Step 4: Agent Sandbox and the chart

05_install.sh installs Agent Sandbox v1.0.3 with its image rewritten to ECR. It then labels namespace openshell with Pod Security restricted, creates the NLB Service (so its hostname exists), and runs Helm with this values file:

global:
  image:
    registry: ${ECR_REGISTRY}/${ECR_PREFIX}
    tag: "0.1.6"
sandbox:
  image:
    repository: ${ECR_REGISTRY}/${ECR_PREFIX}/workload
    tag: "0.1.6"
server:
  telemetryEnabled: false
  drivers:
    kubernetes:
      allowDriverConfig: true   # lets a sandbox pick a RuntimeClass
resources:
  requests:
    cpu: 250m
    memory: 512Mi

The script passes the NLB hostname with --set-json pkiInitJob.serverDnsNames=[...], starts a port-forward on 127.0.0.1:17680, registers the CLI and then writes the client bundle.

Testing

Test 1: The cluster meets the bar

01_cluster.sh ends with checks, and every one passed:

ok    Kubernetes v1.37.1-eks-577a51e
ok    ip-10-70-10-204.ec2.internal: crw-rw-rw-. 1 root 36 10, 232 ... /host/dev/kvm (vmx on 8 vCPUs)
ok    ip-10-70-10-204.ec2.internal: kernel 6.18.56-125.164.amzn2023.x86_64, Landlock active (ABI 7),
      LSMs lockdown,capability,landlock,yama,safesetid,selinux,bpf,ima
ok    i-0da86c3ab4ddf08b1 (m7i.2xlarge): HttpTokens required, HttpPutResponseHopLimit 1, NestedVirtualization enabled
ok    VPC CNI network policy enforcement on (aws-node 2/2 ready, aws-eks-nodeagent present)
ok    eks-pod-identity-agent DaemonSet 2/2 ready
ok    aws-load-balancer-controller 2/2 ready, credentials from EKS Pod Identity
ok    default StorageClass ebs-csi-default-sc (ebs.csi.aws.com, ...)

Gate 1 passed. Landlock is in the active LSM list of the EKS AL2023 kernel, at ABI 7; OpenShell needs 3. The node check reads the ABI with a direct landlock_create_ruleset call from a debug pod. A second run of the script reported Terraform “No changes”.

Test 2: The fence before OpenShell

OpenShell’s Kubernetes driver puts every workload Pod behind a NetworkPolicy with no egress except to its supervisor, and trusts the CNI to enforce it (#3862 asks for a check). So before installing OpenShell, 04_fence.sh tested what the VPC CNI enforces, with plain pods and the same kind of policy:

CheckResult
Pod under deny-all egress vs no policyDeny-all: internet and cluster DNS blocked. No policy: both open
New pods in standard mode, 10 sharing a name prefix1 of 10 got requests out before the policy applied (10 of its 40 requests; each is a curl with a 1 s timeout, then a 100 ms pause)
New pods in standard mode, 10 with unique name prefixes10 of 10 got 9 or 10 requests out first
Fenced pod to its own node’s IP, port 18080Open
Fenced pod to the other node’s IPBlocked
Strict mode (NETWORK_POLICY_ENFORCING_MODE=strict)0 of 10 leaked; a new pod no policy selects starts with all traffic denied

Three things to take from that:

  • There’s a startup gap of a second or a little more in the default mode. The network policy agent programs a new pod’s rules after the pod has started. The VPC CNI agent shares those rules between pods whose names match up to the last -. So only a pod with a new prefix opens a gap: here that was the first of ten sharing a prefix, and all ten with unique prefixes. OpenShell names workload Pods <workspace>--<sandbox>, so sandboxes with hyphen-free names in workspace default all share the prefix default-, while a name like warm-1 gets its own prefix, and its own gap.
  • The node’s own IP is always reachable. That’s deliberate in the VPC CNI’s network policy agent: its source adds the node’s IP to every allow list so that kubelet probes work. A fenced pod can reach every port on its own node.
  • Strict mode closes the gap, but any pod no policy selects then starts with all traffic denied, system pods like CoreDNS included (host-network pods are exempt), so the script turned it on, measured, and put it back. Each switch took 23 seconds.

How much does the gap matter for OpenShell? Less than for a plain pod. OpenShell holds the agent’s launch until the supervisor confirms, and the agent’s sockets go through the in-pod broker either way. The NetworkPolicy is the second layer, not the first. But it’s the layer that catches anything the broker misses, so I’d run strict mode on a dedicated sandbox cluster.

Test 3: Agent Sandbox and the preflight

Installing the chart before Agent Sandbox fails before anything is created:

Error: execution error at (helm-chart/templates/agent-sandbox-preflight.yaml:9:4): Agent Sandbox is required
but neither agents.x-k8s.io/v1beta1 nor agents.x-k8s.io/v1alpha1 is served

With the v1.0.3 controller installed (CRD v1beta1), the install took 31 seconds. The NLB had a hostname 6 seconds after its Service was created.

Test 4: The gateway

openshell-0                  1/1   Running
openshell-data-openshell-0   Bound   1Gi   RWO   ebs-csi-default-sc
ok    gateway env OPENSHELL_TELEMETRY_ENABLED=false
ok    server certificate lists the NLB hostname
  • Pod Security restricted held. The gateway and the certificate hook were admitted under restricted, and so were all sandbox Pods (test 6).
  • The data survives. Deleting openshell-0 brought it back on the same volume.
  • A re-run converges. It produced Helm revision 2 with identical manifests, and the certificate hook skipped because its three Secrets existed.
  • Authentication is per call, not per connection. A TLS client without a certificate completes the handshake, and the first API call returns gRPC status 16, missing authorization header. With the client certificate from the cluster, the CLI reports Authenticated (mTLS transport).

The CLI registration hit the only real snag of the install. The docs register a Kubernetes gateway with openshell gateway add https://127.0.0.1:8080 --local. My laptop also runs the local gateway from part 1, and --local (the help text says “a local mTLS gateway running in Docker on this machine”) filled the new registration with that gateway’s certificates:

Error:   × client error (Connect)
  ╰─▶ invalid peer certificate: BadSignature

The fix is to register first, then write the cluster’s client bundle from Secret openshell-client-tls into ~/.config/openshell/gateways/osh-eks/mtls/. Port 8080 was taken on my laptop, so the port-forward uses 17680.

Test 5: The CLI through an NLB

The chart’s Service can’t take load balancer annotations, so manifests/gateway-nlb.yaml is a Service of our own: internet-facing, IP targets, TCP passthrough on port 8080 only, loadBalancerSourceRanges set to my IP. TLS passes through, so the client certificate reaches the gateway:

Gateway: osh-eks-nlb
Server: https://k8s-openshel-openshel-....elb.us-east-1.amazonaws.com:8080
Status: Connected
Authentication: Authenticated (mTLS transport)
metrics port 9090 through the NLB: 000 (not exposed)

When I set the source range to 203.0.113.7, the CLI was locked out and the script stopped with an error, though not instantly: even after the security group showed only the new address, one more status call got through before the next call was refused. A re-run put my IP back. My first version of that test passed by mistake because it ran before the AWS Load Balancer Controller had updated the security group, so the script now waits for the rule before testing. After the test, 06_remote.sh --delete removed the NLB.

Test 6: First sandboxes on EKS

Gate 2 passed. Sandboxes reach Ready, so the VPC CNI admits the workload-to-supervisor path. One sandbox is nine Kubernetes objects:

sandboxes.agents.x-k8s.io    1  default--first
pods                         2  default--first (workload), os-supervisor-<id> (supervisor)
services                     2  os-boundary-<id>, os-supervisor-<id>
networkpolicies              1  os-control-<id>
secrets                      2  os-sandbox-<id>-..., os-supervisor-<id>-...
persistentvolumeclaims       1  workspace-default--first

Plus three policies for the whole namespace: the two fences (workloads get no egress, supervisors get open egress) and the chart’s SSH ingress rule. Inside the workload:

$ id
uid=10001 gid=10001 groups=10001
$ sh -c "grep -E '^(Seccomp|NoNewPrivs|CapEff)' /proc/self/status"
CapEff:	0000000000000000
NoNewPrivs:	1
Seccomp:	2
Seccomp_filters:	6
$ sh -c "env | grep -ciE 'proxy|token'"
0

The driver runs the workload as UID 10001 whatever the image’s USER says, with no capabilities and a seccomp filter, and there’s no proxy or token variable to steal.

Timings, create call to Ready:

CreateTime
First, image removed from both nodes but its layers still cached (pull 81 ms)16.8 s
Warm8.3 s
Five more warm, median (fastest, slowest)10.4 s (8.9 s, 10.7 s)
Delete, per sandboxabout 31 s

The image isn’t what costs time: the kubelet reported pulling the 175 MB workload image in 81 ms, because its layers were still on the node. The events for one warm create show where the ten seconds go:

  • the EBS volume: about 2 s to provision and 2 to 3 s to attach,
  • three containers starting: about 2 s,
  • the supervisor session.

Locally on Docker the same create took 2.5 seconds. Deleting is slower still, because DeleteSandbox returns only once the Pods and the volume are gone. That’s just past the Python SDK’s default 30-second RPC timeout, so the companion code raises it to 120.

Test 7: Kata

kata-deploy 3.32.0 installed in 66 seconds (a 1.6 GB pull per node). A plain Kata pod started in 3 seconds with its own guest kernel, 6.18.35 against the node’s 6.18.56. OpenShell picks a runtime per sandbox, once the RuntimeClass carries an approval label:

kubectl label runtimeclass kata-qemu openshell.ai/sandbox-attachable=true
openshell sandbox create --name kata1 \
  --driver-config-json '{"kubernetes":{"pod":{"runtime_class_name":"kata-qemu"}}}'

Without the label, creation is refused at once: RuntimeClass 'kata-qemu': external resource not admitted by required labels.

With the label, gate 3 failed. The workload Pod got its Kata VM, and the first init container finished inside it. The second, workspace-init, runs openshell-sandbox seed-workspace as UID 10001 to fill the workspace volume. About 50 seconds after the create, it ended with exit code 255 and no output, and the Pod went to Failed. Kata’s runtime on the node, which logs through containerd, reported this in the previous five minutes:

3 Dead agent
1 QEMU exited with an error

Dead agent means the host had lost contact with the Kata agent, the process inside the VM that runs the containers. QEMU exited with an error (in full, signal: killed) means the VM’s process was killed. So the VM went down. These lines don’t show why, or which came first: they have no timestamps, and Kata also kills QEMU when it tears down a failed Pod. I didn’t capture the guest’s own logs, so the cause is still open. Kata isn’t in OpenShell’s CI yet either (#4459, open).

The sandbox itself stayed in Provisioning (reason ConfigurationPending) until the 300-second provisioning timeout. It failed the same way with --cpu 2 --memory 4Gi. A plain Kata pod with the same UID, the same fsGroup and an EBS volume wrote to the volume without trouble, so the failure is specific to OpenShell’s workload Pod under Kata, not to Kata, the UID or the volume on their own. I didn’t try a bare-metal node: Kata itself works on these nested-virtualization nodes, so a different host seemed unlikely to help.

So the rest of this series runs OpenShell sandboxes on runc, which shares the node’s kernel. Part 3 explains what that means.

Results Summary

Result
Gate 1: Landlock on EKS 1.37 AL2023Passed (kernel 6.18.56, ABI 7)
Gate 2: workload-to-supervisor path under VPC CNIPassed
Gate 3: OpenShell sandbox in a Kata VMFailed: workspace seeding fails inside the VM and the VM goes down; the cause is still open (plain Kata pods work)
Cluster build13 min 30 s
Images to ECR2 min 34 s (6 images)
OpenShell install31 s for the chart, NLB hostname in 6 s
Sandbox create8.3 s to 16.8 s (median of five warm: 10.4 s)
Sandbox deleteabout 31 s each
Objects per sandbox9, plus 3 namespace-wide policies
VPC CNI startup gapa second or a little more for a pod with a new name prefix; closed by strict mode
Re-runsScripts 01 to 06 converged on a second run

Pricing

Idle cost of what this builds, in us-east-1 (AWS price list, October 2026):

ItemPricePer hourPer day
EKS control plane (standard support)$0.10/h$0.100$2.40
2 × m7i.2xlarge on demand (nested virtualization costs nothing extra)$0.4032/h each$0.806$19.35
NAT gateway (+ $0.045/GB processed) and its public IPv4$0.045/h + $0.005/h$0.050$1.20
EBS gp3 root volumes, 2 × 100 GiB, and small change (ECR, the gateway’s 1 GiB)$0.08/GB-month≈ $0.023≈ $0.55
Total idle≈ $0.98≈ $23.50
NLB while it exists (2 AZs, idle)$0.0225/h + IPv4≈ $0.033≈ $0.78
Each running sandbox’s 2 GiB gp3 volume$0.08/GB-month≈ $0.0002≈ $0.005

The nodes are 80% of it. 09_scale.sh down takes them to zero between sessions and leaves about $0.15 an hour.

Cleanup

./09_scale.sh down     # between sessions: zero nodes
./09_scale.sh up       # two nodes, gateway, port-forward
./99_teardown.sh       # everything, when you're done with the series

99_teardown.sh deletes the sandboxes and the NLB, and uninstalls the three Helm releases and Agent Sandbox. It then removes what the chart leaves behind on uninstall: its TLS, JWT and key-encryption Secrets, the gateway’s volume and any workspace volumes. Last come every openshell-eks/* ECR repository, terraform destroy, and the Container Insights log group from part 6, which its agent creates outside Terraform. It finishes by listing any EBS volume, load balancer, ECR repository, osh-eks IAM role or bucket, or Container Insights log group left behind, and fails if there is one. If you ran part 5, destroy its Terraform first: 99_teardown.sh stops before deleting anything while part 5’s roles and bucket still exist.

Verdict

OpenShell installs cleanly on EKS once you know the six or so things the chart assumes and EKS doesn’t provide: an enforcing CNI, the Agent Sandbox controller, a default StorageClass, images you can pull reliably, a certificate name for remote access, and a CLI registration that doesn’t borrow a local gateway’s certificates. Nothing OpenShell installs runs privileged (kata-deploy does, by design), everything in namespace openshell passed Pod Security restricted, and authentication is enforced on every call. For an alpha-exit release that the project doesn’t test on EKS, that’s a good result.

Three things deserve attention before you rely on it:

  • The fence depends on a setting that is off by default. Without enableNetworkPolicy, the policies OpenShell creates are accepted and ignored. With it, a pod with a new name prefix still gets a startup gap of a second or so, and a fenced pod can reach its own node; strict mode on a dedicated cluster closes the gap.
  • Each sandbox costs an EBS volume. That’s about 10 seconds to create, 30 seconds to delete, and one of the per-node volume attachments. Part 6 measures what that does to density.
  • VM isolation isn’t there yet. Kata runs on nested-virtualization M7i nodes, but with an OpenShell 0.1.6 sandbox under Kata, workspace seeding fails inside the VM and the VM goes down; the cause is still open. If you need a separate kernel per agent today, this release on this setup doesn’t give you one.

Compared with running it on a laptop, EKS gives OpenShell what the Docker driver can’t: Pod Security admission, per-sandbox NetworkPolicies, persistent workspaces and a gateway on a real volume. It also gives you more to get right. The next post, part 3, follows a sandbox’s life on this cluster, and what isolation you get with runc alone.

Comments