Understanding Kubernetes: Pod Security
It has been quiet here over the last few weeks - life has been busy on different levels. But finally, it’s time to continue our Understanding Kubernetes series :) In the previous articles we looked at different parts of securing Kubernetes: how Network Policies control communication and how RBAC controls access to resources.
But there is another important question we haven’t answered yet: What is actually allowed to happen inside a container?
A Pod can have the correct network permissions and the correct access rights - but what if the container itself has too many privileges? What if it runs as root? What if it can access parts of the underlying node? This is where Pod Security comes in.
And this is also one of those Kubernetes topics where many people copy a YAML snippet from somewhere, hope it works, and never really look at why it does what it does. 😉 So let’s break it down.
Why does Pod Security matter?
Containers feel isolated - and most of the time they are.
But a container is not a virtual machine. It shares the kernel with the host system, and depending on its configuration, it might have more access than we actually want. A badly configured container could:
- run with root privileges
- access sensitive parts of the host system
- use unnecessary Linux capabilities
- perform actions it does not need for its actual purpose
So, the goal of Pod Security is simple: Reduce the privileges of containers to only what they actually need.
Running as root vs non-root
One of the first things to look at is the user a container runs as. By default, many container images run as root inside the container. This does not automatically mean the container can do anything on the host, but it is still a default we do not always want.
For example:
apiVersion: v1
kind: Pod
metadata:
name: example
spec:
containers:
- name: nginx
image: nginx
This works - but we did not define who the process runs as. We can make this explicit:
apiVersion: v1
kind: Pod
metadata:
name: example
spec:
securityContext:
runAsNonRoot: true
containers:
- name: nginx
image: nginxinc/nginx-unprivileged
Now Kubernetes refuses to start the container if it would run as root.
The important part here is: runAsNonRoot: true - this tells Kubernetes: “Do not start this container if it would run as root.”
Container securityContext vs Pod securityContext
You might have noticed that the securityContext above is defined at Pod level, meaning it’s valid for all containers. But Kubernetes also supports security settings on container level:
spec:
containers:
- name: app
image: my-app
securityContext:
allowPrivilegeEscalation: false
Container-level settings can override Pod-level settings where Kubernetes allows it.
Linux capabilities
Linux has a concept called capabilities: Instead of giving a process full root privileges, Linux splits privileged actions into smaller pieces. For example:
- NET_ADMIN allows network configuration changes
- SYS_TIME allows changing system time
- CHOWN allows changing file ownership
Most applications do not need additional capabilities, so a good default is removing them:
securityContext:
capabilities:
drop:
- ALL
If a container really needs specific capabilities, we can add them explicitly:
securityContext:
capabilities:
add:
- NET_ADMIN
Privileged containers
Another setting you will sometimes see is privileged: true. This gives a container significantly expanded access to the host system.
This can be useful for specific system-level workloads, for example some storage or networking components. But for normal applications we should try to avoid it. A privileged container breaks a lot of the isolation we usually expect from containers.
Seccomp: restricting system calls
Another layer of protection is seccomp. Applications communicate with the Linux kernel through system calls. There are hundreds of them - but most applications only need a small subset. Seccomp allows us to define which system calls processes inside a container are allowed to use.
A simple example could look like this:
securityContext:
seccompProfile:
type: RuntimeDefault
This tells Kubernetes to use the default seccomp profile provided by the container runtime. For most workloads, this is a good baseline.
Pod Security Admission
So far we looked at individual settings. But how do we make sure nobody deploys Pods without them? This is where Pod Security Admission (PSA) comes in.
Pod Security Admission is a built-in Kubernetes admission controller that checks Pods before they are created. It implements the three levels defined by the Pod Security Standards:
- Privileged: no restrictions
- Baseline: prevents known privilege escalations
- Restricted: follows stricter security best practices
For example, we can enforce restricted security in a namespace:
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted
Now Kubernetes will reject Pods that do not follow the restricted policy.
One last note: PSA is not a replacement for good security practices. It enforces certain rules at deployment time, but it does not magically secure a workload. A container image with outdated dependencies, leaked secrets, or a vulnerable application is still vulnerable.
Demo time
Let’s try this out. First, we create a namespace
kubectl create namespace security-demo
and label it with the restricted policy:
kubectl label namespace security-demo \
pod-security.kubernetes.io/enforce=restricted
Next, we try to deploy a Pod that runs as root:
apiVersion: v1
kind: Pod
metadata:
name: insecure
namespace: security-demo
spec:
containers:
- name: nginx
image: nginx
securityContext:
runAsUser: 0
Apply it via
kubectl apply -f insecure.yaml
Instead of receiving a new pod, we’ll get an error message:
Error from server (Forbidden): error when creating “insecure.yaml”: pods “insecure” is forbidden: violates PodSecurity “restricted:latest”: allowPrivilegeEscalation != false (container “nginx” must set securityContext.allowPrivilegeEscalation=false), unrestricted capabilities (container “nginx” must set securityContext.capabilities.drop=[“ALL”]), runAsNonRoot != true (pod or container “nginx” must set securityContext.runAsNonRoot=true), runAsUser=0(container “nginx” must not set runAsUser=0), seccompProfile (pod or container “nginx” must set securityContext.seccompProfile.type to “RuntimeDefault” or “Localhost”)
So, our label works and PSA is in place :) We can also see that PSA does not only check the one setting we violated. It checks the complete Restricted policy and reports all missing requirements.
Let’s now create a compliant version by using another image that runs as non-root and adding the required securityContext settings:
apiVersion: v1
kind: Pod
metadata:
name: secure
namespace: security-demo
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginxinc/nginx-unprivileged
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
This time, our Pod is created successfully. 🎉 It now satisfies both layers of protection:
- Pod Security Admission accepts the Pod because it complies with the restricted policy.
- The container image itself is designed to run as a non-root user, so
runAsNonRoot: truecan be enforced successfully.
Summing up
Pod Security is about limiting what containers are allowed to do. The most important concepts are:
- run containers as non-root whenever possible
- remove unnecessary Linux capabilities
- avoid privileged containers
- use seccomp profiles
- enforce security standards with Pod Security Admission
Together with RBAC and Network Policies, this gives us a much stronger security foundation. And the good news: it does not have to be magic YAML. Once you understand what each setting actually does, Pod Security becomes much easier to reason about.
Next up in this series would have been secrets management and protecting sensitive data. As I already wrote an article covering this topic, I recommend taking a look at it first. For this series, the next topic will therefore be scheduling within Kubernetes. Stay tuned :)
header image created by buddy ChatGPT
