MastertheMesh
sovereignty · EKS · istio ambient · agentgateway · kagent · vault · kyverno
Architecture Part 1 · The infrastructure

Secure Sovereign AI on EKS, Part 1: Zero Trust Infrastructure

TO
Tom O'Rourke
EMEA Field CTO · Solo.io

An open-weight European model, self-hosted so your data stays in-house, and then governed on every side so it stays there. The weights are open; the asset is the data, and every prompt and output that flows through the model, which never leaves eu-west-2. Running the model is the easy part; the architecture that keeps the data in is the point. This is that architecture: an interactive topology of the real eu-west-2 deployment where every component is clickable, and a full write-up of what each piece is and why it's there. The why behind the whole design is the incident in zero-trust agents on Kubernetes.

This is a three-part lab. Part 1, this page, builds the architecture and explains every layer. Part 2 attacks it: it recreates the OpenAI and Hugging Face SSRF through JFrog Artifactory, deploys a rogue agent, and puts each control in front of a real exploit on the live cluster, showing the fix, the test, and the log. Part 3 is the same architecture on Google Cloud, every AWS service swapped for its GCP equivalent; this lab deploys to AWS today, and Part 3 is the GKE version of the same diagram.

Istio ambient + Vault CA agentgateway, one TLS door kagent + gVisor substrate PSA + Kyverno hardened AWS edge

Sovereignty is only worth as much as you can prove, so this build proves its claims rather than asserting them, along three lines:

The sovereignty section sets each of these out in full. Around it sit the layers a zero-trust deployment needs, from the hardened AWS edge to admission control to the one governed door in front of the model. It is all live, and every Solo product is marked in the diagram.

A production-grade, zero-trust architecture for agentic workloads

This is what a production deployment looks like when the workload is an AI model and the agents that call it: every layer hardened, and nothing trusted by default. The topology below is the real eu-west-2 build, from the AWS edge through the cluster to the managed services. Click any component, and its full detail, what it is, what we configured, and why, appears in the panel beside it (on a phone it opens as a card).

every layer deployed and verified on the live cluster dashed = best practice, not deployed in this lab ▸ Click any component to see what it is, what we configured, and why.

AWS · eu-west-2 (London)
VPC 10.42.0.0/16 · private subnets
Edge & network (hardened)
EKS 1.34 · uk-sovereign-ai
Workloads (ambient mesh)
Node groups (AL2023 · IMDSv2 · hop-limit 1)
Platform operations · not deployed in this lab
AWS managed

The layers, low to high

The topology above groups the stack by where things run. Here is the other way to read it, the way a security review does: by layer. Each control sits at a layer, and the higher the layer, the more it knows about what is actually happening. The network at the bottom sees a packet, an address and a port. The mesh above it sees a workload identity. The gateway at the top sees the whole request: which principal, which model, which tool, how often. That is why there is a control at every layer rather than one wall. Each one enforces something the layer beneath it cannot even see, and an attacker has to clear all of them, not one.

▲ sees the most — the request, the caller, the tool
L7 · the requestmay this caller make this exact request?
agentgatewayKeycloak JWT in Strict mode, CEL authorization over the token's claims, per-tool MCP authorization, a per-identity rate limit, and a UK PII guard on both the request and the response.
Admission · the workloadshould this workload be allowed to exist at all?
Pod Security Admission · Kyverno · AgentRegistryThe restricted profile, no :latest or unknown-registry image, no secret in an env var, resource limits required, and an agent or tool server admitted only if the registry authored it.
L4 · the identitywhich workload is this, cryptographically, and is the link encrypted?
Istio ztunnelA SPIFFE identity on a one-hour Vault-signed certificate for every workload, mutual TLS between all of them, and L4 authorization keyed on that identity rather than on an IP.
L3 · the packetcan this pod send a packet there at all?
NetworkPolicy · Route 53 DNS firewallDefault-deny egress: a workload reaches only DNS and the gateway, and the DNS firewall answers NXDOMAIN for any name off the allowlist, so even the resolver is not a way out.
Infra · the flooris the ground underneath locked down?
AWS: VPC · security groups · NAT · IMDS · KMS · IRSA · node OSPrivate subnets with one NAT out, the metadata endpoint unreachable from a pod, each workload its own IAM role, and etcd plus Vault under KMS keys you hold in-region.
▼ sees the least — an address and a port

