Seccomp-BPF Syscall Filtering
Apply seccomp-BPF syscall filtering with libseccomp and systemd SystemCallFilter to harden Linux services using allowlists, denylists, and argument-level rules.
Before you start
- ▸Root or sudo access on the target system
- ▸Basic familiarity with C compilation and systemd unit files
- ▸Kernel 3.17 or newer (seccomp(2) syscall required; all current LTS distros qualify)
- ▸auditd or journald access to observe denied syscall events during development
Seccomp (Secure Computing Mode) in BPF mode lets you attach a Berkeley Packet Filter program to a process that intercepts every system call and decides whether to allow, deny, or kill the caller. It is one of the most effective ways to shrink the attack surface of a service: even if an attacker achieves arbitrary code execution inside a process, they cannot make syscalls the filter disallows. Container runtimes, browsers, and modern init systems all rely on it. This guide covers the two filter strategies—allowlists and denylists—then shows how to write filters with libseccomp, and how to apply them declaratively with systemd's SystemCallFilter.
Allowlists vs Denylists
The choice of strategy determines your security posture and maintenance burden.
- Denylist – start with all syscalls permitted, block known-dangerous ones (e.g.,
ptrace,kexec_load). Easy to deploy, but you must keep the list current as new dangerous syscalls appear in the kernel. - Allowlist – start with a default action of
SCMP_ACT_KILL_PROCESSorSCMP_ACT_ERRNO, then explicitly permit the small set the process actually needs. Harder to build correctly but far more robust; an unknown syscall is denied by default.
For production hardening, prefer allowlists wherever you can profile the target process. Denylists are acceptable as a quick-win layer added to an existing service when you cannot yet profile it fully.
How Seccomp-BPF Works
When a process calls prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) or seccomp(SECCOMP_SET_MODE_FILTER, ...), the kernel loads a classic BPF program. On each subsequent syscall the BPF program runs in kernel space and returns one of several actions:
SECCOMP_RET_ALLOW– let it through.SECCOMP_RET_ERRNO– return a synthetic error to userspace (process survives).SECCOMP_RET_KILL_PROCESS– deliver SIGSYS and terminate the entire process group immediately.SECCOMP_RET_TRAP– send SIGSYS so a signal handler (or tracer) can decide.SECCOMP_RET_LOG– allow but log to the audit subsystem.
Filters are inherited by children and cannot be relaxed once set (without CAP_SYS_ADMIN and the no_new_privs bit, and even then only to add more restrictive filters).
Installing libseccomp
libseccomp provides a C API (and bindings for Python, Go, Rust) that compiles human-readable rules down to BPF bytecode so you never have to write raw BPF.
Debian / Ubuntu
sudo apt install libseccomp-dev libseccomp2 seccomp
Fedora / RHEL 9 / Rocky 9
sudo dnf install libseccomp-devel libseccomp
Arch Linux
sudo pacman -S libseccomp
Writing a Filter with libseccomp in C
The example below builds a strict allowlist for a hypothetical network daemon: it permits only the syscalls needed for I/O and process lifecycle, then kills on anything else.
cat > seccomp_demo.c <<'EOF'
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int apply_filter(void) {
/* Default action: kill the process on any unlisted syscall */
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL_PROCESS);
if (!ctx) return -1;
/* Explicitly allow the syscalls this daemon needs */
int rc = 0;
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(accept4),0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(recvmsg),0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(sendmsg),0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(epoll_wait), 0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(munmap), 0);
rc |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(futex), 0);
if (rc != 0) { seccomp_release(ctx); return -1; }
rc = seccomp_load(ctx); /* applies the filter to this process */
seccomp_release(ctx);
return rc;
}
int main(void) {
if (apply_filter() != 0) {
fprintf(stderr, "seccomp filter failed\n");
return 1;
}
/* Daemon work happens here */
write(STDOUT_FILENO, "filter active\n", 14);
/* execve() is not in the allowlist — would trigger SIGSYS */
return 0;
}
EOF
gcc -O2 -o seccomp_demo seccomp_demo.c -lseccomp
./seccomp_demo
Output will be filter active. If you add a call to execve() after apply_filter() the process is killed immediately with SIGSYS.
Argument-Level Filtering
libseccomp can match on individual syscall arguments, not just the syscall number. For example, allow open only with O_RDONLY:
# In C: restrict openat to read-only flags (flag argument is arg index 2)
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 1,
SCMP_A2(SCMP_CMP_MASKED_EQ, O_WRONLY | O_RDWR, 0));
This level of granularity lets you permit a syscall family while blocking the dangerous flag combinations within it.
Profiling Which Syscalls a Process Uses
Before writing an allowlist you need to know what syscalls the process actually makes. Two practical methods:
- strace:
strace -f -e trace=all -o strace.log ./myservice, then extract unique syscall names withgrep -oP '^[a-z0-9_]+' strace.log | sort -u. - systemd audit integration: Add
SystemCallFilter=~with an empty deny set plusSystemCallLog=yesin a unit drop-in to log all calls without blocking.
Run the process through every realistic code path—startup, normal operation, shutdown, and error handling—before finalising the list.
systemd SystemCallFilter
For services managed by systemd you rarely need to write C. The SystemCallFilter= directive compiles a seccomp filter at service start time.
Using Predefined Groups (Allowlist Mode)
systemd ships named syscall groups (prefixed with @). Specify what is allowed by listing groups; anything not listed gets ENOSYS by default.
# /etc/systemd/system/myservice.service.d/seccomp.conf
[Service]
SystemCallFilter=@system-call-filter
SystemCallFilter=@basic-io @io-event @ipc @network-io @process @signal
SystemCallErrorNumber=EPERM
View all available groups and their members:
systemd-analyze syscall-filter
Denylist Mode with the ~ Prefix
Prefix a group or name with ~ to block it while allowing everything else:
[Service]
# Block dangerous syscall classes, allow the rest
SystemCallFilter=~@debug @mount @reboot @swap @privileged @resources @clock
Real-World Example: Hardening nginx
sudo mkdir -p /etc/systemd/system/nginx.service.d
sudo tee /etc/systemd/system/nginx.service.d/seccomp.conf >/dev/null <<'EOF'
[Service]
SystemCallFilter=@system-call-filter
SystemCallFilter=@basic-io @io-event @ipc @network-io @process @signal @file-system
SystemCallFilter=~@debug @reboot @swap @module
SystemCallErrorNumber=EPERM
SystemCallArchitectures=native
EOF
sudo systemctl daemon-reload
sudo systemctl restart nginx
SystemCallArchitectures=native prevents the process from switching to a 32-bit personality to bypass the filter—always include it.
Verifying the Filter Is Active
# Check the process's seccomp status (replace PID)
grep Seccomp /proc/$(pgrep -n nginx)/status
A value of 2 means SECCOMP_MODE_FILTER is active. Value 0 means no filter is loaded.
# Inspect the BPF filter attached to the process
sudo bpftool prog list
# Or for a specific PID:
cat /proc/$(pgrep -n nginx)/fdinfo/* 2>/dev/null | grep seccomp
# For systemd units, confirm the filter appears in the unit's effective settings
systemctl show nginx | grep -i syscall
Troubleshooting
- Service crashes immediately at start (SIGSYS): The filter is too strict. Switch the default action to
SCMP_ACT_LOG(libseccomp) or useSystemCallLog=yesand audit logs to identify the blocked call. On systemd, temporarily setSystemCallErrorNumber=EPERMinstead of kill so the service can log an errno. - Filter not taking effect: Confirm
no_new_privsis set. systemd sets it automatically viaNoNewPrivileges=yes; without it a setuid binary can drop the filter. Check withgrep NoNewPrivs /proc/PID/status. - 32-bit compatibility issues: If you omit
SystemCallArchitectures=native, a process could callpersonality()to switch ABI and evade filters keyed to 64-bit numbers. Always set it. - glibc wrapper surprises: Some libc functions use different underlying syscalls across versions or architectures (e.g.,
pthread_createusescloneorclone3). Profile on the exact runtime you deploy to. - Audit logging: Enable
SCMP_ACT_LOGas the default during development and watchauditdorjournalctl -kfor denied calls before switching toSCMP_ACT_KILL_PROCESS.
Frequently asked questions
- Can a process remove or relax its own seccomp filter?
- No. Once a filter is loaded it can only be made more restrictive, never relaxed. Additional filters added later are ANDed with the existing ones—the most restrictive rule always wins.
- Does seccomp filtering affect performance?
- The overhead is measurable but small—typically under 1% on syscall-heavy workloads on modern kernels with the constant-action optimization. It is negligible for most services and worth the security gain.
- What is the difference between SystemCallFilter allow mode and deny mode in systemd?
- When SystemCallFilter lists names or groups without a ~ prefix, systemd builds an allowlist and blocks everything else with ENOSYS. Prefixing entries with ~ builds a denylist against an otherwise-open default.
- How do I handle multiarch or 32-bit binaries running under a 64-bit kernel?
- Set SystemCallArchitectures=native in systemd units, or call seccomp_arch_remove() to strip non-native architectures from the libseccomp context. This prevents syscall number remapping attacks across ABIs.
- Can seccomp filtering be bypassed via /proc or other kernel interfaces?
- Seccomp only intercepts system calls, not /proc reads that do not require a gated syscall path. Combine it with other mechanisms—namespaces, capabilities dropping, and mandatory access control—for defense in depth.
Related guides
Manage Secrets with Ansible Vault
Encrypt Ansible secrets with AES-256 using ansible-vault: encrypt files and inline vars, automate with password files, and isolate group-level secrets with vault IDs.
AppArmor Explained
Learn how AppArmor profiles work, how to switch between enforce and complain mode, create new profiles, and diagnose access denials on Ubuntu, Debian, and Arch.
Apply CIS Benchmarks with OpenSCAP
Use OpenSCAP and scap-security-guide to evaluate, report on, and remediate Linux systems against CIS Benchmarks — covering install, eval, and automation.
How to Audit a Linux System with auditd
Set up auditd on Linux to track file access, syscalls, and privilege use. Covers persistent rules, file watches, ausearch, and aureport across major distros.