Use seccomp Profiles in Kubernetes

Apply Kubernetes seccompProfile types Unconfined, RuntimeDefault, and Localhost on Pods and containers, install a custom profile on the kubelet seccomp path, and diagnose denied syscalls with logs and exit codes.

Published

Updated

Read time 10 min read

Reviewed byDeepak Prasad

Kubernetes seccomp profiles comparing Unconfined, RuntimeDefault, and Localhost syscall filtering on a Linux worker node
Tested on Rocky Linux 10.2 (Red Quartz) — kubeadm worker node with containerd
Package kubectl 1.36.3
kubelet 1.36.3
containerd 2.2.5
Applies to Ubuntu, Debian, Kali Linux, Linux Mint, Pop!_OS, Raspberry Pi OS, elementary OS, Zorin OS, Parrot OS, MX Linux, RHEL, Rocky Linux, AlmaLinux, Oracle Linux, CentOS Stream, Fedora
Kubernetes cluster
Cert prep CKS
Lab environment Multi-node kubeadm cluster with containerd and SSH node access — install Kubernetes with kubeadm
Privilege root on worker nodes to install profile files under /var/lib/kubelet/seccomp/; cluster-admin kubectl to create test Pods
Scope seccomp concepts, prerequisite checks, Unconfined versus RuntimeDefault versus Localhost, Pod and container seccompProfile YAML, custom profile that denies chmod, kubelet profile path, denial testing, multi-node distribution notes, and troubleshooting. Does not cover Linux capabilities, AppArmor, full OCI seccomp JSON reference, profile-generation tooling, runtime installation, or offensive syscall research.
Related guides Harden Linux nodes for Kubernetes
Kubernetes security architecture

seccomp (secure computing mode) filters which Linux system calls a container process may invoke. Kubernetes exposes that filter through seccompProfile on the Pod or container securityContext. This walkthrough compares Unconfined, RuntimeDefault, and Localhost on a kubeadm lab worker, deploys a custom profile that blocks chmod, and reads the resulting logs and exit codes.

IMPORTANT
This guide configures seccomp syscall filtering on Linux nodes. It does not cover capability add/drop, AppArmor profiles, or full SecurityContext hardening. Privileged containers always run seccomp unconfined regardless of the YAML you set.

What seccomp restricts

seccomp sits between your application and the Linux kernel:

  • It filters system calls (not individual files or network ports).
  • It reduces kernel attack surface when a workload is compromised.
  • It applies per Pod or per container through securityContext.seccompProfile.
  • It complements capabilities, AppArmor or SELinux, and sandboxed runtimes such as gVisor.

SCMP_ACT_ERRNO, used in this lab, returns an errno such as EPERM. Other seccomp actions may log the call, send SIGSYS, notify userspace, or terminate the process. In shell utilities you often see Operation not permitted. The container keeps running unless the application treats that error as fatal.


Verify prerequisites

seccomp filtering requires Linux nodes with kernel and runtime support. On a worker node, read the kernel build configuration from /boot (Rocky Linux and RHEL do not ship /proc/config.gz by default):

bash
grep -E '^CONFIG_SECCOMP(_FILTER)?=' "/boot/config-$(uname -r)"
output
CONFIG_SECCOMP=y
CONFIG_SECCOMP_FILTER=y

If /boot/config-$(uname -r) is missing but /proc/config.gz exists, use it as a fallback:

bash
zgrep -E '^CONFIG_SECCOMP(_FILTER)?=' /proc/config.gz

Confirm the cluster version and runtime from the control plane:

bash
export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl version
output
Client Version: v1.36.3
Server Version: v1.36.3

Kubelet stores Localhost profiles under /var/lib/kubelet/seccomp/ by default. The directory may not exist until you create it:

bash
ls -ld /var/lib/kubelet/seccomp/ 2>/dev/null || echo "create before Localhost profiles"

Check whether the kubelet applies a default seccomp profile when the Pod spec omits one:

bash
grep '^seccompDefault:' /var/lib/kubelet/config.yaml ||
  echo "seccompDefault omitted; default is false"
output
seccompDefault omitted; default is false

seccompDefault is configured per kubelet and defaults to false. When seccompDefault is absent or false in the kubelet configuration, containers without an explicit profile run as Unconfined. When seccompDefault: true, the kubelet applies RuntimeDefault. Pod Security Admission does not set or mutate seccomp profiles; the Restricted policy rejects Pods that do not explicitly request RuntimeDefault or Localhost.


Understand profile types