Two controls sit across all of it rather than at one layer. gVisor gives the agents a userspace-kernel boundary, so a workload that does run is boxed in at the syscall level. And observability watches every band, because a control that fires is only useful if someone, or something, can see that it did.

How a request reaches the model

The agent calls the gateway's in-cluster service over the ambient mesh (mutual-TLS), the gateway checks its Keycloak JWT, and only then does the request proceed. The gateway-to-vLLM hop is over the mesh too: ztunnel wraps it in mTLS with hourly SPIFFE certificates, and the model's authorization policy admits the gateway's SPIFFE identity and nothing else. So the request never touches a public IP, never crosses the network unencrypted, and never reaches a workload that has not proven who it is. The NLB out front is the cluster's one public address, and anything arriving there still has to clear the gateway's JWT check before it can reach the model.

The two planes: identity and policy

Read the stack as two planes that cross at every workload: identity (who is this?) and policy (what may they do?).

Identity has two layers. Service identity is the mesh: ztunnel gives every workload a short-lived SPIFFE certificate at L4, signed by a Vault you run, so which workload is talking is cryptographic, not an IP that changes on restart. Caller identity is the IdP: Keycloak issues a JWT the gateway and waypoint check at L7, so which principal (which agent, user or team) is proven too. SPIFFE says the connection is the gateway's workload; the JWT says the request is alice in the platform group. You need both. Elsewhere people split these across two products (SPIFFE plus Okta or Entra); here they are the mesh and Keycloak, both in-region.

Policy is what those identities let you decide. Admission (Pod Security Admission and Kyverno) refuses a bad workload before it runs: no privileged pods, no unsigned or unknown-registry images, no secrets in env vars, and an agent onto kagent only if it came through agentregistry. NetworkPolicy is default-deny at the packet level for the workloads outside the mesh, and ztunnel's L4 authorization does the same job inside it (a k8s NetworkPolicy is a no-op once ztunnel re-originates the traffic). The agentgateway waypoint is L7, where per-agent and per-tool authorization lives, and agentgateway is the one door where authentication, rate limits, guardrails and the audit trail meet. No single control is load-bearing: two independent ones have to fail at once for anything to leak.

What runs where

Everything runs in one EKS cluster in eu-west-2, split across a hardened platform node group and a GPU node group, with the sandboxed agents on their own isolated group. The security stack shares the platform nodes; only the model needs the GPU.

One EKS cluster in eu-west-2 with three node groups. The platform group (3x m6i.xlarge, hardened) runs agentgateway, Istio ambient (ztunnel and waypoint), Vault and cert-manager, Keycloak, Kyverno and Pod Security, agentregistry, and Prometheus, Grafana and Alertmanager; the security stack shares these nodes. The gpu-od group (g7e, 96 GB) runs vLLM serving Mistral-Small-3.2-24B, the only workload that needs the GPU, and scales to zero between sessions. The sandbox group is tainted and isolated, running kagent agents under gVisor, sharing neither node nor kernel with the model or gateway.
NamespaceWhat runs thereNode group
modelsvLLM serving Mistral-Small-3.2-24B, no credential of its owngpu-od (g7e, 96 GB)
agentgateway-systemagentgateway, ext-auth, the rate-limiter, WAF, the response cache and the approved-mirror egress brokerplatform
istio-systemistiod, ztunnel (every node) and istio-cniplatform + all
vault / cert-managerthe mesh CA (Vault), the cert-manager issuer that feeds it, and the Secrets Store CSI providerplatform
keycloakthe OIDC identity provider for JWTsplatform
kyvernoadmission policies, alongside built-in Pod Security Admissionplatform
kagentthe agent controller and runtime, and each agent's L7 waypointplatform
agentsthe sandboxed agents themselves, run under gVisorsandbox (isolated)
gvisor-systemthe gVisor (runsc) runtime installer for the sandbox nodessandbox
mcp-toolsthe sovereign MCP tool server the gateway frontsplatform
agentregistrythe catalogue of agents and MCP servers and the one path they deploy from, with its ClickHouse and OTel telemetryplatform
monitoringPrometheus, Grafana, Alertmanager, the Mailpit alert sink and the availability canaryplatform
velerothe backup controller; snapshots to an in-region S3 bucketplatform
appsthe legitimate demo client that calls the model through the gatewayplatform
artifactory / internalthe artifact store reachable only through the brokered egress path, and the internal-only API used as the Part 2 SSRF targetplatform

