Home› Blog› Week 18 - EKS Self-Service: A Namespace Is Not a B…
AWS Weekly Lab AWS Terraform

A namespace is not a boundary

It is a label. It does not limit CPU, it does not stop a pod reaching a pod next door, and it has nothing to say about AWS permissions. This week builds a cluster where a new team gets a namespace that genuinely is a boundary — then tries to break it, and finds the test was lying.

Verified against current vendor documentation on 16 September 2026. Pricing, limits and API behaviour were checked against the official docs on that date. Cloud services change fast — if you are reading this much later, treat the specifics as a starting point and re-check the linked sources.
AWS Platform Engineering Lab · Week 18 of 52

Lab Overview

A developer needs somewhere to run a container. In most organisations that is a ticket: somebody senior creates a space on the cluster, sets limits so the new team cannot consume everything, and wires up permissions so their application reaches its own storage and nobody else's. It takes days, and it is done slightly differently every time.

The problem. Kubernetes looks like it already solved this. It has namespaces, creating one takes a second, and it appears to divide the cluster. It does not. A namespace is a name: it enforces no CPU limit, blocks no network traffic, and grants no cloud permissions. Everything people assume it does has to be added deliberately, as separate objects.

The objective. Build a real tenant boundary for two teams on one cluster, then attack it to find out whether it holds.

The outcome. The boundary held. The first test that said so was meaningless, which turned out to be the more useful finding.

DifficultyAdvanced
Lab runtimeRan 11h 22m before teardown (22:06–09:28)
Measured cost$2.00 for 11h 22m
DeploymentTerraform via HCP Terraform with OIDC
Full lab guideGitHub — prerequisites, Terraform, commands, validation, cleanup

Cloud Services and Concepts

ServicePurpose in this lab
Amazon EKSThe shared cluster both tenants run on.
EKS Pod IdentityGrants AWS permissions to pods on EC2 nodes. AWS's recommended mechanism.
IRSAThe older mechanism — and the only one that works on Fargate.
AWS FargateRuns pods with no node to manage. Also the reason both mechanisms are here.
Amazon S3One bucket per tenant — what the isolation test actually tries to cross.
ResourceQuota / NetworkPolicyThe Kubernetes objects that turn a namespace into a boundary.

What a boundary actually costs to build: three objects per tenant, none of which arrive with the namespace. A ResourceQuota is the ceiling, a NetworkPolicy is the boundary, and an AWS role is the permission.

Kubernetes allows cross-namespace traffic by default, which is the opposite of what most people assume. Until a NetworkPolicy denies it, one tenant's pod can reach another's.

Architecture and Workflow

Icon-Architecture/48/Arch_Amazon-Elastic-Kubernetes-Service_48 Icon-Architecture/48/Arch_Amazon-EC2_48 Icon-Architecture/48/Arch_AWS-Fargate_48 Icon-Architecture/48/Arch_Amazon-Simple-Storage-Service_48 ONE EKS CLUSTER · KUBERNETES 1.36 · $0.10/HR FROM CREATION TENANT-A · ON EC2 Managed node · t3.small the Pod Identity agent needs somewhere to run Pod Identity what AWS recommends · no trust policy per cluster namespace tenant-a ResourceQuota · NetworkPolicy · ServiceAccount TENANT-B · ON FARGATE Fargate · no node to manage no DaemonSets, no privileged pods IRSA the only option here · one OIDC provider per cluster namespace tenant-b ResourceQuota · NetworkPolicy · ServiceAccount Pod Identity cannot run on Fargate — its agent is a privileged DaemonSet, and Fargate runs neither The webhook still injects the variables, so the pod looks configured and then times out. It reads as an IAM problem. tenant-a bucket private · encrypted tenant-b bucket private · encrypted denied — and tested, not assumed Each role reaches one bucket: ListBucket on it, GetObject and PutObject on its contents, nothing else. Built and destroyed inside 12 hours. EKS bills from creation, with no free tier.
  1. One EKS cluster hosts two tenants, each in its own namespace.
  2. Each namespace carries a ResourceQuota, which caps consumption and makes requests and limits mandatory for every pod in it.
  3. A NetworkPolicy denies cross-namespace ingress, which Kubernetes otherwise permits.
  4. Tenant pods on EC2 nodes get AWS permissions through Pod Identity.
  5. Tenant pods on Fargate get them through IRSA, because Pod Identity cannot work there.
  6. Each tenant role reaches exactly one S3 bucket and nothing else.
  7. An isolation test runs as part of the build and tries to cross every one of those lines.

