CKS — Seccomp Profiles

10 minute read

Seccomp Profiles

Seccomp (Security Computing Mode) is a Linux kernel feature that filters system calls made by a process. Containers need a small fraction of the ~300+ available syscalls — Seccomp blocks the rest, preventing attackers from abusing dangerous syscalls even after gaining code execution.

AppArmor controls file and network access. Seccomp controls which kernel operations a process can perform.


1. Why Seccomp Matters

Without Seccomp:
  Container Process → Kernel (all ~300 syscalls available)
                      ├── read()      ✓
                      ├── write()     ✓
                      ├── open()      ✓
                      ├── mount()     ✓  ← Dangerous
                      ├── ptrace()    ✓  ← Dangerous
                      ├── reboot()    ✓  ← Dangerous
                      └── ...

With Seccomp (RuntimeDefault):
  Container Process → Seccomp Filter → Kernel
                      ├── read()      ✓  Allowed
                      ├── write()     ✓  Allowed
                      ├── open()      ✓  Allowed
                      ├── mount()     ✗  BLOCKED
                      ├── ptrace()    ✗  BLOCKED
                      ├── reboot()    ✗  BLOCKED
                      └── ...

Dangerous Syscalls Blocked by Default Profiles

Syscall What It Does Why It’s Dangerous
mount Mount filesystems Escape container by mounting host filesystem
ptrace Trace other processes Debug/inject into other containers
reboot Reboot the system Denial of service
kexec_load Load a new kernel Replace the running kernel
unshare Create new namespaces Namespace escape
keyctl Kernel key management Access other containers’ secrets
bpf Load BPF programs Kernel-level code injection

2. Seccomp Profile Types

Type Description Use Case
Unconfined No Seccomp filter applied Default (no protection) — avoid in production
RuntimeDefault Container runtime’s built-in profile Recommended baseline — blocks ~50 dangerous syscalls
Localhost Custom JSON profile on the node Fine-grained control for specific workloads

RuntimeDefault uses the container runtime’s (containerd/CRI-O) pre-built Seccomp profile. It blocks dangerous syscalls while allowing everything a typical container needs.

RuntimeDefault blocks (subset):
  ├── mount, umount, umount2
  ├── ptrace
  ├── reboot
  ├── kexec_load, kexec_file_load
  ├── unshare
  ├── keyctl
  ├── bpf
  ├── userfaultfd
  ├── add_key, request_key
  └── ... (~50 syscalls total)

Exam Tip: RuntimeDefault is the recommended starting point for all workloads. The CKS exam may ask you to apply it to a Pod.


3. Applying Seccomp to Pods

Pod-Level Seccomp (applies to all containers)

apiVersion: v1
kind: Pod
metadata:
  name: secure-pod
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: nginx:1.27

Container-Level Seccomp (per container)

apiVersion: v1
kind: Pod
metadata:
  name: multi-container
spec:
  containers:
  - name: web
    image: nginx:1.27
    securityContext:
      seccompProfile:
        type: RuntimeDefault
  - name: sidecar
    image: busybox
    command: ["sh", "-c", "sleep infinity"]
    securityContext:
      seccompProfile:
        type: Unconfined    # This container has no Seccomp

Using a Localhost Profile

apiVersion: v1
kind: Pod
metadata:
  name: custom-seccomp-pod
spec:
  containers:
  - name: app
    image: nginx:1.27
    securityContext:
      seccompProfile:
        type: Localhost
        localhostProfile: profiles/my-custom-profile.json

The profile path is relative to the kubelet’s Seccomp profile root:

/var/lib/kubelet/seccomp/profiles/my-custom-profile.json
                  ^^^^^^^^^^^^^^^
                  kubelet seccomp root

4. Custom Seccomp Profiles

Custom profiles are JSON files that define exactly which syscalls are allowed or denied.