Type Behavior
Unconfined No seccomp filtering applied by Kubernetes for that scope
RuntimeDefault Uses the container runtime default OCI seccomp profile
Localhost Loads a JSON profile from the kubelet seccomp directory on the node

Privileged containers ignore your seccompProfile and run unconfined. Pod Security Standards Restricted expects RuntimeDefault or Localhost. Pod Security Admission checks the selected type but does not inspect whether the Localhost JSON is actually stricter—see Pod Security Standards for admission context.


Prepare the lab namespace

Create an isolated namespace for test Pods:

bash
kubectl create namespace seccomp-lab --dry-run=client -o yaml | kubectl apply -f -
output
namespace/seccomp-lab created

Pin workloads to the worker where you install Localhost profiles:

yaml
nodeSelector:
  kubernetes.io/hostname: worker01

Remove the selector after you distribute profiles to every node in production.


Apply RuntimeDefault to a Pod

RuntimeDefault is the usual production baseline when you do not need a custom syscall list. Apply a Pod that runs chmod under the runtime profile:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: seccomp-runtime-default
  namespace: seccomp-lab
spec:
  restartPolicy: Never
  nodeSelector:
    kubernetes.io/hostname: worker01
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: test
    image: busybox:1.36
    command: ["sh", "-c", "touch /tmp/f && chmod 600 /tmp/f && echo chmod-ok"]

Deploy and read the result:

bash
kubectl apply -f seccomp-runtime-default.yaml
bash
kubectl wait -n seccomp-lab \
  --for=jsonpath='{.status.phase}'=Succeeded \
  pod/seccomp-runtime-default \
  --timeout=60s
bash
kubectl get pod -n seccomp-lab seccomp-runtime-default
bash
kubectl logs -n seccomp-lab seccomp-runtime-default
output
NAME                      READY   STATUS      RESTARTS   AGE
seccomp-runtime-default   0/1     Completed   0          8s
chmod-ok

On containerd 2.2.x with Kubernetes 1.36, the runtime default profile still allows chmod for this busybox test. That is expected—RuntimeDefault is the runtime curated list, not an empty allow-all policy.

Confirm the runtime applied a seccomp policy on worker01. Select the Pod sandbox first so you inspect the container you just created:

bash
POD_ID=$(crictl pods \
  --namespace seccomp-lab \
  --name seccomp-runtime-default \
  -q | head -1)

CONTAINER_ID=$(crictl ps -a \
  --pod "$POD_ID" \
  --name test \
  -q | head -1)

crictl inspect "$CONTAINER_ID" |
  jq '.info.runtimeSpec.linux.seccomp'
output
{
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_X32"
  ],
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "action": "SCMP_ACT_ALLOW",
      "names": [
        "accept",
        "accept4",
        "access",
        ...
      ]
    }
  ]
}

RuntimeDefault produces a populated seccomp policy in the OCI runtime spec.


Compare Unconfined

Unconfined removes seccomp filtering for the Pod scope. Use it only when a legacy workload truly cannot run under a profile:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: seccomp-unconfined
  namespace: seccomp-lab
spec:
  restartPolicy: Never
  nodeSelector:
    kubernetes.io/hostname: worker01
  securityContext:
    seccompProfile:
      type: Unconfined
  containers:
  - name: test
    image: busybox:1.36
    command: ["sh", "-c", "touch /tmp/f && chmod 600 /tmp/f && echo chmod-ok"]

Deploy and confirm the same chmod test completes:

bash
kubectl apply -f seccomp-unconfined.yaml
bash
kubectl wait -n seccomp-lab \
  --for=jsonpath='{.status.phase}'=Succeeded \
  pod/seccomp-unconfined \
  --timeout=60s
bash
kubectl logs -n seccomp-lab seccomp-unconfined
output
chmod-ok

Both Unconfined and RuntimeDefault print chmod-ok, so logs alone do not prove a difference. Inspect the runtime spec on worker01:

bash
POD_ID=$(crictl pods \
  --namespace seccomp-lab \
  --name seccomp-unconfined \
  -q | head -1)

CONTAINER_ID=$(crictl ps -a \
  --pod "$POD_ID" \
  --name test \
  -q | head -1)

crictl inspect "$CONTAINER_ID" |
  jq '.info.runtimeSpec.linux.seccomp'
output
null

Unconfined leaves linux.seccomp unset in the runtime spec—no seccomp policy is applied.

Keep Unconfined off production paths unless you document an exception. It is wider than RuntimeDefault and fails Restricted policy checks.


Create a Localhost profile

Author an OCI seccomp JSON profile on the node. This lab denies chmod, fchmod, and fchmodat while allowing everything else:

