What a permissive pod can do
A pod specification is a request to run code on a node, and Kubernetes will honour a lot by default. A container can run as root, mount the host filesystem, share the host network, add Linux capabilities or run privileged. Any one of those can turn a single application bug into node compromise, and from the node an attacker can reach other pods and credentials.
Pod Security Standards (PSS) are the built-in guardrail. This page describes how we apply them as part of AWS EKS consulting. They are one layer of our wider EKS security work, alongside network policies and least-privilege IAM.
The three profiles
| Profile | Intent | Typical use |
|---|---|---|
| Privileged | No restrictions | CNI, storage drivers, node agents |
| Baseline | Blocks known privilege escalations | Legacy apps, a stepping stone |
| Restricted | Hardened, current best practice | Application namespaces |
Baseline prevents things like privileged containers, host networking and host paths. Restricted adds requirements: containers must run as non-root, disallow privilege escalation, drop all capabilities (adding back only NET_BIND_SERVICE) and use a runtime default or localhost seccomp profile.
How enforcement works
The Pod Security Admission controller ships with Kubernetes and is enabled on EKS. You apply labels to a namespace to choose the mode and profile:
enforce: reject violating pods.audit: allow, but record a violation in the audit log.warn: allow, but show a warning to the user.
kubectl label namespace payments \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/audit-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latest \
--overwrite
# once clean, switch on enforcement and pin a version
kubectl label namespace payments \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=v1.30 \
--overwrite
Pinning the version avoids surprises when a new Kubernetes release tightens a profile. A dry run is also available: kubectl label --dry-run=server --overwrite ns --all pod-security.kubernetes.io/enforce=baseline reports which existing namespaces would have violations.
An audit-first rollout
Switching on enforcement across a live cluster is how teams break deployments. We go in this order:
- Apply
warnandauditon every namespace and read the output for a representative period. - Group the findings by cause: root users, missing seccomp, added capabilities, host paths.
- Fix manifests first (usually a
securityContextblock), then base images that insist on root. - Move system components into labelled namespaces with the profile they actually need.
- Enable
enforcenamespace by namespace, starting with the least critical.
A compliant container security context looks like this:
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
Pair it with a pod-level fsGroup where volumes need group access, and with readOnlyRootFilesystem: true where the application permits. That last setting is not required by Restricted but is a cheap extra hardening step.
Where PSS is not enough
PSS looks at pod specs and nothing else. It does not check which registry an image comes from, whether resource requests exist, whether labels are present or whether a service type is allowed. For those we add policy as code:
- Kyverno uses Kubernetes-style YAML policies and suits teams that want readable rules and mutation, such as injecting default security contexts.
- OPA Gatekeeper uses Rego, which is powerful but has a steeper learning curve.
- ValidatingAdmissionPolicy is built into recent Kubernetes versions and avoids a webhook component for simpler rules.
We usually choose one engine, not several. Policies are written to the same standards we apply in CI, where tools such as kube-linter or Checkov can catch failures before merge. Related access control concepts are explained in the Kubernetes RBAC glossary entry.
Common failure modes
- Third-party Helm charts that run as root. We override values, pick alternative images, or isolate the chart in a Baseline namespace with a documented reason.
- Init containers forgotten. They are checked against the profile too.
- Add-on namespaces labelled Restricted by accident, breaking the CNI or CSI driver. System namespaces are labelled deliberately.
- Stale exemptions. We record each exemption with an owner and review date.
- Webhook availability: if a policy engine is down and fails closed, deployments stop. We run it with replicas and sensible failure policies.
A worked example: a Helm chart that runs as root
A typical case: a third-party chart for an internal tool deploys a container whose image expects to run as root and write into its own filesystem. In audit mode the namespace reports several violations at once: root user, no seccomp profile, privilege escalation allowed. We work through them in order of effort. The seccomp profile and privilege escalation settings are usually one-line values changes. The root user is harder, because the image may write to paths owned by root.
Our options, in order of preference: set a non-root user and mount an emptyDir at the paths that need to be writable, switch to a maintained image variant that already runs unprivileged, or build a thin derived image that fixes file ownership. If none of those works, the workload goes into a separate Baseline namespace with the reason and an owner recorded, so the exception is visible instead of quietly weakening the whole cluster.
Exemptions and system namespaces
Pod Security Admission can be configured with exemptions for particular namespaces, users or runtime classes. We use them sparingly. Exempting a whole namespace is easier to audit than exempting a user, and every exemption is written down with an owner and a review date.
When not to start here
If your cluster has no network segmentation, no IAM isolation and no image scanning, PSS is still worth enabling, but it is not the first thing that will reduce risk. We rank controls by exposure and effort during a security review, and sometimes start with IAM or public endpoint exposure instead.
Delivery and handover
You get namespace labels and policies in Git, a table of exceptions with reasons, updated manifests or chart values for affected workloads, and a developer guide with a compliant securityContext template. New services should pass by default.
How to tell it is working
Try to deploy a privileged pod in an application namespace: it must be rejected. Track audit violations over time, which should trend to zero, and check that every exemption has an owner. To discuss your starting point, book a consultation.