Context: Why Does Seccomp Matter?
Containers are not virtual machines. Under the hood, they are simply processes sharing the host Linux kernel, isolated via cgroups and namespaces. If a web application suffers from a Remote Code Execution (RCE) vulnerability, an attacker can interact directly with the host kernel through system calls (syscalls).
The Linux x86_64 kernel currently supports over 450 syscalls. A typical Node.js or Go API service only needs about 40 to 70 syscalls to serve requests. Yet by default, Docker only blocks roughly 44 of the most sensitive syscalls (such as reboot, kexec_load). Over 300 remaining syscalls remain wide open.
If a zero-day kernel vulnerability emerges in a rarely used syscall like bpf() or keyctl(), an attacker can escalate privileges and achieve a container breakout. At that point, your entire host server is compromised.
As with broader Docker security practices, the most thorough solution is to switch from a blacklist to a whitelist approach (Default Deny). Allow only the syscalls your application actually needs, and block everything else.
Environment Setup
First, verify whether your kernel and Docker daemon support Seccomp.
Check the kernel configuration flag:
grep CONFIG_SECCOMP= /boot/config-$(uname -r)
# An output of CONFIG_SECCOMP=y indicates Seccomp is supported
Confirm that the Docker runtime recognizes Seccomp:
docker info | grep -i seccomp
# Expected output:
# Security Options:
# seccomp
# Profile: builtin
Install strace and auditd on your staging machine to extract and debug syscalls:
sudo apt-get update && sudo apt-get install -y strace jq auditd
Step-by-Step Implementation
A Docker Seccomp profile uses the standard OCI JSON format. The three most critical fields you need to pay attention to are: defaultAction, architectures, and the syscalls array.
Step 1: Capture the Application’s Actual Syscalls
Don’t guess what your application needs to call. Let strace accurately profile the app’s activity in a test environment (requiring elevated Linux capabilities like SYS_PTRACE):
# Run test container and collect syscall statistics
docker run --rm --cap-add SYS_PTRACE -v $(pwd):/app -w /app python:3.11-slim \
strace -c -f python -c "print('Hello from isolated container')"
The output provides a summary table of invocations along with execution counts (such as read, write, mmap, execve, brk). To be thorough, run your automated test suite or execute load tests during tracing. This step ensures you capture all syscalls triggered under heavy workloads.
Step 2: Create a Whitelist Seccomp Profile
Create a file named custom-seccomp.json. The key setting is defaultAction: set it to SCMP_ACT_ERRNO to deny any syscall not explicitly listed:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept4",
"arch_prctl",
"brk",
"clock_gettime",
"clone",
"close",
"connect",
"epoll_create1",
"epoll_ctl",
"epoll_pwait",
"execve",
"exit",
"exit_group",
"fcntl",
"fstat",
"futex",
"getcwd",
"getdents64",
"getpid",
"getrandom",
"madvise",
"mmap",
"mprotect",
"munmap",
"nanosleep",
"newfstatat",
"openat",
"poll",
"read",
"readlinkat",
"restart_syscall",
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"sched_yield",
"set_robust_list",
"set_tid_address",
"sigaltstack",
"statx",
"write"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
Step 3: Attach the Profile When Running Containers
With the standard CLI, use the --security-opt flag:
docker run --rm -it \
--security-opt seccomp=/path/to/custom-seccomp.json \
alpine:latest sh
With Docker Compose in a production stack, declare it inside the service definition:
services:
api:
image: my-secure-api:1.0
security_opt:
- seccomp:/etc/docker/seccomp/custom-seccomp.json
ports:
- "8080:8080"
Testing and Monitoring Violations
After enforcing your policy, you need to verify whether the protection mechanism actually works as expected.
1. Actively Test with Unauthorized Actions
Try invoking a syscall not included in your whitelist, such as attempting to change the hostname (which calls sethostname):
docker run --rm \
--security-opt seccomp=/path/to/custom-seccomp.json \
alpine:latest hostname hacker-box
# Expected output:
# hostname: sethostname: Operation not permitted
The Operation not permitted error is triggered immediately. The kernel terminates the attempt before the operation can modify system state.
2. Inspect Violation Logs with Auditd
If a container exits unexpectedly, the application likely attempted to invoke an unauthorized syscall. You can quickly inspect the audit log:
sudo ausearch -m SECCOMP -ts recent
Or filter directly from the audit log file:
sudo grep "type=SECCOMP" /var/log/audit/audit.log | tail -n 5
In the logs, look for the syscall=170 field. To identify which function syscall 170 corresponds to, run:
ausyscall x86_64 170
# Output: sethostname
A handy tip for staging: instead of using SCMP_ACT_ERRNO, which immediately crashes the app upon encountering a denied call, switch to "defaultAction": "SCMP_ACT_LOG" (supported since Linux 4.14). In this mode, the app continues running smoothly while any unauthorized syscalls are recorded in the audit log. Simply monitor the logs for 2 to 3 days, whitelist any missing legitimate syscalls, and then enforce SCMP_ACT_ERRNO when deploying to production.
