Understanding Kubernetes: Pod Security

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: true can 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