BPF based syscall filtering between security and compatibility
Namespaces isolate what a process can see, seccomp limits what a process is allowed to do at all. Anyone thinking about container hardening purely in terms of capabilities and namespaces overlooks one of the most effective defenses against kernel exploits: deliberately blocking system calls an application simply never needs during normal operation.
Table of Contents
- 1. What seccomp is and why it was built
- 2. How BPF based filtering actually works
- 3. Filter actions: from ALLOW to KILL
- 4. The Docker default profile in detail
- 5. Building a custom seccomp profile
- 6. Applying a custom profile to a container
- 7. Weighing security against compatibility
- 8. Debugging blocked syscalls
- 9. Practical relevance for Magento containers
- 10. Summary
- 11. FAQ
1. What seccomp is and why it was built
seccomp, short for secure computing mode, is a kernel feature that restricts the set of system calls a process is allowed to make. Originally introduced in 2005 as an extremely restrictive mode that only permitted read, write, exit, and sigreturn, seccomp evolved into a considerably more flexible tool with the seccomp-bpf mode added in 2012, allowing complex, programmable filter rules instead of a rigid all or nothing model.
The underlying idea is simple: a web server process will never call reboot, a PHP FPM worker will never load a new kernel module, and a database process will never manipulate the system clock. Every one of these syscalls that an application never needs during normal operation, but that an attacker could potentially abuse after a successful exploit, can be preemptively blocked with seccomp, long before any application logic even comes into play.
2. How BPF based filtering actually works
seccomp-bpf reuses the same Berkeley Packet Filter technology originally developed for network packet filtering, now applied instead to syscall numbers and their arguments. On every system call, the kernel runs the loaded BPF program, which decides based on the syscall number and optionally individual arguments whether the call gets allowed, denied, logged, or simulated with an error code, all within a handful of kernel cycles with no noticeable performance impact.
A seccomp filter gets loaded as a small BPF bytecode program via the seccomp() system call or the older prctl(PR_SET_SECCOMP), and afterward is irreversibly attached to the process and all its child processes: once set, a filter can never be loosened, only further tightened with additional, more restrictive filters. This one way direction is a deliberate security design that prevents a compromised process from lifting its own restrictions after the fact.
# Check the current seccomp status of a process (0=disabled, 2=filter active)
grep Seccomp /proc/$$/status
# List the seccomp actions available on the running kernel
cat /proc/sys/kernel/seccomp/actions_avail
3. Filter actions: from ALLOW to KILL
A seccomp filter can return one of several actions for every syscall: SCMP_ACT_ALLOW lets the call through unchanged, SCMP_ACT_ERRNO makes the syscall fail and return a defined error code without terminating the process, SCMP_ACT_TRAP triggers a SIGSYS signal the process can handle itself, and SCMP_ACT_KILL terminates the calling thread, or the entire process, immediately.
For production environments, SCMP_ACT_ERRNO is usually the more pragmatic choice over SCMP_ACT_KILL: a process that gets a blocked syscall back with a clean error code like EPERM can often catch that error and keep running, while a hard kill terminates the entire application instantly and, in the worst case, causes unexpected downtime just because a rarely used but legitimate syscall was overlooked.
4. The Docker default profile in detail
Docker has enabled a predefined seccomp profile by default for every newly started container since version 1.10, explicitly blocking about 44 of the more than 300 available Linux syscalls, while leaving the vast majority allowed. Blocked among others are reboot, swapon, mount outside specific permitted exceptions, kexec_load, as well as syscalls for manipulating kernel modules and controlling the system clock, all operations a normal application container would never legitimately need.
It matters to distinguish this from capabilities: a syscall blocked in the seccomp profile stays blocked even if the process theoretically holds the matching capability, since seccomp acts as an additional, independent protection layer at the syscall level that applies before and independently of the capability check. Docker disables the default profile entirely via --security-opt seccomp=unconfined, which is practically never appropriate in production and should be reserved exclusively for very specific debugging.
# Inspect the active seccomp profile of a running container
docker inspect shop-php --format '{{.HostConfig.SecurityOpt}}'
# Deliberately disable the default profile (isolated debugging only!)
docker run --rm -it --security-opt seccomp=unconfined alpine sh
5. Building a custom seccomp profile
A custom seccomp profile usually starts as a restrictive base defining a whitelist of allowed syscalls, rather than starting from the more permissive default profile and blocking a few additional syscalls on top. For Docker, a profile is a JSON file with a defaultAction that applies to every syscall not explicitly listed, plus a list of syscalls entries defining actions that deviate from that default.
The biggest practical challenge in building a custom, restrictive profile is fully identifying every syscall an application actually needs, since even simple PHP or Node.js processes surprisingly rely on many, sometimes rarely used, syscalls for edge cases like signal handling or specific memory management operations. In practice, an iterative approach works best: start with a very restrictive profile, systematically identify blocked syscalls through audit logs, and gradually extend the profile with the syscalls actually needed.
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": [
"read", "write", "open", "openat", "close", "stat", "fstat",
"mmap", "munmap", "brk", "rt_sigaction", "rt_sigprocmask",
"access", "socket", "connect", "accept4", "epoll_wait",
"exit", "exit_group", "futex", "clone", "execve"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
6. Applying a custom profile to a container
A finished profile gets attached in Docker via --security-opt seccomp=/path/to/profile.json at container start, and it replaces the default profile entirely rather than extending it. Podman uses the same syntax with an identical JSON structure, which makes switching between the two runtimes for this specific feature seamless.
For Kubernetes, a dedicated SeccompProfile section has existed in the security context definition since version 1.19, which can reference either the runtime default profile, a profile stored locally on the node, or, since 1.27, alternatively an inline defined localhost path. This declarative approach lets seccomp profiles get versioned as part of the deployment manifest instead of being maintained manually on every node.
# Attach a custom profile at container start
docker run -d --name shop-php \
--security-opt seccomp=/etc/docker/seccomp/php-restrictive.json \
magento-php-fpm:8.4
7. Weighing security against compatibility
An overly restrictive profile leads to application crashes or mysterious errors that look unrelated to security at first glance, for example a PHP process that inexplicably hangs on certain filesystem operations because a rarely called but necessary syscall got blocked. An overly permissive profile, on the other hand, loses exactly the protection seccomp is supposed to provide and, in the extreme case, degenerates into a pure formality with no real security benefit.
The pragmatic rule of thumb for production environments is to start with the Docker default profile, which already represents a sensible compromise between broad compatibility and blocking obviously dangerous syscalls, and to only build a custom, more restrictive profile for highly sensitive workloads where the extra maintenance burden is justified by the actual risk profile, for example containers with direct access to sensitive customer data or payment information.
8. Debugging blocked syscalls
A syscall blocked by seccomp manifests differently depending on the configured action: with SCMP_ACT_ERRNO, the application usually reports a seemingly harmless Permission denied or Function not implemented error, with no immediate indication of the actual trigger. The most reliable way to pin down the blocked syscall concretely is strace, which logs every system call along with its return value and shows exactly where the process runs into a seccomp block.
For a more systematic analysis, the audit approach using SCMP_ACT_LOG instead of SCMP_ACT_ERRNO as the default action works well: blocked syscalls then get logged instead of actually denied, so every syscall genuinely needed can be collected over a representative test run from the kernel audit log or via journalctl -k, before the profile gets switched to enforcing mode in production.
# Capture a process's syscalls live to find which ones get blocked
strace -f -e trace=all -o /tmp/trace.log php-fpm8.4 -F
# Filter kernel audit messages related to seccomp
journalctl -k | grep -i seccomp
9. Practical relevance for Magento containers
For a typical Magento PHP FPM container, the Docker default profile is generally sufficient and already covers every syscall needed for running PHP, Composer, and the usual filesystem and network operations, with no custom profile required. A custom, more restrictive profile pays off mainly for specialized helper containers with a tightly scoped task, for example a dedicated cron container that only ever runs scheduled Magento indexer jobs and needs no network or file operations beyond that.
Across all hardening measures, one rule holds: seccomp does not replace any of the other protection layers such as namespaces, capabilities, or read only root filesystems, but adds an additional, orthogonal line of defense. Only the interplay of several independent mechanisms actually makes a container escape significantly harder for an attacker, since any single layer taken alone could in principle be bypassed, while all of them together form a considerably more robust defense.
| Filter action | Behavior on blocked syscall | Process keeps running | Typical use |
|---|---|---|---|
| SCMP_ACT_ALLOW | Syscall executes normally | yes | whitelist of allowed syscalls |
| SCMP_ACT_ERRNO | Syscall fails with a defined error code | yes, if the application handles the error | production environments, soft blocking |
| SCMP_ACT_TRAP | SIGSYS signal raised on the process | depends on the custom signal handler | specialized error handling |
| SCMP_ACT_KILL | Process or thread terminates immediately | no | maximum hardening, high risk of false positives |
| SCMP_ACT_LOG | Syscall executes and gets logged | yes | audit mode for profile development |
Mironsoft
Server administration, Docker hosts, and performance tuning
Linux servers nobody on the team really understands anymore?
We handle setup, hardening, and performance tuning of Linux servers and Docker hosts for Magento deployments, documented and traceable instead of grown and unclear.
Server Audit
Review the existing server configuration for security gaps and performance bottlenecks.
Docker Host Setup
Set up and secure production-ready Docker environments for Magento cleanly.
Monitoring & Tuning
Measure resource usage and tune systemd, kernel, and services with purpose.
10. Summary
seccomp
Core idea
BPF based filtering of allowed and blocked syscalls per process
Docker default
About 44 of over 300 syscalls blocked, broad compatibility
Custom profile
Whitelist approach, developed iteratively via audit logs
Debugging tool
strace for acute cases, SCMP_ACT_LOG for systematic auditing