Design decisions

Two identity mechanisms, which looks like indecision and is not. AWS recommends Pod Identity over IRSA. AWS also offers Fargate so you never manage a node. Follow both recommendations and you get a cluster that cannot work: the Pod Identity agent is a privileged DaemonSet, and Fargate runs neither DaemonSets nor privileged pods.

And the way it fails is the interesting part. The webhook still injects the environment variables, so the pod looks correctly configured. The credential fetch then times out against an agent that was never there. It presents as an IAM misconfiguration, and you can spend a long time auditing trust policies that were right all along.

The isolation test is part of the build. A boundary nobody has attacked is a boundary nobody has tested.

Prerequisites

RequirementDetail
AWS accountPermission for EKS, EC2, Fargate, VPC, IAM and S3
Terraform>= 1.10, plus the Kubernetes provider
Control plane loggingAudit logs on — a denied cross-tenant call is only visible there
Regionus-east-1

Cost starts immediately and there is no free tier. The EKS control plane bills $0.10/hr from creation and a NAT gateway adds $0.045/hr — about $0.166/hr fixed for this build. A cost that starts immediately is safer than one that starts in thirty days, because you design around it.

Know who the cluster admin is before you need to be one. On a CI runner it is not you — which is why access entries are declared in Terraform.

Implementation

A cluster with both an EC2 node group and a Fargate profile, two tenant namespaces with their quotas and policies, two S3 buckets, and the roles that connect them. The lab guide carries the full Terraform.

week-18-eks-self-service/
|
+-- terraform/
|   +-- environments/dev/   HCP backend, OIDC, wires the modules
|   +-- modules/cluster/    VPC, EKS, node group, Fargate profile, access entries
|   +-- modules/tenant/     namespace, quota, NetworkPolicy, bucket, role
|
+-- tests/                  the isolation probes
+-- scripts/                cleanup, including the OIDC provider sweep
Only the directories that matter. The full repository is on GitHub.

The tenant module is the point: one module instantiated twice produces two boundaries built identically, rather than two boundaries built slightly differently by hand on different days.

The EKS console showing one t3.small node in Ready state, managed by the week18-platform node group
One t3.small node. It exists so the Pod Identity agent has somewhere to live — everything else here could have been serverless.
The EKS console showing the Pod Identity association mapping the tenant-a namespace and service account to an IAM role
One association: namespace, service account, role. Compare it with the IRSA trust policy two steps above — that one needs editing for every new cluster, this one does not.

Validation and Evidence

Four probes, each deliberately trying to cross a line the design says it cannot.

TestExpectedActualStatus
Tenant reads its own bucketAllowedAllowedPass
Tenant reads the other tenant's bucketDeniedDenied — visible in the audit logPass
Cross-namespace network callDenied by NetworkPolicyDeniedPass
Pod without requests/limitsRejected by the quotaRejected before a container startedPass
Pod Identity on FargateWorksCredential fetch times out; looks like an IAM faultFail — by design, use IRSA there
The first run of the suiteMeaningful resultTwo green ticks against pods that never ranFail — see below
The first test run showing two passes, and the manual probe revealing every pod was rejected by the quota before starting
Every probe was rejected before a container started. Two of those four lines are green.
The isolation test passing all four checks, with both cross-tenant reads denied
Four for four. Pod Identity on EC2 and IRSA on Fargate reach the same boundary by different routes.

