Agent Substrate Day

Running Agent Substrate
on EKS

What it takes on AWS, which services to use,
and where AWS can help the project

Carlos Santana
Carlos Santana
Sr. Specialist Solutions Architect, Amazon Web Services
linkedin.com/in/csantanapr

Agenda

  1. Agent Substrate and its architecture on AWS
  2. Cluster, gVisor, microVM, mode 3 on AWS Nitro Sandbox
  3. Snapshots on S3, IAM roles, state on RDS
  4. Secrets, Bedrock and per-actor volumes
  5. Two demos, lessons, production checklist

Everything shown runs on

Agent Substrate v1.0.0EKS 1.37Amazon Linux 2023 m8i (nested virt)S3RDS for PostgreSQL 18 Secrets ManagerECRBedrock EFSS3 Files

Every Service is ClusterIP: no public endpoints

Thousands of idle agents share a small pool of worker pods

Actors (agents): thousands, mostly idle agentagentagentagent agentagentagentagent … Workers: a small pool of ready pods worker · agentworker · agentworker · freeworker · free placed on demand Object storage: suspended actors' memory, rootfs and durable volumes resume on the next request, on any worker with a compatible runtime suspend resume Control plane ate-api-server · ate-controller atelet (per node) atenet router + egress PostgreSQL for state

One EKS cluster, three node groups, managed AWS services around it

Amazon EKS 1.37 · private subnets system · m8i.xlarge ate-api-server ×2 ate-controller · ate-idp-server atenet-router · atenet-egress credential providers LLM gateway (agentgateway) External Secrets · Reloader gvisor · m8i.2xlarge taint sandboxClass=gvisor ateworker-gvisor pods runsc, userspace kernel atelet DaemonSet microvm · m8i.2xlarge taint sandboxClass=microvm nested virtualization on ateworker-microvm pods Kata + Cloud Hypervisor atelet DaemonSet EKS Pod Identity agent · VPC CNI with NetworkPolicy · EBS CSI · EFS CSI PodCertificateRequest and ClusterTrustBundle are GA in 1.37: no feature flags Actors have no external egress by default: only through atenet-egress + EgressPolicy. ateworker blocks node link-local ranges, so actors cannot reach IMDS. Amazon S3snapshots · microVM guest assets Amazon RDS for PostgreSQLactor + template state · TLS verify-full AWS Secrets Manageractor secrets · DB logins Amazon ECRactor images, pulled by atelet Amazon Bedrockinference via the in-cluster LLM gateway Amazon EFS · S3 Filesper-actor volumes · EFS CSI driver

Everything runs in private subnets, with no public application endpoints

Agent Substrate on Amazon EKS in us-west-2: a VPC with public and private subnets; in the private subnets the EKS cluster with system, gVisor and microVM node groups running ate-api-server, ate-controller, ate-idp-server, atenet-router, atenet-egress, External Secrets with the Reloader, OpenClaw, efs-csi-controller-ate, agentgateway, atelet and worker pods; plus RDS for PostgreSQL and the rotation Lambdas; with Amazon S3, S3 Files, EFS, EBS, ECR, Secrets Manager, Bedrock, IAM with Pod Identity, CloudFormation and CloudWatch Logs.

Every Substrate dependency maps to an AWS service

Agent Substrate needsOn AWSStatus in v1.0.0
PodCertificate + ClusterTrustBundleEKS 1.37 (both GA)works, no flags
Snapshot object storeAmazon S3works
microVM guest assetsS3 asset bucketworks; small patch for S3
PostgreSQLAmazon RDS for PostgreSQLworks
Workload identityEKS Pod Identityworks
Actor image pullsAmazon ECRworks
Nested virt for microVMIntel 7th/8th gen (m8i here)works
Per-actor volumes (CSI)Amazon EFS and S3 Filesworks; ExternalVolumes preview
Egress credential injectionAWS Secrets Managerworks
MetricsAmazon Managed Service for Prometheusships a GKE PodMonitoring

One node group per sandbox class, network policy on eksctl · EKS 1.37 · AL2023

apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata: { name: substrate-day,
            region: us-west-2, version: "1.37" }
addons:
  - name: vpc-cni  # + enableNetworkPolicy: "true"
  - name: eks-pod-identity-agent
  - name: aws-ebs-csi-driver
managedNodeGroups:
  - name: system   # control plane components
    instanceType: m8i.xlarge
    disablePodIMDS: true   # IMDSv2, hop limit 1
  - name: gvisor
    instanceType: m8i.2xlarge
    disablePodIMDS: true
    taints:
      - { key: ate.dev/sandboxClass, value: gvisor,
          effect: NoSchedule }
  - name: microvm
    instanceTypes: [m8i.2xlarge]
    launchTemplate: { id: lt-… }  # nested virt, hop limit 1
    taints:
      - { key: ate.dev/sandboxClass, value: microvm,
          effect: NoSchedule }
  • One tainted node group per sandbox class
  • Turn on VPC CNI network policy
  • mTLS needs no feature flags in 1.37
  • Private subnets for nodes and data stores

Two sandbox classes, one cluster

gVisor · soft isolation

  • Userspace kernel intercepts syscalls
  • Runs on any node, no special hardware
  • Native checkpoint/restore of the process tree
  • Class gvisor; default SandboxConfig gvisor-default

microVM · VM isolation

  • Kata guest on Cloud Hypervisor, its own guest kernel
  • Needs /dev/kvm; atelet advertises it as ate.dev/kvm
  • Memory snapshot, restored on demand with userfaultfd
  • Class: microvm, guest assets from S3
actorthe agent's own processes gVisor userspace kernelrunsc · intercepts the actor's syscalls isolation boundary host Linux kernelAmazon Linux 2023 EC2 instancem8i.2xlarge · no special hardware actorthe agent's own processes guest kernelthe actor's own Linux kernel (Kata) isolation boundary Cloud HypervisorVMM on KVM, through /dev/kvm host Linux kernelAmazon Linux 2023 EC2 instancem8i.2xlarge · nested virtualization on

Same ActorTemplate shape for both. Pick per workload with sandboxClasses on the WorkerPool.

gVisor, microVM, and a 3rd mode on AWS Nitro Sandbox

The installer's opening animation: the Agent Substrate mascot, the title, and actors dropping onto gVisor, microVM and AWS Nitro Sandbox nodes in an Amazon EKS cluster

An interactive installer on top of the same scripts

A loop through the installer's six screens: Welcome, Check your setup, Settings with the demos and plugins checked, Review with the config file and the commands in order, Provision with its live checklist and log at three points in the run, and Done with what was created and the step times

What it adds

  • Checks first: tools, account, Region, vCPU quota, nested virtualization
  • Pick the cluster name, Region, node groups, demos and plugins
  • Live checklist from the scripts' own steps
  • Teardown from the Done screen
  • Same create-cluster.sh and deploy.sh underneath

A third mode: each worker Pod is an AWS Nitro Sandbox upstream proposal

1 · gVisorv1.0.0 EC2 nodegVisor node group atelet Unix socket shared node files worker Pod regular Pod gVisor sandbox runsc userspace kernel actor the agent's own processes soft isolation: a userspace kernel 2 · microVMv1.0.0 EC2 nodenested virtualization on atelet Unix socket shared node files worker Pod regular Pod with /dev/kvm microVM Cloud Hypervisor, its own guest kernel actor the agent's own processes VM isolation: its own guest kernel 3 · AWS Nitro Sandbox EC2 nodelaunched by EKS Auto Mode atelet mutual TLS, port 8087 over the Pod network worker Pod is a Nitro sandbox RuntimeClass isolated-sandbox actor the agent's own processes hardware isolation: AWS Nitro

Mode 3 runs on images from the proposal branch, ghcr.io/csantanapr/substrate. The installer turns it on.

Mode 3 on AWS Nitro Sandbox: hardware isolation per worker Pod