The configuration

Every object that stands this up, straight from the source in the lab repo. Pick a product on the left and a config on the right. Account ids are placeholders and anything secret-shaped is redacted; none of this is hand-typed on the page, it is generated from the source files so it cannot drift from what actually runs. Where a layer is applied by a script rather than a manifest (the mesh, the CA, Vault, kagent, agentregistry), the tab shows that script's exact block.

What makes this sovereign

Sovereignty here is three things you can point at: where the data lives, who holds the keys, and how the traffic is controlled. All three are in-region and provable, not asserted. Below the tiers is the Sovereign Guide: what is sovereign today, point by point.

Data residency
Weights, inference, S3 and etcd all in eu-west-2. Requests never leave the region.
MET
Key custody
Vault is the root of trust for identity and secrets; Secrets on disk and the backups are encrypted under keys you own in-region.
MET
Traffic control
One governed door to the model and default-deny egress, so nothing leaves the region by accident.
MET

Sovereign Guide

Not air-gapped: brokered egress

Sovereign does not mean sealed. The agents need the internet to be useful, so they have it, but only through the gateway. Nodes sit in private subnets with a single NAT gateway as the one way out; NetworkPolicy stops the non-mesh workloads reaching it directly, and a Route 53 Resolver DNS firewall stops the mesh ones, whose ztunnel-reoriginated traffic a pod NetworkPolicy would miss. An agent that wants GitHub or a public MCP server goes through agentgateway, which authenticates the call, checks the destination against an allowlist, injects the credential the agent never holds, and logs it. The result is the opposite of a workload dialling anywhere it likes: one back door, brokered, and every outbound call on the record. It is also the only version of internet access you can put in front of an auditor.

Operational readiness

The architecture above is what the cluster is. Running it for real is a set of practices around it, and they are as much a part of the sovereignty story as any control on the diagram. Three are worth naming.

Build and release: CI/CD, GitOps and provenance

Everything on the cluster arrived as an image or an agent, and where those come from is the supply chain the runtime controls can only check, not create. cosign, Trivy and restrict-registries verify an image at admission; they cannot build a trustworthy one. That is the pipeline behind a sovereign deployment: images built from source you control, scanned for vulnerabilities and signed with a key you hold before they go anywhere near production, promoted through environments rather than pushed straight to prod, and released with the provenance to say which commit produced which running artifact. On the cluster side that is GitOps. Because every object here is already declarative YAML, a controller like Argo CD or Flux reconciles the cluster to what is in git, and git becomes the single source of truth: a change is a reviewed, signed pull request with an audit trail, drift is detected and corrected on its own, and a rollback is a git revert rather than a scramble. The whole cluster state, sovereign and reviewable, in a repo you control.

Test the restore

The lab backs up with Velero to an in-region bucket and snapshots the volumes, and that is the easy half. The half that matters is the restore, because a backup you have never restored is a hope, not a plan. So the restore gets rehearsed on a schedule: stand the cluster back up from the backup, bring Vault back from its own raft snapshot, re-attach the volumes, and prove the model answers and the agents run again, all inside the region. Time it and cost it, because that number is your real recovery time. A restore first attempted during an actual incident is how a recoverable outage becomes a permanent one.

Rehearsals and gamedays: kill switches you have pulled before

The point of layered controls is that one of them eventually fires for real, and the time to work out what to do next is not then. A gameday rehearses it. You inject a plausible exploit, a prompt-injected agent reaching for the model, a token that should not exist, a workload trying to leave the region, and you practise the response until it is routine. Part of that is knowing the kill switches and having used them before: cut an agent's egress at the gateway or the network, revoke a Keycloak token or client, scale a suspect agent to zero, isolate a namespace, or rotate the Vault-signed identities so a stolen certificate dies inside the hour it already lives. Every one of those is a control this architecture already has. A gameday is what turns having them into being able to reach for them under pressure.

