Learn Seccomp - Seccomp in Kubernetes
Episode 10 of 23

Learn Seccomp - Seccomp in Kubernetes

Applying seccomp in Kubernetes via the seccompProfile field on securityContext, its evolution from alpha annotations, and its relation to Pod Security Standards. Including custom profile distribution across nodes and the seccomp-profiles-operator.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 9 you locked down services directly on the host. In a Kubernetes cluster the challenge is bigger: many nodes, many pods, and different runtimes. But the principle is the same — a pod runs containers that share the node's kernel, and dangerous syscalls must be pruned. The difference is that Kubernetes adds one layer of abstraction: seccompProfile in securityContext.

Episode 10 covers how to apply seccomp in Kubernetes: the seccompProfile field, its evolution from alpha annotations, Pod Security Standards, distributing custom profiles across nodes, and seccomp-profiles-operator.

Evolution: From Alpha Annotations to the seccompProfile Field

Kubernetes has a long history with seccomp. Originally (long before version 1.19), the only way was an alpha annotation:

pod-legacy.yaml — alpha annotation (historical)
apiVersion: v1
kind: Pod
metadata:
  name: legacy-app
  annotations:
    seccomp.security.alpha.kubernetes.io/pod: "runtime/default"
spec:
  containers:
    - name: app
      image: busybox
      command: ["sleep", "3600"]

This annotation was fragile: no schema validation, hard to apply per-container, and its alpha status meant it could change at any time. That's why Kubernetes introduced a proper seccompProfile field in securityContext (beta since 1.19, GA since 1.25). Since then, seccomp has been treated as infrastructure — not an experiment.

Note

If you find old documentation or Helm charts still using the seccomp.security.alpha.kubernetes.io/pod annotation, plan a migration to the seccompProfile field. The old annotation still reads for compatibility, but it's not the recommended way for new configuration.

The Three seccompProfile Values

The securityContext.seccompProfile.type field accepts three values:

  • Unconfined — no seccomp filter. This is the historical Kubernetes default; avoid it unless you're truly confident.
  • RuntimeDefault — the container runtime's built-in profile. With Docker/containerd that's the docker-default from episode 8: a deny-list of dangerous syscalls maintained by the community.
  • Localhost — a custom profile as a JSON file present on every node, referenced via localhostProfile.

The precedence rules are simple: a pod-level seccompProfile acts as the default for all containers, and a container can override it with its own seccompProfile. The most specific level wins.

Example: RuntimeDefault and Localhost

Here's the most common pattern — a pod using the runtime's built-in profile:

pod-secure.yaml — RuntimeDefault at the pod level
apiVersion: v1
kind: Pod
metadata:
  name: webapp
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
    - name: nginx
      image: nginx:1.27

For full control, point to a custom profile stored on the node:

pod-localhost.yaml — custom profile from the node
apiVersion: v1
kind: Pod
metadata:
  name: webapp-custom
spec:
  containers:
    - name: nginx
      image: nginx:1.27
      securityContext:
        seccompProfile:
          type: Localhost
          localhostProfile: profiles/webapp-allowlist.json

localhostProfile is relative to /var/lib/kubelet/seccomp/ on the node. The profile file must exist on all nodes where the pod could be scheduled — this is the source of an operational problem we'll discuss in a moment.

Tip

Start with RuntimeDefault for all workloads. It gives you a solid seccomp baseline at zero operational cost. After that, graduate gradually to Localhost with a purpose-built allow-list for workloads you understand (following the strace baseline flow from episodes 7 and 8).

Pod Security Standards: Baseline and Restricted

Kubernetes doesn't stop at the API level — it also standardizes policy. The Pod Security Standards define two levels related to seccomp:

  • Baseline — requires seccompProfile.type: RuntimeDefault for all pods.
  • Restricted — inherits the Baseline requirements, plus other restrictions like allowPrivilegeEscalation: false and dropping capabilities.

These policies are enforced per-namespace via the pod-security.kubernetes.io/enforce label:

namespace.yaml — enforce Restricted
apiVersion: v1
kind: Namespace
metadata:
  name: secure-ns
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted

Pods that don't meet the Restricted level are rejected (because of enforce) or reported in the audit log (because of audit) — the right label pair for a gradual rollout. This is the fastest way to normalize seccomp across a cluster: not relying on individual discipline, but enforcing through the platform.

Distributing Custom Profiles Across Nodes

Here's the real challenge of Localhost: profiles are files on the node filesystem, and those files must be identical on all nodes — including nodes added later. Managing this manually is a recipe for disaster. Three common approaches:

  1. Bake into the node image — embed the profile files into the node image (immutable infrastructure). Consistent, but adding a new profile means building a new node image.
  2. Provision via config management — Ansible/Cloud-Init places files on every node. Flexible, but don't forget new nodes.
  3. seccomp-profiles-operator — a Kubernetes operator that manages profiles as resources and distributes them to all nodes automatically.

The third approach fits clusters that keep changing. With the operator, profiles are declared as a CRD:

seccomp-profile.yaml — profile via operator
apiVersion: security-profiles-operator.x-k8s.io/v1beta1
kind: SeccompProfile
metadata:
  name: webapp-allowlist
  namespace: security
spec:
  defaultAction: SCMP_ACT_ERRNO
  defaultErrnoRet: 1
  archs:
    - SCMP_ARCH_X86_64
  syscalls:
    - action: SCMP_ACT_ALLOW
      names:
        - read
        - write
        - openat
        - close
        - mmap
        - mprotect
        - exit_group

The operator then writes the profile file to each node and exposes it at the standard path /var/lib/kubelet/seccomp/operator/<namespace>/<name>.json. Your pod just references it: localhostProfile: operator/security/webapp-allowlist.json. When a new node joins, the operator automatically places its profiles — nothing is missed.

Important

Before adopting Localhost, make sure you have an answer to one question: what happens when a new node joins? If the answer is "lucky guess", use RuntimeDefault first or install an operator soon. A pod pointing to a profile that doesn't exist will fail to schedule — and that failure hurts when you're on-call at night.

Verifying on the Node

A declared profile isn't necessarily a present profile. Verify on the node where the pod runs:

KubernetesCheck the profile file and runtime on the node
ls -la /var/lib/kubelet/seccomp/
crictl info | grep seccomp

crictl is the CLI for containerd — crictl info | grep seccomp confirms the node runtime has seccomp support enabled. Combine it with inspecting a running pod via kubectl describe:

KubernetesView a pod's seccomp status
kubectl describe pod webapp | grep -i seccomp

These three verification layers — the file exists, the runtime supports it, the pod references it — close the loop from declaration to execution.

Conclusion

In episode 10 you understood how to apply seccomp in Kubernetes: the seccompProfile field with three types (Unconfined, RuntimeDefault, Localhost) that replaced the alpha annotation, its relation to Pod Security Standards (Baseline and Restricted), and the challenge of distributing profiles across nodes, answered with seccomp-profiles-operator. You also know how to verify profiles on a node via crictl and kubectl describe.

The keys to take home:

  • Start with RuntimeDefault; move up to Localhost once the workload is understood.
  • Enforce via Pod Security Standards, not per-person discipline.
  • A Localhost profile must exist on all nodes — use an operator or answer that question first.

So far seccomp comes from the outside: containers, service units, pods. But there's one approach closer to the defensive line — inside the application itself. In episode 11, we move into Seccomp for Applications & Daemons: loading filters via libseccomp, python-seccomp, and Go, restricting socket families for network daemons, and blocking dangerous syscalls like unshare, mount, and ptrace.