bash
cat > deny-chmod.json <<'EOF'
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": ["chmod", "fchmod", "fchmodat"],
      "action": "SCMP_ACT_ERRNO",
      "errnoRet": 1
    }
  ]
}
EOF

This deny-list profile exists only to demonstrate an observable syscall failure. It is not a production allow-list and should not be treated as stronger than RuntimeDefault. Restricted policy permits Localhost, but does not evaluate profile contents.

Pick a syscall your test binary actually calls so you can observe a clean failure without blocking container startup syscalls such as execve or openat.


Install the profile on the node

Create the kubelet seccomp tree and install the file on every node that may run the Pod:

bash
mkdir -p /var/lib/kubelet/seccomp/profiles
install -m 0644 deny-chmod.json /var/lib/kubelet/seccomp/profiles/deny-chmod.json
ls -l /var/lib/kubelet/seccomp/profiles/deny-chmod.json
output
-rw-r--r--. 1 root root 175 Jul 28 03:24 /var/lib/kubelet/seccomp/profiles/deny-chmod.json

Kubelet resolves Localhost paths relative to /var/lib/kubelet/seccomp/. A missing file surfaces at container creation time—not at schedule time.


Reference the Localhost profile in YAML

Point localhostProfile at the path relative to the seccomp root. Do not prefix /var/lib/kubelet/seccomp/ in the manifest. Kubelet joins the root and relative path when loading the profile.

Deploy a Pod that uses the installed profile. Localhost profile availability is checked during container creation:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: seccomp-localhost
  namespace: seccomp-lab
spec:
  restartPolicy: Never
  nodeSelector:
    kubernetes.io/hostname: worker01
  securityContext:
    seccompProfile:
      type: Localhost
      localhostProfile: profiles/deny-chmod.json
  containers:
  - name: test
    image: busybox:1.36
    command: ["sh", "-c", "set -e; touch /tmp/f; chmod 600 /tmp/f; echo chmod-ok"]

Apply the manifest and wait for the denied chmod to fail the container:

bash
kubectl apply -f seccomp-localhost.yaml
bash
kubectl wait -n seccomp-lab \
  --for=jsonpath='{.status.phase}'=Failed \
  pod/seccomp-localhost \
  --timeout=60s
bash
kubectl get pod -n seccomp-lab seccomp-localhost
bash
kubectl logs -n seccomp-lab seccomp-localhost
bash
kubectl get pod -n seccomp-lab seccomp-localhost -o jsonpath='{.status.containerStatuses[0].state.terminated.exitCode}{"\n"}'
output
NAME                READY   STATUS   RESTARTS   AGE
seccomp-localhost   0/1     Error    0          8s
chmod: /tmp/f: Operation not permitted
1

Inspect the applied Localhost policy on worker01:

bash
POD_ID=$(crictl pods \
  --namespace seccomp-lab \
  --name seccomp-localhost \
  -q | head -1)

CONTAINER_ID=$(crictl ps -a \
  --pod "$POD_ID" \
  --name test \
  -q | head -1)

crictl inspect "$CONTAINER_ID" |
  jq '.info.runtimeSpec.linux.seccomp'
output
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "action": "SCMP_ACT_ERRNO",
      "errnoRet": 1,
      "names": [
        "chmod",
        "fchmod",
        "fchmodat"
      ]
    }
  ]
}

The runtime spec contains the custom deny rule for chmod, fchmod, and fchmodat.


Override one container in a multi-container Pod

Pod-level seccompProfile sets the default for every container. A container-level securityContext overrides only that container:

yaml
apiVersion: v1
kind: Pod
metadata:
  name: seccomp-multi
  namespace: seccomp-lab
spec:
  restartPolicy: Never
  nodeSelector:
    kubernetes.io/hostname: worker01
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: default-profile
    image: busybox:1.36
    command: ["sh", "-c", "touch /tmp/a && chmod 600 /tmp/a && echo default-ok"]
  - name: localhost-profile
    image: busybox:1.36
    securityContext:
      seccompProfile:
        type: Localhost
        localhostProfile: profiles/deny-chmod.json
    command: ["sh", "-c", "set -e; touch /tmp/b; chmod 600 /tmp/b; echo localhost-ok"]

Deploy and read each container log after the Pod finishes:

bash
kubectl apply -f seccomp-multi.yaml
bash
kubectl wait -n seccomp-lab \
  --for=jsonpath='{.status.phase}'=Failed \
  pod/seccomp-multi \
  --timeout=60s