Those three are practices. The architecture itself would also gain a few things in production that the lab leaves out on purpose, to keep it one region, one cluster and one command. They are worth naming so the gap is explicit rather than assumed.

Keep the AWS traffic in-region: VPC endpoints

The mesh keeps workload-to-workload traffic inside the region, but the cluster also talks to AWS itself: S3 for the weights and the backups, ECR for images, STS and KMS for identity and keys, CloudWatch for logs. In the lab those calls leave through the NAT gateway to the public AWS endpoints. In production they should not leave the VPC at all. An interface VPC endpoint (PrivateLink) for ECR, STS, KMS and CloudWatch, and a gateway endpoint for S3, keeps every AWS API call on the Amazon network inside eu-west-2, off the public internet. For a build whose claim is that nothing leaves the region, the AWS control-plane traffic is part of that claim, and endpoints are how you close it. They also take the NAT out of the path for AWS traffic, which removes a dependency and some data-processing cost.

No single points of failure

The lab runs one of several things a production cluster should run more than one of, because a single NAT gateway, a single-replica Vault and an in-cluster Keycloak are cheaper and easier to read. Production removes each: a NAT gateway per availability zone rather than one, Vault as a three-node raft cluster rather than a single replica so the mesh CA survives a node loss, and Keycloak's database on a managed multi-AZ store rather than a StatefulSet in the cluster it authenticates. The model is the honest exception: it is one replica on one GPU, and giving it real availability means GPU replicas behind the gateway and a recovery time you have measured. The PodDisruptionBudgets are already here; spreading the critical workloads across zones with topology constraints is the piece to add. None of this is in the lab, and all of it is standard EKS.

Run more than one cluster

The lab is one cluster. Ambient mesh does not stop at a cluster boundary: an east-west gateway peers two or more clusters so a workload in one reaches a workload in another over the same mTLS, with the waypoints still enforcing L7 policy in each cluster and the SPIFFE identities recognised across the peering. Global services then give you failover, so if a service's pods are gone in one cluster the traffic shifts to the copy in another. Kept in-region, several clusters spread across the eu-west-2 zones rather than one, this is how the single-cluster point of failure goes away without the data ever leaving the region. Multi-region uses the same mechanism, and for a sovereign build it is the thing you deliberately do not do: the clusters stay in one region on purpose.

A WAF at the gateway

agentgateway is the one door, and a web application firewall is a layer it runs at that door: inspecting each request against a rule set before it reaches the model or an agent, refusing the injection and abuse patterns that a schema check and a prompt guard are not shaped to catch. It sits in front of the JWT check and the rate limit as the coarse filter, so an obviously hostile request is turned away before any of the finer policy runs. In a production sovereign deployment the WAF is part of the edge, enabled on the gateway alongside the auth and rate-limit policies already there.

A few more, in brief

Three that belong on a production account rather than on the architecture diagram, noted here as best practice:

Running the environment

The GPU is the main cost, at roughly six dollars an hour, and it scales to zero between sessions. It is not the only cost: with the GPU off, the rest of the environment, the EKS control plane, the platform nodes that host the security stack, the NAT gateway and the storage, still idles at a few hundred dollars a month. That floor is the price of keeping the whole zero-trust environment alive with the model switched off. Bringing the model back is a scale-up of the GPU node group and a few minutes for vLLM to load the weights from the in-region volume; the weights never leave eu-west-2, so a rebuild never touches an external model host. Note that inference itself is not metered: once the GPU is up, a token costs nothing extra, which makes this a data-residency choice rather than a cost-saving one.

How to run it

The whole environment is code in the lab repo: the cluster is an eksctl definition, the Kubernetes objects are plain manifests, and an ordered set of scripts stands each layer up. There is no Terraform here because eksctl is already declarative infrastructure-as-code for EKS, but the shape is the same: one config describes the VPC, node groups and addons, and everything above it is versioned YAML and Helm values. Nothing secret is in the repo. The AWS account id and the weights bucket name are derived at run time from whichever profile is in effect, and the Solo Enterprise licence keys are read from your environment, never committed.