1 · worker Pod, Pending runtimeClassName: isolated-sandbox nodeSelector ate.dev/pool=isolated 2 GiB per worker 2 · EKS Auto Mode built-in Karpenter NodePool ate-isolated picks the instance and launches it no node group to manage 3 · EC2 nodelaunched for these Pods atelet mutual TLS, port 8087 over the Pod network worker Pod is a Nitro sandbox hardware isolation actor the agent's own processes worker Pod is a Nitro sandbox hardware isolation actor the agent's own processes hardware isolation for every worker Pod
apiVersion: ate.dev/v1
kind: WorkerPool
spec:
  networkTransport:
    runtimeClassName: isolated-sandbox  # from EKS Auto Mode

Why it matters

  • Hardware isolation per Pod: each worker Pod is its own AWS Nitro Sandbox
  • A Pending Pod is the request: Auto Mode launches an instance that fits
  • VPC networking, Pod Identity and network policies work as for any Pod
  • Try it: the installer's isolated-sandbox Mode 3 plugin

Nested virtualization runs microVMs without bare metal

  • Recent Intel families in us-west-2:
    c7im7ir7ic8im8ir8ii7ix8i
  • Turned on per instance with CpuOptions
  • No bare metal needed for microVMs
  • Check a node for /dev/kvm and vmx
# launch template data for the microvm node group
{
  "CpuOptions": { "NestedVirtualization": "enabled" },
  "MetadataOptions": {
    "HttpTokens": "required",
    "HttpPutResponseHopLimit": 1 },
  "BlockDeviceMappings": [{ "Ebs": {
    "VolumeSize": 200, "Encrypted": true } }]
}

# find instance types that support it
aws ec2 describe-instance-types --filters \
  Name=processor-info.supported-features,\
Values=nested-virtualization

# on a microvm node
crw-rw-rw- 1 root kvm 10, 232 /dev/kvm
kernel 6.18.56-125.164.amzn2023.x86_64

Suspended actors live in S3, moved by a sidecar

  • objectstore-plugin sidecar moves every snapshot
  • ATE_STORAGE_BACKEND=s3 with Pod Identity
  • Read-only asset bucket for microVM assets

Bucket best practices

Block Public AccessBucketOwnerEnforceddefault encryptionTLS-only bucket policyabort incomplete uploads

# a golden (tag) snapshot in S3
atespaces/ate-demo-counter/tags/<uid>/
  manifest.json
  pages.img.zstd          # process memory
  pages_meta.img.zstd
  fs/pages.img.zstd       # durable volumes
  rootfs/fscheckpoint.pb.zstd
  rootfs/multitar.img.zstd
  rootfs/pages.img.zstd   # root filesystem writes

# asset bucket (read-only for atelet)
microvm-assets/amd64/<sha256>   # cloud-hypervisor
microvm-assets/amd64/<sha256>   # virtiofsd
microvm-assets/amd64/<sha256>   # guest kernel
microvm-assets/amd64/<sha256>   # guest rootfs

Identity: EKS Pod Identity, one role per ServiceAccount

# trust policy, scoped to this account and cluster
{ "Principal": { "Service": "pods.eks.amazonaws.com" },
  "Action": ["sts:AssumeRole", "sts:TagSession"],
  "Condition": {
    "StringEquals": { "aws:SourceAccount": "<account>" },
    "ArnLike": { "aws:SourceArn": "arn:aws:eks:…:cluster/substrate-day" } } }

aws eks create-pod-identity-association \
  --namespace ate-system --service-account atelet \
  --role-arn arn:aws:iam::<account>:role/substrate-day-us-west-2-atelet

Lesson: atelet pulls actor images with its own role

ECR pull goes on atelet's Pod Identity role, not the node role

ServiceAccountAllowed
ate-system/ate-api-serversnapshot bucket: get, put, delete, list
ate-system/ateletsnapshot bucket: get, put (no delete) · asset bucket read · ECR pull
llm-gateway/llm-gatewaybedrock:InvokeModel* on its pinned inference profiles
actorsnothing: no role, no keys, no IMDS

Each component gets only the S3 actions it needs

ate-api-server Deployment · system nodes sidecar: objectstore-plugin control copies a golden snapshot for each tag deletes snapshots nothing refers to atelet DaemonSet · every worker node sidecar: objectstore-plugin node snapshots: uploads on suspend, fetches on resume pulls actor images fetches microVM assets, checks sha256 ate-setup (operator) stages microVM assets at install time actors no role · no keys · no IMDS S3 snapshot bucket atespaces/<a>/tags/… atespaces/<a>/actors/<uid>/… ECR private actor images S3 asset bucket microvm-assets/amd64/<sha256> List · Get · Put · Delete Get · Put · no Delete pull Get Put, once at install A worker node can't delete snapshots. Only the control plane garbage collects.

Actor state lives in RDS: private, TLS verify-full, two logins

  • Private subnets; 5432 only from the cluster
  • Two logins: owner and readwrite
  • sslmode=verify-full against the RDS CA bundle
  • Connection strings as env vars, never flags
# install: logins read from Secrets Manager
export ATE_API_POSTGRES_OWNER_CONNECTION_STRING=\
"postgres://substrate_owner_user:…@<rds>:5432/atepg\
?sslmode=verify-full\
&sslrootcert=/run/postgres-server-ca/server-ca.pem"
export ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING=…

ate-setup deploy ate-system \
  --image-repo ghcr.io/agent-substrate/substrate \
  --image-tag v1.0.0 \
  --snapshot-location s3://<snapshots>/ \
  --ateapi-postgres-server-ca-file global-bundle.pem \
  --sandbox-classes gvisor,microvm

Rotate the DB password, and ate-api-server follows

Secrets Manager rotation, alternating users: the old login works until the next rotation Rotation function AWS-hosted Lambda private subnets sets user or user_clone External Secrets Pod Identity refresh 1m templates both DSNs Kubernetes Secret ate-api-server-secret-envvars …READ_WRITE_CONNECTION_STRING …OWNER_CONNECTION_STRING ESO Reloader sees the Secret change, starts a rolling restart ate-api-server reads the DSNs from env at start: new pods use the new password Amazon RDS verify-full TLS read write change rolling restart envFrom connects, TLS every 30 days CREATE ROLE / ALTER USER as the master login

Rotation wiring, with no change to the Substrate manifests

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: ate-db, namespace: ate-system }
spec:
  refreshInterval: 1m
  secretStoreRef: { kind: SecretStore, name: aws }  # Pod Identity
  target:
    name: ate-api-server-secret-envvars  # what ate-setup made
    creationPolicy: Merge               # keep its other keys
    template:
      data:
        ATE_API_POSTGRES_READ_WRITE_CONNECTION_STRING: >-
          postgres://{{ .user }}:{{ .pw | urlquery }}@<rds>:5432/…
  data:
  - secretKey: user
    remoteRef: { key: <cluster>/ate-readwrite, property: username }
  - secretKey: pw
    remoteRef: { key: <cluster>/ate-readwrite, property: password }
  # the owner login: the same, with ate-owner
---
apiVersion: reloader.external-secrets.io/v1alpha1
kind: Config
spec:
  notificationSources: [{ type: KubernetesSecret }]
  destinationsToWatch:
  - { type: Deployment, deployment: { names: [ate-api-server] } }

Rotation

  • AWS-hosted PostgreSQLMultiUser Lambda
  • Every 30 days, or rotate-secret now

External Secrets Operator

  • Templates both DSNs
  • Controller only: nothing listening

ESO Reloader

  • Rolls ate-api-server on Secret change

Actors use API keys they never hold

Actor gVisor or microVM holds only a placeholder: the secret is never in its memory, files or snapshots atenet-egress terminates TLS matches the EgressPolicy rule replaces the header the only way out for actors External API GitHub, a SaaS API, any HTTPS host sees the real secret aws-sm-credential-provider mTLS: admits only atenet-egress atespace policy, default deny 30s cache for AWSCURRENT ate-system · plugin, own repo AWS Secrets Manager GetSecretValue with the provider's Pod Identity role only secrets tagged ate.dev/injectable=true 1 1 Authorization: Bearer placeholder 2 2 FetchSecret(uri, actor ID) 3 4 3 GetSecretValue 4 the value, to the gateway only 5 Authorization: Bearer <secret>

