Exploring Docker's built-in seccomp profile (docker-default), the list of blocked syscalls, and the behavior of containers that violate it. Including the JSON profile format for OCI runtimes and creating custom allow-list profiles via --security-opt.

In episode 7 you mastered filter debugging: disassembling BPF, observing syscalls, and testing deny behavior. Now it's time to use all of it in the place you'll encounter most in the real world: containers. A container isn't a virtual machine — it shares the same kernel as the host. And when that kernel is shared, a dangerous syscall from one container can shake everything.
Episode 8 dissects how Docker and OCI runtimes (runc/crun) apply seccomp: the built-in docker-default profile, the list of blocked syscalls, the profile JSON format, and how to build custom profiles for your applications.
A container is a set of processes on the host kernel, isolated by namespaces, cgroups, and — when configured — seccomp. An analogy: apartments in one building. Every resident has their own room (namespaces) and their own electricity allocation (cgroups), but they all share one foundation and one electrical system (the kernel). If one resident can touch the main switch, everyone is affected.
Syscalls like reboot, kexec_load, or mount are that "main switch". Namespaces don't stop them — they still reach the same kernel. It's seccomp that decides those syscalls cannot be invoked at all.
Since Docker 1.10, every container you run automatically receives a seccomp profile named docker-default — with no extra configuration. This is one of the most important moments in container security history: seccomp became a default defense, not a manually enabled feature.
The profile works as a deny-list: its defaultAction is SCMP_ACT_ERRNO(EPERM), meaning all syscalls are blocked except those explicitly allowed. Wait — actually, let's be precise: docker-default is built as a deny-list over the ~300 commonly needed syscalls, with defaultAction: SCMP_ACT_ERRNO(EPERM) so any syscall outside the allow set is rejected. The list of blocked syscalls is around forty, with the most frequently cited like this:
acct, add_key, bpf, clock_adjtime, clock_settime, create_module,
delete_module, finit_module, get_kernel_syms, init_module, io_cancel,
ioperm, iopl, kexec_file_load, kexec_load, keyctl, lookup_dcookie,
mbind, mount, move_pages, name_to_handle_at, nfsservctl,
open_by_handle_at, perf_event_open, personality, pivot_root,
process_vm_readv, process_vm_writev, ptrace, quotactl, reboot,
request_key, sethostname, setns, settimeofday, shmctl, sysfs,
swapon, swapoff, umount, umount2, unshare, uselib, userfaultfdLook at the pattern, don't just memorize the list:
mount, umount, reboot, sethostname, swapon, kexec_load, init_module — touching the host directly.unshare, setns, pivot_root — building or jumping into new namespaces, a classic escape path.ptrace, perf_event_open, process_vm_readv, process_vm_writev, keyctl — reading memory or debugging neighboring processes.ioperm, iopl, vm86, sysfs, _sysctl — obsolete syscalls modern applications don't need.Two things you should know about clone: docker-default does not block clone entirely, but it blocks invocations with certain namespace-creation flags (e.g. CLONE_NEWUSER). This is where the argument-filter power from episode 5 is used — blocking a syscall with specific arguments, not the syscall itself.
When an application calls a blocked syscall, it receives EPERM — the application sees "Operation not permitted" without knowing who refused. This is your first chance to use the episode 7 skills: confirm with ausearch -m SECCOMP that what refused it is seccomp, not an ordinary permission issue.
This profile is active automatically. To make sure your Docker daemon supports it, check the SecurityOptions list:
docker info --format '{{.SecurityOptions}}'Make sure name=seccomp is in the list.
Note
The profile can be turned off with --security-opt seccomp=unconfined, but that's the same as taking off your seatbelt. Keep the default seccomp; make unconfined only a temporary debugging tool.
A container seccomp profile isn't a C binary — it's JSON following the Open Container Initiative (OCI) runtime spec. runc, crun, containerd, and CRI-O all read the same format — you learn it once, it applies everywhere. The basic structure: defaultAction, defaultErrnoRet, archMap, and a list of syscalls:
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"]
}
],
"syscalls": [
{
"names": [
"read", "write", "readv", "writev", "openat", "close",
"fstat", "lseek", "mmap", "mprotect", "munmap", "brk",
"exit", "exit_group", "rt_sigaction", "clone", "execve",
"getpid", "access", "fcntl", "futex", "clock_gettime",
"nanosleep", "socket", "connect", "sendto", "recvfrom",
"shutdown", "setsockopt", "getsockopt"
],
"action": "SCMP_ACT_ALLOW"
}
]
}Reading its parts:
defaultAction — what happens to syscalls that are not listed. SCMP_ACT_ERRNO means rejected with an errno.defaultErrnoRet — the default errno (1 = EPERM). This is where the "deny" behavior is set globally.archMap — the valid architectures along with their compatible sub-architectures. Needed so the profile doesn't go wrong when a 32-bit binary runs on a 64-bit kernel.syscalls — the list of rules; each entry has names, action, and optionally args for argument-based rules (the JSON form of episode 5's argument filters).Note this is an allow-list profile: you write the syscalls that are allowed, everything else is denied. This approach is far stricter than the docker-default deny-list, and it's what you want for an application whose behavior you understand.
Store the profile as a JSON file, then point Docker to it with --security-opt:
docker run --rm --security-opt seccomp=profile-allowlist.json nginx:1.27The same approach works in Compose files (via security_opt) and in other runtimes: containerd and CRI-O ultimately pass everything down to runc's config.json, and the seccomp object in that config is the exact same JSON. You learn one format, and that format follows you everywhere.
Warning
Be careful with --security-opt seccomp=unconfined. It's very useful for quick debugging — "was it seccomp blocking or not" — but it must never be a permanent production condition. A container without seccomp means every host syscall is open to it.
Building a profile from scratch sounds daunting, but the flow is something you already mastered in episode 7:
strace -f -c.names in the JSON.brk, mmap, mprotect, rt_sigaction, openat often appear in the first seconds; an application that fails to boot usually lost these.defaultAction to SCMP_ACT_LOG, run in staging, and read ausearch -m SECCOMP for syscalls not yet on the list.SCMP_ACT_ERRNO.Start with a small profile and expand gradually. A profile that's too thin crashes the application in the first second; a profile that's too fat is a deny-list disguised as an allow-list. The log-first-enforce-later iteration is the right balance.
In episode 8 you understood that containers share the host kernel, so dangerous syscalls must be pruned — and Docker already does this by default with the docker-default profile that blocks dozens of syscalls. You can read the OCI profile format (JSON with defaultAction, archMap, and syscalls), apply it via --security-opt seccomp=profile.json, and build an allow-list from an strace baseline.
The keys to take home:
docker-default is a baseline deny-list; your application deserves an allow-list.strace, test with LOG, then lock with ERRNO.JSON profiles are great, but they're not the only path. Many daemons run directly on the host without containers — and they need seccomp too. In episode 9, we move into Seccomp in systemd & Service Units: SystemCallFilter, SystemCallArchitectures, and RestrictAddressFamilies to lock down a service without changing a single line of application code.