By default, every pod deployed to an Amazon EKS cluster can run as root, mount host paths, and escalate privileges. Most teams don’t realize this until a security review surfaces it. While certain system-level workloads genuinely need elevated access (Karpenter, for example, requires host-level permissions to manage nodes), application pods rarely do. Without restrictions, a pod running as root with full capabilities gives an attacker who compromises the application a direct path to the underlying node.
One approach to catching these issues early is integrating static analysis tools like Checkov into your deployment pipeline. Custom Checkov rules can block non-compliant YAML before it reaches kubectl apply. However, pipeline-level checks depend on every deployment flowing through that pipeline. They offer no protection against direct kubectl access or any path that bypasses CI.
Pod Security Admission (PSA) solves this at the cluster level. Built into Amazon EKS since version 1.25, PSA acts as a gatekeeper inside the Kubernetes API Server. Every pod creation request passes through it, regardless of how it was submitted. Platform engineering teams configure enforcement per namespace using three labels and three security levels.
The three labels control how violations are handled:
– pod-security.kubernetes.io/enforce — rejects non-compliant pods
– pod-security.kubernetes.io/warn — allows the pod but returns a warning to the user
– pod-security.kubernetes.io/audit — allows the pod but logs an audit annotation
Each label accepts one of three security levels as its value:
– privileged — no restrictions (the default when no labels are set)
– baseline — blocks known privilege escalations (hostNetwork, hostPID, privileged containers)
– restricted — follows current pod hardening best practices (runAsNonRoot, drop ALL capabilities, read-only root filesystem, seccomp profile)
In this article, I will first enable PSA on the default namespace with enforce: restricted. Once enabled, every new pod creation request targeting that namespace must pass the restricted security profile before the API Server admits it. The existing nginx Deployment from Article 2 is currently running in that namespace without any securityContext. After enabling enforcement, I will demonstrate three things: (1) pods deployed prior to enabling PSA continue running undisturbed, (2) new pods that violate the restricted level get rejected, and (3) adding a proper securityContext to the Deployment allows pods to pass enforcement.
This use case is part of a multi-part series exploring Amazon EKS with Terraform and GitHub Actions. The complete list of articles is available in the repository README.
Solution Overview
Unlike previous articles in this series, there is nothing to install. PSA has been running in every EKS cluster since Amazon released version 1.25 — it just needs to be activated via namespace labels. To make the flow clear, I organized this use case into two iterations:
Iteration 1. Enable Pod Security Enforcement: Apply the PSA labels to the default namespace to activate enforcement at the restricted level.
Iteration 2. Hardening the Deployment: Add a securityContext to the existing nginx Deployment so that its pods comply with the restricted profile.

The architecture diagram above shows the admission flow. When a pod creation request reaches the API Server, PSA checks the target namespace’s labels and evaluates the pod spec against the configured security level. Compliant pods are admitted; non-compliant pods are rejected.
Prerequisites
This use case builds on two earlier articles in this series. Article 1 provisions the EKS cluster (version 1.30, which includes PSA support), and Article 2 deploys the nginx application that serves as the enforcement target in this article. Please go through those before starting here. The enforce-pod-security branch also includes components from Article 3, Article 4, Article 5, and Article 6. Those components are not required for PSA configuration, but the GitHub branch builds on them.
You can find the complete implementation in my GitHub repository: kunduso-org/aws-eks-terraform (branch: enforce-pod-security). The code includes GitHub Actions CI/CD and security scanning with Checkov.
First Iteration: Enable Pod Security Enforcement
Implementation
In this iteration, since the Nginx deployment was hosted in the default namespace, I applied the PSA labels to that namespace to activate enforcement at the restricted level. I also reorganized the platform manifests directory and updated the deployment workflow to apply security manifests as a separate step.
Step 1: Create the namespace label manifest
This update is in the platform/manifests/security/namespace-default.yaml file that applies all three PSA labels to the default namespace. Setting all three to restricted implies: (a) non-compliant pods are rejected (enforce), (b) warnings are shown to the user at kubectl apply time (warn), and (c) violations are logged in the API Server audit log (audit).

Since the default namespace already exists, kubectl apply patches it with the new labels rather than creating it.
Step 2: Update the platform deployment workflow
The existing workflow (from a previous article) applied all manifests in a single step. To support the new directory structure, I split it into two steps. The security manifests step runs first and does not depend on any CRDs. The Karpenter manifests step (from Article 3) runs second and continues to wait for Karpenter CRDs to be created before applying.