Actor secrets: the wiring and its three gates

# EgressPolicy on the actor
rules:
- https:
    hostnames: ["api.github.com"]
    effects:
      replaceHeaders:
      - header: Authorization
        prefix: "Bearer "
        credentialUri: ate-secret://secretsmanager.amazonaws.com/
          accounts/<id>/regions/us-west-2/
          secrets/ate/team-a/github/stages/AWSCURRENT
---
# provider policy: atespace -> secrets, default deny
policies:
- atespace: team-a
  allowedSecrets:
  - { account: "<id>", region: us-west-2, namePrefix: ate/team-a/ }

Three gates

  • mTLS: only atenet-egress may ask
  • Provider policy per atespace
  • IAM: ate/*, tag ate.dev/injectable

On EKS

  • Pod Identity role, no keys

Rotation

  • AWSCURRENT cached 30s, no redeploy

Actors reach Bedrock through a gateway, with no AWS credentials

Actor Anthropic or OpenAI SDK no AWS credentials atenet-egress EgressPolicy: one host placeholder → actor JWT LLM gateway agentgateway · ClusterIP SigV4 with Pod Identity Bedrock only the pinned profiles allowed model "claude" (default) → us.anthropic.claude-haiku-5-5 "claude-haiku" · "claude-sonnet" · "claude-opus" → newest active profile per family, read at deploy IAM: InvokeModel on each profile ARN, and on its foundation models only with bedrock:InferenceProfileArn = that profile

Lesson: block IMDS for pods

Set disablePodIMDS: true (hop limit 1) on every node group, so the gateway signs with its own role, never the node instance role.

Why a gateway, not header injection

SigV4 signs each request's body hash, so signing lives in a proxy that holds the role.

Per-actor EFS volumes follow the actor to any zone CSI · ExternalVolumes preview

ate-api-server first resume: CreateVolume delete actor: DeleteVolume EFS CSI controller Envoy: mTLS, accepts only ate-api-server's SPIFFE ID same image as the EKS add-on gRPC Amazon EFS one access point per actor /ate/<volume>, uid of the actor's user mount target in every AZ or S3 Files: the same driver, files land in an S3 bucket CreateAccessPoint Pod Identity, scoped to this file system node A · us-west-2c atelet NodePublishVolume on resume EFS node plugin NFS over TLS into /var/lib/ate actor (gVisor or microVM) sees the access point at its mount path suspend: snapshot to S3, volume unmounted node B · us-west-2b same access point, mounted here the mount target in node B's zone actor resumes from its S3 snapshot its files are where it left them resume elsewhere

EFS wiring, and an actor moving between zones

# StorageClass: one access point per actor
provisioner: efs.csi.aws.com
parameters:
  provisioningMode: efs-ap    # s3files-ap for S3 Files
  fileSystemId: fs-…
  directoryPerms: "700"
  uid: "<uid>"              # EFS_UID: the actor's user
  gid: "<gid>"
  basePath: /ate
---
# CSIDriverConfig: ate-api-server finds the controller
driverName: efs.csi.aws.com
controllerEndpoint:
  tcp://efs-csi-controller-ate.kube-system.svc…:50052
tls:
  enabled: true
  usePodIdentity: true
  serverName: efs-csi-controller-ate.kube-system.svc
---
# ActorTemplate
volumes:
- name: data
  external: { template: { capacityBytes: 1073741824,
                          storageClassName: efs-ate } }
containers:
- volumeMounts: [{ name: data, mountPath: /home/counter }]
StorageSandboxZones
EFSgVisorus-west-2c → us-west-2b
EFSmicroVMus-west-2a → us-west-2b
S3 FilesgVisorus-west-2b → us-west-2c
S3 FilesmicroVMus-west-2b → us-west-2a

A captured run: same access point, new zone

Best practices

  • Give the access point the actor's uid
  • Run actors as non-root

Demo: suspend frees the worker, and every count keeps going gVisor and microVM

Watch for

  1. Counts in memory, a file and rootfs: 3, 3, 3
  2. Suspend: SUSPENDED, the worker is free
  3. Next request resumes it: all three read 4
kubectl ate create actor demo-gvisor \
  -a ate-demo-counter --template counter
kubectl ate proxy actor demo-gvisor \
  -a ate-demo-counter 8001:80 &
curl localhost:8001
kubectl ate suspend actor demo-gvisor -a ate-demo-counter
curl localhost:8001     # resumes
== demo-gvisor (gVisor) memory: 3 | file: 3 | rootfs: 3 $ kubectl ate suspend actor demo-gvisor \ -a ate-demo-counter state: ACTOR_STATE_SUSPENDED memory: 4 | file: 4 | rootfs: 4 state: ACTOR_STATE_RUNNING == demo-microvm (Kata + Cloud Hypervisor) memory: 3 | file: 3 | rootfs: 3 $ kubectl ate suspend actor demo-microvm \ -a ate-demo-counter-microvm state: ACTOR_STATE_SUSPENDED memory: 4 | file: 4 | rootfs: 4 state: ACTOR_STATE_RUNNING

Condensed from a run of this demo on the cluster above

Demo: an always-on agent that sleeps between messages OpenClaw

  • An always-on gateway holds the chat channel
  • Each agent is a suspendable actor
  • It thinks with Bedrock and holds no keys

Watch for

  1. The actor answers, then suspends to S3
  2. The next message wakes it from its snapshot
  3. A new conversation gets a new actor

Flow

chat message → gateway → atenet-router → actor →
atenet-egress → LLM gateway → Bedrock → reply

actor oc-agent and its egress policy exist actor answers /readyz through atenet-router actor said: Pong, here I am ready to help whenever you need me. actor suspended, snapshot in S3 actor resumed on demand from its snapshot actor said (via gateway): Pong, right on time. gateway created a new actor + egress policy and woke it

Condensed from a run of this demo on the cluster above

What works on EKS today, and the rough edges

Works with v1.0.0

  • Published images on stock EKS 1.37 and AL2023
  • gVisor and microVM suspend and resume
  • S3, RDS, ECR and Bedrock with Pod Identity
  • Actor secrets injected from Secrets Manager
  • Per-actor EFS and S3 Files volumes (preview)

Rough edges

  • ate-setup assumes GKE for any real cluster
  • S3 uses the pod's role, not per-actor tokens
  • RDS needs a role grant before setup.sql
  • DB password rotation needs a rolling restart
  • EFS needs a second CSI controller

Where AWS can help the project next

Neutral install seams

  • ate-setup deploy --component: one piece at a time
  • ate-setup render: manifests to your own tooling

Per-actor AWS access

  • Per-actor S3 credentials for snapshot transfers
  • Actor JWT to AWS token exchange

Plugins and drivers

  • Secrets Manager provider in the plugins repo
  • Password file for the database logins
  • EFS CSI endpoint Substrate can call

Production checklist for EKS

Cluster

  • EKS 1.37+
  • Node group per sandbox class, tainted
  • Nested-virt Intel (m8i, c8i, r8i, 7i) for microVM
  • VPC CNI network policy on, IMDSv2 hop limit 1

Data

  • S3: private, TLS-only, encrypted, lifecycle
  • Separate read-only asset bucket
  • RDS private, verify-full, owner/readwrite split
  • DB logins rotated, synced, rolled out

Identity & access

  • Pod Identity role per ServiceAccount
  • No credentials in actors
  • --ateapi-authz-bootstrap-owners: your operators
  • ClusterIP only, private EKS API endpoint

Thank you

Agent Substrate runs on EKS today.
Help us make it boring:
plugins, neutral install seams, per-actor credentials.

github.com/agent-substrate/substrateagent-substrate-plugins

Carlos Santana
Carlos Santana
linkedin.com/in/csantanapr
QR code for github.com/csantanapr/eks-agent-substrate
Scripts, demos and slides
github.com/csantanapr/eks-agent-substrate
Running Agent Substrate on EKS