Security and Cost

Security

The identity mechanism is chosen by where the pod runs, not by preference — Pod Identity on EC2, IRSA on Fargate, because Pod Identity cannot work there.

Each tenant role reaches exactly one bucket: s3:ListBucket on the bucket, GetObject/PutObject on its contents, nothing else anywhere.

The IRSA trust policy pins both :sub and :aud. Without :sub, any service account in the cluster can assume the role — and the omission is silent.

NetworkPolicy denies cross-namespace ingress, because Kubernetes allows it by default.

Control plane audit logging is on. A denied cross-tenant call is only visible there.

Cost

$2.00 measured from Cost Explorer on 16 September — not a calculation. At publication AWS had not posted the charges, so the section originally gave the rate and the duration and left the multiplication to the reader.

LineCost
EKS control plane — 11h 22m at $0.10, the rate card exactly$1.11
NAT gateway ($0.54 hours, $0.03 data)$0.57
t3.small node$0.23
Elastic IP and the remainder$0.08

After teardown: $0.00, which is what a verified destroy looks like on a bill.

Troubleshooting and Lessons

The test passed against pods that never ran

Problem. The first run reported two passed, two failed. The two failures looked like a real isolation problem.
Root cause. Every probe was rejected before a container started. The ResourceQuota requires every pod to declare CPU and memory requests and limits; my probes declared none, so the admission controller refused all four. The checks looked only for denial strings in the output — and a pod that never starts produces none. So "no denial found" scored as success on the two tests that expected success.
Resolution. Add resources to the probes — and make anything resembling a pod failing to start a hard failure regardless of what the output says.
Lesson. A test that cannot distinguish "denied" from "never ran" is not a test. Two green ticks against rejected pods is worse than a red run, because nobody investigates a pass.

Adding a quota changes the contract for every existing pod

Problem. Pods that previously deployed fine began being refused.
Root cause. A ResourceQuota makes requests and limits mandatory in that namespace — retroactively, for everything.
Resolution. Declare resources on every workload before applying the quota.
Lesson. The quota is not only a ceiling; it is a new admission requirement.

Two AWS recommendations that are mutually exclusive

Problem. Pod Identity silently failed on Fargate.
Root cause. The agent is a privileged DaemonSet; Fargate runs neither. The webhook still injects the environment variables, so the pod looks correctly configured and the credential fetch simply times out.
Resolution. IRSA for Fargate pods, Pod Identity for EC2 pods, in the same cluster — which is AWS's own answer.
Lesson. It presents as an IAM misconfiguration. You can audit correct trust policies for a long time.

Cleanup and Key Takeaways

The script checks clusters, NAT gateways, elastic IPs, VPCs, instances, roles, buckets and log groups by name, then does a tag search on Week=18 for anything the name checks missed.

OIDC providers are on that list deliberately. IRSA creates one per cluster, they do not appear anywhere in the EKS console once the cluster is gone, they survive a careless teardown, and IAM caps an account at 100 of them. They cost nothing, which is exactly why they accumulate.

EKS access entries listing the SSO administrator role, with tabs for Pod Identity associations and OIDC identity providers
The fix, and a good summary of the week: access entries, Pod Identity associations and OIDC providers as three tabs on one page.

Key takeaways

  • A namespace is a label. The quota is the ceiling, the NetworkPolicy is the boundary, and the AWS identity is the permission. None of them arrive on their own.
  • A test that cannot tell "denied" from "never ran" is not a test.
  • Two AWS recommendations can be mutually exclusive. Pod Identity and Fargate are each the right default and together produce a silent failure.
  • Adding a quota changes the contract for every pod in the namespace, retroactively.
  • A cost that starts immediately is safer than one that starts in thirty days, because you design around it.

References

Comments

How was your experience?
Your feedback helps improve this site.
PoorExcellent