Deployment
Before I merged this change, the default namespace carried only the standard kubernetes.io/metadata.name=default label and no PSA enforcement. As the image below shows, the nginx pods from Article 2 were running for 51 days without any security restrictions.

After merging PR #58, the platform pipeline ran, and the Apply Security Manifests step applied the namespace labels. The step output displayed two expected warnings. The first warns that the default namespace is missing the kubectl.kubernetes.io/last-applied-configuration annotation (because the namespace was created by Kubernetes, not by kubectl apply). This is a one-time message and is automatically resolved. The second warning identifies existing pods that violate the new enforcement level — but does not terminate them.

Running kubectl describe namespace default on my laptop confirmed that all three PSA labels were now active on the namespace.

Verification
To verify that enforcement is active, I ran the kubectl rollout restart deployment/nginx-app -n default command. The warn mode immediately returned a warning listing the four violations (allowPrivilegeEscalation, unrestricted capabilities, runAsNonRoot, seccompProfile). The Deployment object was updated successfully, but the new ReplicaSet could not create pods.

The kubectl describe deployment nginx-app -n default output confirmed the failure. The Deployment displayed ReplicaFailure: True with reason FailedCreate, and the new ReplicaSet had 0/1 replicas created. Meanwhile, the original pods from 51 days ago continued running undisturbed.

On describing the new ReplicaSet, the explicit PSA rejection message in the Events section was displayed. Each attempt to create a pod was forbidden with “violates PodSecurity restricted:latest” followed by the specific violations.

Hence, the solution was to harden the Deployment with an appropriate security context.
Second Iteration: Harden the Deployment
Implementation
With enforcement active, the second iteration was to update the nginx Deployment so its pods could comply with the restricted profile. This required three changes: switching to a non-root container image, adding a securityContext that satisfies all four PSA requirements, and mounting a writable volume for runtime files.
Step 3: Update the Deployment
The original Deployment used public.ecr.aws/nginx/nginx:1.27, which runs as root (UID 0) and listens on port 80 (a privileged port). The restricted level requires runAsNonRoot: true, which makes this image incompatible.
Hence, I replaced it with nginxinc/nginx-unprivileged:1.27. This image runs as UID 101, listens on port 8080, and writes all runtime files (pid, cache, temp) to /tmp. Since readOnlyRootFilesystem: true prevents writes to the container filesystem, I added an emptyDir volume mounted at /tmp to give nginx a writable location for its runtime files.

The securityContext addresses each violation that PSA reported in the first iteration:
– runAsNonRoot: true — the container cannot run as UID 0
– seccompProfile.type: RuntimeDefault — applies the default seccomp filter
– allowPrivilegeEscalation: false — prevents processes from gaining additional privileges
– readOnlyRootFilesystem: true — makes the container filesystem immutable
– capabilities.drop: ["ALL"] — removes all Linux capabilities
Step 4: Update the Service
Since the unprivileged nginx image listens on port 8080 instead of 80, the Service’s targetPort needed to be updated. The Service port remains 80, so the Ingress and ALB continue to work without changes.

Deployment
After merging PR #59, the deploy-app.yml workflow ran and applied the updated Deployment and Service. The new pods were created successfully — PSA evaluated the pod spec against the restricted level and admitted them.

Verification
Running kubectl get pods -n default confirmed the new pods were running with the hardened configuration. The old 51-day pods had been replaced by new pods with the updated image and security context.

Running kubectl get pod <pod-name> -n default -o yaml | grep -A 5 "securityContext" showed the security context fields were applied as expected: runAsNonRoot: true, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, capabilities dropped, and seccomp profile set to RuntimeDefault.

Compare this to the First Iteration, where the same Deployment triggered ReplicaFailure because the pod spec lacked these fields. The four violations PSA reported (allowPrivilegeEscalation, unrestricted capabilities, runAsNonRoot, seccompProfile) were now addressed, and pods passed the restricted enforcement level without modifying any cluster component.
Conclusion
Pod Security Admission provides a zero-install mechanism for restricting what pods can do in the cluster. By labeling namespaces with enforce: restricted and adding a compliant securityContext to deployments, platform teams gain a safety net that works regardless of how workloads reach the cluster.
While PSA enforces only on namespaces that carry the labels, any unlabeled namespace defaults to privileged, which results in no restrictions at all. For a single-team cluster in which the platform team controls all namespaces via infrastructure as code, this is manageable. In multi-team environments, a policy engine like Kyverno must be configured to ensure no namespace escapes enforcement. I’ll cover that in a future article.
If you have any questions or suggestions, feel free to comment or get in touch.