Skip to content

Seccomp, Capabilities & Syscall Filtering

Chapter 18: Seccomp, Capabilities & Syscall Filtering

Section titled “Chapter 18: Seccomp, Capabilities & Syscall Filtering”
  • Understand seccomp and how it restricts system calls
  • Know how Docker and Kubernetes use seccomp profiles
  • Write custom seccomp profiles
  • Understand the relationship between capabilities and seccomp

PAM (Pluggable Authentication Modules) handles how users log in, and Capabilities divide up ‘root’ powers. Instead of giving a program full root access (which is dangerous), you can give it just one specific Capability, like the ability to open network ports, keeping the system much safer.

seccomp (Secure Computing Mode) filters which system calls a process can make. Even if an attacker achieves code execution, they’re limited to a small set of syscalls.

Seccomp Architecture
─────────────────────
Application calls: open("/etc/shadow", O_RDONLY)
▼ glibc
syscall: openat(AT_FDCWD, "/etc/shadow", O_RDONLY)
▼ Kernel seccomp filter
┌─────────────────────────────────────────────────────┐
│ Is 'openat' in the allowed syscall list? │
│ │
│ YES → syscall proceeds │
│ NO → SIGKILL or SIGSYS sent to process │
│ (depends on filter action) │
└─────────────────────────────────────────────────────┘
Seccomp uses BPF (Berkeley Packet Filter) programs
to evaluate syscalls — extremely efficient.
seccomp Modes:
───────────────
SECCOMP_MODE_DISABLED: No filtering (default)
SECCOMP_MODE_STRICT: Only allow read(), write(), exit(), sigreturn()
- Extremely restrictive, almost unusable
SECCOMP_MODE_FILTER: Use BPF filter to allow/deny specific syscalls
- What Docker, Kubernetes, and sandboxes use

Docker ships with a default seccomp profile that blocks ~44 dangerous syscalls:

// /etc/docker/seccomp.json (simplified excerpt)
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": [
"keyctl", // Access kernel keyrings
"add_key", // Add key to kernel keyring
"request_key", // Request key from keyring
"ptrace", // Trace other processes
"mbind", // NUMA memory policy
"move_pages", // Move pages between NUMA nodes
"get_mempolicy",
"set_mempolicy",
"userfaultfd", // User-space page fault handling
"perf_event_open", // Performance monitoring
"kcmp", // Compare process file structures
"mount", // Mount filesystems
"umount2",
"swapon",
"swapoff",
"kexec_load", // Load new kernel
"kexec_file_load",
"reboot", // Reboot system
"init_module", // Load kernel module
"finit_module",
"delete_module",
"clock_settime", // Set system clock
"settimeofday",
"stime",
"adjtimex"
],
"action": "SCMP_ACT_ERRNO" // Return EPERM
}
]
}
Terminal window
# Run container with default seccomp profile (auto-applied by Docker)
docker run nginx
# Run with custom seccomp profile
docker run --security-opt seccomp=/path/to/custom.json nginx
# Run without seccomp (dangerous! only for debugging)
docker run --security-opt seccomp=unconfined nginx
# Verify seccomp status inside a container
cat /proc/$$/status | grep Seccomp
# Seccomp: 2 (0=disabled, 1=strict, 2=filter)

// minimal-webserver.json — Minimal profile for a web server
{
"defaultAction": "SCMP_ACT_ERRNO",
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"]
}
],
"syscalls": [
{
"names": [
"accept4", // Accept connections
"bind", // Bind socket
"brk", // Expand heap
"clock_gettime", // Get time
"close", // Close fd
"connect", // Connect socket
"epoll_create1", // Create epoll
"epoll_ctl", // Control epoll
"epoll_pwait", // Wait for events
"exit", // Exit process
"exit_group", // Exit all threads
"fcntl", // File control
"fstat", // File stats
"futex", // Fast mutex
"getcwd", // Get working dir
"getdents64", // Read directory
"getpid", // Get PID
"getsockname", // Get socket name
"getsockopt", // Get socket options
"listen", // Listen on socket
"lseek", // Seek in file
"mmap", // Memory mapping
"mprotect", // Memory protection
"munmap", // Unmap memory
"nanosleep", // Sleep
"open", // Open file
"openat", // Open file (at)
"read", // Read
"recvfrom", // Receive
"recvmsg", // Receive message
"rt_sigaction", // Signal handling
"rt_sigprocmask",// Signal mask
"rt_sigreturn", // Signal return
"sendmsg", // Send message
"sendto", // Send to
"setsockopt", // Set socket options
"shutdown", // Shutdown socket
"socket", // Create socket
"stat", // File stat
"write", // Write
"writev" // Write vector
],
"action": "SCMP_ACT_ALLOW"
}
]
}

# Kubernetes pod with seccomp profile
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
spec:
securityContext:
seccompProfile:
type: RuntimeDefault # Use container runtime's default profile
# type: Localhost # Use a locally stored custom profile
# localhostProfile: profiles/myapp.json
# type: Unconfined # No seccomp (avoid!)
containers:
- name: myapp
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop: ["ALL"] # Drop ALL capabilities
add: [] # Add back only what's needed