The scripts here run each layer by hand so you can read what each does. A production cluster would be driven by GitOps instead, a controller reconciling it to what is in git rather than a person running scripts, which is out of scope for a lab about the security layers themselves. That, along with the pipeline that builds and signs what runs here, and the backup and gameday rehearsals that keep it recoverable, is in Operational readiness above.

Before you start

You provide four things; the scripts create everything else.

You provideWhat it is
AWS accountAn account and an SSO or IAM profile with permission to create a VPC, EKS, IAM roles, KMS and S3. Exported as SOVEREIGN_AWS_PROFILE.
CLI toolsaws, eksctl, kubectl, helm, jq, python3 on your path.
GPU capacityA 4-GPU-class instance in your region (this build uses g7e.2xlarge). London capacity is tight, so a capacity reservation before a session is worth it.
Solo licencesSOLO_ISTIO_LICENSE_KEY and AGENTGATEWAY_LICENSE_KEY (plus kagent and agentregistry) exported in your shell, or in a file you point SOVEREIGN_ENV_FILE at. Never in the repo.

The model itself needs nothing from you: Mistral-Small-3.2-24B is Apache-2.0 and not gated, so there is no Hugging Face token. The first run pulls the weights once, mirrors them to an S3 bucket in your account, and every run after that restores from S3 in-region and never contacts Hugging Face again.

One command

The whole environment stands up from a single orchestrator. Set your profile and licence file, then run it; it creates the cluster and all three node groups, then runs every layer in the one order that works. Every phase is idempotent, so a re-run after a failure picks up where it left off.

export SOVEREIGN_AWS_PROFILE=your-sso-profile
export SOVEREIGN_ENV_FILE=~/your-solo-licences.env   # exports SOLO_ISTIO_LICENSE_KEY etc.

./deploy-all.sh                 # empty account -> defended sovereign model with agents

Under the hood it runs eleven phases, each also runnable on its own (./deploy-all.sh <phase>):

cluster     # eksctl: VPC, and all three node groups (platform, gpu-od, sandbox)
model       # network policy, storage, IRSA, the GPU node, the weights, vLLM
ca          # Vault (raft + KMS auto-unseal + PKI), then the ambient mesh signed by it
idp         # Keycloak
gateway     # agentgateway, then TLS on the edge (HTTPS listener, HTTP -> 301)
enrol       # put the workloads in the mesh
policy      # Pod Security Admission + Kyverno
obs         # Prometheus, Grafana, Alertmanager, Mailpit
substrate   # gVisor on the sandbox node group
kagent      # the agent runtime, OIDC to Keycloak
verify      # Mistral answers, over TLS, through the gateway

The GPU meter starts at the model phase; everything before it is cheap. Nothing secret is in the repo: the account id and bucket name are derived at run time and the licence keys come from your environment.

Stopping and starting

The GPU is the main cost, and it scales to zero on its own command; the EKS control plane, platform nodes and networking are the rest, and they idle cheaply. Bring the GPU down at the end of a session and back up for the next one; the weights are already on the in-region volume, so a restart is a few minutes, not a re-pull.

./scripts/gpu.sh down     # stop the GPU meter (the rest idles cheaply)
./scripts/gpu.sh up       # bring the model back for the next session

./scripts/teardown.sh down   # remove everything, after confirming the weights are safe in S3

The full source, the eksctl config and every script in this order are in the lab repo, linked from the top of this page.

That is the infrastructure. Part 2, testing the exploits puts it under test: a rogue agent and real attacks, each refused by a layer and caught in the logs, with real cluster output. The model answering over TLS, a 401 with no token, four admission refusals, Vault surviving a pod kill, an agent denied a tool it is not entitled to, a pod provably sandboxed under gVisor, and the gateway itself detecting an unauthorised call and emailing the SOC.

And if the cloud is Google rather than AWS, Part 3 is the same architecture on GKE, every AWS service swapped for its Google Cloud equivalent. This lab deploys to AWS today; Part 3 is the GKE version of the same diagram, the security unchanged because it lives in the mesh and the gateway, not the cloud underneath.