Profile Structure

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86",
    "SCMP_ARCH_AARCH64"
  ],
  "syscalls": [
    {
      "names": ["syscall1", "syscall2"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Actions

Action Behavior
SCMP_ACT_ALLOW Allow the syscall
SCMP_ACT_ERRNO Block the syscall and return an error to the caller
SCMP_ACT_LOG Allow the syscall but log it (useful for auditing)
SCMP_ACT_KILL Kill the process immediately
SCMP_ACT_KILL_PROCESS Kill the entire process (not just the thread)
SCMP_ACT_TRACE Notify a ptrace tracer

Strategy: Allow List vs Deny List

Strategy defaultAction Syscall Actions Security
Allow list (recommended) SCMP_ACT_ERRNO SCMP_ACT_ALLOW for needed syscalls High — blocks everything not listed
Deny list SCMP_ACT_ALLOW SCMP_ACT_ERRNO for dangerous syscalls Lower — new dangerous syscalls not covered

Best Practice: Use an allow-list approach. Start with defaultAction: SCMP_ACT_ERRNO (deny all) and explicitly allow only what the application needs.

Example 1: Allow-List Profile for a Simple Container

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_AARCH64"
  ],
  "syscalls": [
    {
      "names": [
        "accept",
        "accept4",
        "access",
        "arch_prctl",
        "bind",
        "brk",
        "close",
        "connect",
        "dup",
        "dup2",
        "dup3",
        "epoll_create",
        "epoll_create1",
        "epoll_ctl",
        "epoll_wait",
        "execve",
        "exit",
        "exit_group",
        "fcntl",
        "fstat",
        "futex",
        "getcwd",
        "getdents64",
        "getpid",
        "getppid",
        "getsockname",
        "getsockopt",
        "ioctl",
        "listen",
        "lseek",
        "mmap",
        "mprotect",
        "munmap",
        "nanosleep",
        "newfstatat",
        "openat",
        "pipe2",
        "poll",
        "pread64",
        "read",
        "readlink",
        "recvfrom",
        "rt_sigaction",
        "rt_sigprocmask",
        "rt_sigreturn",
        "sendto",
        "set_robust_list",
        "set_tid_address",
        "setsockopt",
        "socket",
        "stat",
        "uname",
        "write",
        "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Example 2: Deny-List Profile (Block Specific Syscalls)

{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": [
        "mount",
        "umount",
        "umount2",
        "ptrace",
        "reboot",
        "kexec_load",
        "kexec_file_load",
        "unshare",
        "keyctl",
        "bpf",
        "add_key",
        "request_key",
        "userfaultfd"
      ],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}

Example 3: Log-Only Profile (Audit Mode)

Useful for discovering which syscalls an application actually uses before creating an allow list.

{
  "defaultAction": "SCMP_ACT_LOG",
  "syscalls": [
    {
      "names": [
        "mount",
        "ptrace",
        "reboot",
        "kexec_load"
      ],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}

Check the audit log:

# On the node
sudo grep -i seccomp /var/log/audit/audit.log
# Or
sudo dmesg | grep seccomp

5. Deploying Custom Profiles

Step 1: Create the Profile on the Node

# SSH to the node
ssh admin@node01

# Create the directory
sudo mkdir -p /var/lib/kubelet/seccomp/profiles

# Create the profile
cat <<'EOF' | sudo tee /var/lib/kubelet/seccomp/profiles/deny-mount.json
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": ["mount", "umount", "umount2", "ptrace", "reboot"],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}
EOF

Step 2: Deploy a Pod Using the Profile

# custom-seccomp-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: seccomp-test
spec:
  containers:
  - name: test
    image: nginx:1.27
    securityContext:
      seccompProfile:
        type: Localhost
        localhostProfile: profiles/deny-mount.json
kubectl apply -f custom-seccomp-pod.yaml

Step 3: Verify the Profile

# Check Pod status
kubectl get pod seccomp-test
# NAME           READY   STATUS    RESTARTS   AGE
# seccomp-test   1/1     Running   0          5s

# Verify Seccomp is applied
kubectl exec seccomp-test -- cat /proc/1/status | grep Seccomp
# Seccomp:	2
# Seccomp_filters:	1

Seccomp status values in /proc/PID/status:

Value Meaning
0 Disabled (unconfined)
1 Strict mode (only read, write, exit, sigreturn)
2 Filter mode (custom BPF filter — this is what Kubernetes uses)

Step 4: Test Enforcement

# Try to mount (should fail)
kubectl exec seccomp-test -- mount -t tmpfs tmpfs /mnt
# mount: /mnt: permission denied.

# Normal operations should work
kubectl exec seccomp-test -- cat /etc/hostname
# seccomp-test

6. Generating Seccomp Profiles

Manually writing allow-list profiles is tedious. Tools can trace an application and generate profiles automatically.

Using strace to Find Required Syscalls

# Run the application with strace to list all syscalls used
strace -cf -o /tmp/syscall-trace nginx -g 'daemon off;'

# -c = count syscalls (summary)
# -f = follow forks
# -o = output to file

# Extract unique syscall names
strace -f nginx -g 'daemon off;' 2>&1 | \
  grep -oP '^\w+' | sort -u

Using Security Profiles Operator

The Security Profiles Operator (SPO) can record and manage Seccomp profiles within Kubernetes.

# Install the operator, then create a recording
apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
kind: SeccompProfile
metadata:
  name: my-app-profile
  namespace: app-ns
spec:
  defaultAction: SCMP_ACT_ERRNO
  architectures:
  - SCMP_ARCH_X86_64
  syscalls:
  - action: SCMP_ACT_ALLOW
    names:
    - read
    - write
    - openat
    - close
    - fstat
    - mmap
    - brk
    - rt_sigaction
    - access
    - execve
    - exit_group

Profile Recording with SPO

apiVersion: security-profiles-operator.x-k8s.io/v1alpha1
kind: ProfileRecording
metadata:
  name: record-my-app
  namespace: app-ns
spec:
  kind: SeccompProfile
  recorder: logs
  podSelector:
    matchLabels:
      app: my-app

After recording, the operator generates a SeccompProfile resource you can review and apply.


7. Seccomp + Pod Security Standards

Pod Security Standards (PSS) enforce Seccomp requirements:

Level Seccomp Requirement
Privileged No restriction
Baseline No restriction (allows Unconfined)
Restricted Must set seccompProfile.type to RuntimeDefault or Localhost

If your namespace enforces the Restricted level, all Pods must have a Seccomp profile:

apiVersion: v1
kind: Namespace
metadata:
  name: secure-ns
  labels:
    pod-security.kubernetes.io/enforce: restricted
# This Pod will be REJECTED in a restricted namespace (no Seccomp)
apiVersion: v1
kind: Pod
metadata:
  name: no-seccomp
  namespace: secure-ns
spec:
  containers:
  - name: app
    image: nginx:1.27
# Error: seccompProfile must be set to RuntimeDefault or Localhost
# This Pod will be ALLOWED
apiVersion: v1
kind: Pod
metadata:
  name: with-seccomp
  namespace: secure-ns
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
    runAsNonRoot: true
    runAsUser: 1000
  containers:
  - name: app
    image: nginx:1.27
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]

8. Common CKS Exam Patterns

Pattern 1: Apply RuntimeDefault

Task: Ensure all containers in Pod "web" use RuntimeDefault Seccomp.

Answer:
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault

Pattern 2: Deploy with Custom Profile

Task: A Seccomp profile is at /var/lib/kubelet/seccomp/profiles/audit.json
      on all nodes. Apply it to a Pod.

Answer:
spec:
  containers:
  - name: app
    securityContext:
      seccompProfile:
        type: Localhost
        localhostProfile: profiles/audit.json

Pattern 3: Create + Deploy Profile

Task: Create a Seccomp profile that blocks mount and ptrace,
      then apply it to a Pod.

Steps:
1. SSH to node
2. Create /var/lib/kubelet/seccomp/profiles/block-dangerous.json
3. Create Pod YAML with Localhost profile
4. Apply and verify

Pattern 4: Debug Why a Pod Crashes

Task: Pod is crashing with "operation not permitted" errors.
      A Seccomp profile is too restrictive.

Steps:
1. Check /proc/1/status for Seccomp mode
2. Check node logs: dmesg | grep seccomp / audit.log
3. Identify blocked syscall
4. Add it to the profile's allow list
5. Reload profile and recreate Pod

Summary

Component Detail
What Syscall filtering — controls which kernel operations a process can perform
Profile location /var/lib/kubelet/seccomp/profiles/
Types Unconfined, RuntimeDefault, Localhost
Recommended default RuntimeDefault
Apply to Pod securityContext.seccompProfile.type: RuntimeDefault
Custom profile type: Localhost, localhostProfile: profiles/name.json
Profile format JSON with defaultAction and syscalls array
Verify cat /proc/1/status \| grep Seccomp → value 2 = filter mode
Logs dmesg \| grep seccomp or audit.log

Practice Questions

Q1: Apply the RuntimeDefault Seccomp profile to all containers in the following Pod.

Original Pod:

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
  - name: nginx
    image: nginx:1.27
  - name: sidecar
    image: busybox
    command: ["sleep", "infinity"]

Updated Pod:

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: nginx
    image: nginx:1.27
  - name: sidecar
    image: busybox
    command: ["sleep", "infinity"]

Adding seccompProfile at the Pod level applies it to all containers.

Q2: Write a custom Seccomp profile that blocks mount, ptrace, reboot, and kexec_load syscalls, allowing everything else. Where should this file be placed?

Profile (/var/lib/kubelet/seccomp/profiles/block-dangerous.json):

{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": [
        "mount",
        "umount",
        "umount2",
        "ptrace",
        "reboot",
        "kexec_load",
        "kexec_file_load"
      ],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}

Placement:

  • Full path: /var/lib/kubelet/seccomp/profiles/block-dangerous.json
  • localhostProfile value in YAML: profiles/block-dangerous.json (relative to /var/lib/kubelet/seccomp/)
sudo mkdir -p /var/lib/kubelet/seccomp/profiles
sudo tee /var/lib/kubelet/seccomp/profiles/block-dangerous.json <<'EOF'
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": ["mount","umount","umount2","ptrace","reboot","kexec_load","kexec_file_load"],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}
EOF
Q3: A Pod is crashing because its Seccomp profile is blocking a syscall the application needs. How do you identify the blocked syscall and fix the profile?

Step 1: Check node kernel logs for Seccomp denials:

# Find which node the Pod runs on
kubectl get pod crashing-pod -o wide

# SSH to the node
ssh admin@node01

# Check for Seccomp audit entries
sudo dmesg | grep -i seccomp
# audit: type=1326 audit(...): auid=... uid=0 ... syscall=157 ... sig=0 arch=c000003e
# syscall=157 is prctl

sudo ausearch -m seccomp 2>/dev/null || sudo grep seccomp /var/log/audit/audit.log

Step 2: Decode the syscall number:

# Map syscall number to name
ausyscall 157
# prctl

# Or check the syscall table
grep 157 /usr/include/asm/unistd_64.h

Step 3: Add the syscall to the profile:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": ["read", "write", "...", "prctl"],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Step 4: Recreate the Pod:

kubectl delete pod crashing-pod
kubectl apply -f crashing-pod.yaml
Q4: What is the difference between SCMP_ACT_ERRNO and SCMP_ACT_KILL? When would you use each?
Action Behavior Application Impact
SCMP_ACT_ERRNO Blocks the syscall and returns an error code (EPERM) to the process Process receives an error, can handle it gracefully (e.g., retry, log, continue)
SCMP_ACT_KILL Immediately kills the thread that made the syscall Process crashes with SIGSYS — no chance to handle the error

When to use each:

  • SCMP_ACT_ERRNO (recommended default action) — the process receives a “Permission denied” error. Well-written applications handle this gracefully. This gives you security blocking without crashes.

  • SCMP_ACT_KILL — use for extremely dangerous syscalls where you want zero tolerance. If a process attempts kexec_load, you want it killed immediately, not given a chance to try something else.

Example combining both:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "syscalls": [
    {
      "names": ["read", "write", "openat", "close"],
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "names": ["kexec_load", "reboot"],
      "action": "SCMP_ACT_KILL"
    }
  ]
}
Q5: How do you verify that a running Pod has a Seccomp profile applied? Show the command and explain the output.

Method 1: Check /proc/PID/status inside the container:

kubectl exec seccomp-pod -- cat /proc/1/status | grep -i seccomp

Output:

Seccomp:	2
Seccomp_filters:	1
Seccomp Value Meaning
0 Disabled (SECCOMP_MODE_DISABLED) — no filter
1 Strict (SECCOMP_MODE_STRICT) — only read, write, exit, sigreturn
2 Filter (SECCOMP_MODE_FILTER) — custom BPF filter applied ← this is what Kubernetes uses

Seccomp_filters: 1 means one filter is loaded.

Method 2: Check Pod spec:

kubectl get pod seccomp-pod -o jsonpath='{.spec.securityContext.seccompProfile}'
# {"type":"RuntimeDefault"}

# Or per container:
kubectl get pod seccomp-pod -o jsonpath='{.spec.containers[0].securityContext.seccompProfile}'

Method 3: Check node logs for filter loading:

sudo dmesg | grep seccomp

Previous Up Next
AppArmor System Hardening System Hardening Lab

Updated: