CKS — Seccomp Profiles
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: The Recommended Baseline
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:
RuntimeDefaultis 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 localhostProfilevalue 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 attemptskexec_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 |