bash
kubectl logs -n seccomp-lab seccomp-multi -c default-profile
bash
kubectl logs -n seccomp-lab seccomp-multi -c localhost-profile
output
default-ok
chmod: /tmp/b: Operation not permitted

The first container inherited RuntimeDefault and completed. The second container used the Localhost profile and failed on chmod while the Pod-level setting still applied to the sibling.


Compare the three modes

Profile chmod result Pod phase
Unconfined chmod-ok Succeeded
RuntimeDefault chmod-ok Succeeded
Localhost deny-chmod Operation not permitted, exit 1 Failed

The blocked syscall is chmod. For syscall logging in production, use SCMP_ACT_LOG in the profile or the Security Profiles Operator recording features—not the SCMP_ACT_ERRNO denial used in this lab.


Distribute profiles across nodes

Localhost profiles are node-local files. A Pod scheduled to a node without profiles/deny-chmod.json fails with CreateContainerError:

output
Warning  Failed  kubelet  Error: failed to create containerd container: cannot load seccomp profile "/var/lib/kubelet/seccomp/profiles/does-not-exist.json": open ... no such file or directory

Distribution options:

  • Configuration management (Ansible, Salt, cloud-init) copying versioned JSON into /var/lib/kubelet/seccomp/profiles/
  • The Security Profiles Operator
  • Node labels and taints during rollout so Pods land only on nodes that already have the profile
  • Filename versioning (deny-chmod-v2.json) so rollbacks do not overwrite files in use

Troubleshooting

Symptom Likely cause Fix
CreateContainerError on start Missing or invalid Localhost JSON Confirm file path under /var/lib/kubelet/seccomp/; validate JSON syntax
Operation not permitted at runtime Profile denies the syscall your app needs Adjust profile allow list or pick a narrower test syscall
Works on one node only Profile not copied to all workers Distribute file with CM or Security Profiles Operator
seccompProfile ignored Container is privileged: true Remove privileged; use capabilities instead
RuntimeDefault blocks unexpected syscall Runtime profile differs by containerd or CRI-O version Test on target runtime; fall back to explicit Localhost profile
Pod Security Admission rejects spec Namespace enforces Restricted Use RuntimeDefault or Localhost; avoid Unconfined

What's Next


References


Summary

You compared three seccomp modes on a Linux worker: Unconfined and RuntimeDefault both let the lab chmod test succeed, while a Localhost profile installed at /var/lib/kubelet/seccomp/profiles/deny-chmod.json returned Operation not permitted and exit code 1. crictl inspect on worker01 showed that RuntimeDefault applied a populated seccomp policy, Unconfined left seccomp unset, and the Localhost container carried the custom deny rule. Pod-level seccompProfile sets the default; container-level settings override it for individual containers in the same Pod.

The operational gotcha is node locality—localhostProfile is a relative path on the kubelet host, not a cluster object. A missing file produces CreateContainerError with the full absolute path in the event message, which is the fastest way to spot a half-finished rollout.

For production, start with RuntimeDefault on new workloads, add Localhost profiles only where you have tested syscall requirements, and distribute JSON with configuration management or the Security Profiles Operator. Pair seccomp with SecurityContext and Pod Security Standards so admission and syscall filtering tell the same story.


Frequently Asked Questions

1. What is the difference between RuntimeDefault and Localhost seccomp in Kubernetes?

RuntimeDefault applies the container runtime built-in seccomp profile for the image architecture. Localhost loads a JSON profile from the node filesystem under the kubelet seccomp directory, referenced by a relative path such as profiles/demo.json. RuntimeDefault needs no node file; Localhost requires the same file on every node that can run the Pod.

2. Where do Localhost seccomp profiles live on the node?

Kubelet reads Localhost profiles from its seccomp root, defaulting to /var/lib/kubelet/seccomp/. The localhostProfile field is relative to that directory. On this lab cluster a missing file produced CreateContainerError with the full path /var/lib/kubelet/seccomp/profiles/does-not-exist.json in the kubelet event message.

3. Does seccomp apply to privileged containers?

Privileged containers run with an unconfined seccomp profile regardless of the seccompProfile you set in the Pod spec. To enforce syscall filtering you must avoid privileged mode and use RuntimeDefault or a tuned Localhost profile instead.

4. How do I tell seccomp blocked a syscall?

The process usually receives EPERM or EACCES from the blocked syscall. In shell tests you see Operation not permitted. The container may exit non-zero when the script uses set -e. Check kubectl logs, container exitCode and reason, and kubelet or audit logs for seccomp denial messages when logging is enabled.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)