18.5 Using strace to Build Seccomp Profiles

Section titled “18.5 Using strace to Build Seccomp Profiles”
Terminal window
# Find which syscalls your application actually uses
strace -f -e trace=all ./myapp 2>&1 | \
grep "^[a-z]" | cut -d'(' -f1 | sort -u
# More complete: trace for several minutes during normal operation
strace -f -e trace=all -o /tmp/syscalls.txt ./myapp &
APP_PID=$!
sleep 60 # Let it run normally
kill $APP_PID
cat /tmp/syscalls.txt | grep "^[a-z]" | cut -d'(' -f1 | sort -u > /tmp/needed_syscalls.txt
# Use oci-seccomp-bpf-hook for automatic profile generation
# Generates a seccomp profile from an actual container run:
podman run --annotation \
io.containers.trace-syscall=/tmp/output-profile.json \
nginx
# The generated profile can be applied to subsequent runs

Layered Security for a Production Container
─────────────────────────────────────────────
Kubernetes Pod Security:
┌─────────────────────────────────────────────────────────┐
│ SecurityContext: │
│ • runAsNonRoot: true (not root) │
│ • runAsUser: 1000 (specific UID) │
│ • readOnlyRootFilesystem: true (immutable FS) │
│ • allowPrivilegeEscalation: false │
│ • capabilities.drop: ALL (no Linux capabilities) │
│ • seccompProfile: RuntimeDefault │
│ │
│ AppArmor Profile: (via annotation) │
│ • Path-based access control │
│ • Can only read /app/*, /etc/ssl/* │
│ • Can write /tmp/*, /var/log/app/* │
│ │
│ Seccomp Profile: │
│ • Only ~30 necessary syscalls allowed │
│ • No mount, kexec, init_module, ptrace, etc. │
│ │
│ Network Policy: │
│ • Only port 8080 ingress │
│ • Only database port 5432 egress │
└─────────────────────────────────────────────────────────┘
Even with full application code execution:
→ Cannot escalate to root (no capabilities)
→ Cannot read sensitive files (AppArmor)
→ Cannot use dangerous syscalls (seccomp)
→ Cannot talk to unexpected services (NetworkPolicy)
→ Cannot modify filesystem (readOnlyRootFilesystem)

Q1: What is seccomp and how is it different from AppArmor/SELinux?

Answer: Seccomp (Secure Computing Mode) restricts which system calls a process can make using BPF filters. It operates at the syscall level — if a process tries to call mount() and that’s not in the allowed list, the kernel kills the process or returns an error. AppArmor and SELinux restrict what resources a process can access (files, capabilities, network) after syscalls are made. They’re complementary: seccomp limits the syscall attack surface (fewer ways to interact with the kernel), while AppArmor/SELinux limits resource access. Docker uses both: seccomp to block ~44 dangerous syscalls, and optionally AppArmor for file access control.

Q2: Why would you use defaultAction: SCMP_ACT_ERRNO instead of SCMP_ACT_KILL?

Answer: SCMP_ACT_KILL kills the process immediately if it makes a disallowed syscall. SCMP_ACT_ERRNO returns an error (like EPERM) to the calling process without killing it. SCMP_ACT_ERRNO is generally preferred because: (1) It allows graceful handling — the application might have error handling for failed syscalls. (2) Easier to debug — the application logs the error instead of dying silently. (3) Some applications probe for optional features by trying syscalls and handling failures. Use SCMP_ACT_KILL only for syscalls that are never acceptable and where you want to prevent even error-path execution.


LayerToolRestricts
SyscallsseccompWhich kernel calls can be made
Capabilitiesdrop: ALLWhich privileges process has
File accessAppArmorWhich paths can be accessed
Resource accessSELinuxType-based resource control
NetworkNetworkPolicyWhich endpoints can be reached

Chapter 14 (Security Fundamentals).


Advantages: Modular authentication (easy to add 2FA), Capabilities avoid giving full root to binaries. Disadvantages: Misconfiguring PAM can lock everyone out of the server permanently.


  • Using SUID root instead of assigning specific capabilities.
  • Misconfiguring PAM modules causing login lockouts.

SymptomCauseDiagnosisFix
Cannot ping as standard userMissing cap_net_rawgetcap /bin/pingsetcap cap_net_raw+ep /bin/ping
User locked out of systemPAM faillock triggeredfaillock --user <username>faillock —user —reset

Objective: Work with capabilities and PAM.

Terminal window
# 1. View capabilities of a binary
getcap /usr/bin/ping
# 2. Grant a capability to a binary
# sudo setcap cap_net_bind_service+ep /path/to/my_webserver
# 3. Check PAM configuration
cat /etc/pam.d/sshd

  1. Remove the SUID bit from a custom binary that needs to bind to port 80. Use setcap to grant it only cap_net_bind_service.
  2. Configure PAM to require a minimum password length of 12 characters using pam_pwquality.

  • Capabilities break down root privileges into distinct units.
  • PAM (Pluggable Authentication Modules) handles authentication dynamically.
  • Seccomp restricts which system calls a process can make.

  • man capabilities
  • man pam

  • Chapter 35 — Docker/K8s security using capabilities and seccomp

Last Updated: July 2026