Context & Why
Modern Linux 6.x kernels expose over 450 system calls. A typical Go microservice serving JSON over HTTP needs roughly 40 of them. It never needs to call ptrace, reboot, mount, or load kernel modules via init_module. Even so, standard unprivileged processes can still dispatch almost any syscall by default.
Kernel bugs remain among the most reliable paths from an application exploit to root. When attackers achieve remote code execution in a web worker, they immediately probe the kernel interface to escalate privileges. If that compromised process cannot open raw sockets, spawn subshells, or trace peers, an attacker hits a dead end.
Secure Computing Mode (Seccomp) solves this problem at the kernel boundary. First merged in Linux 2.6.12 and upgraded with BPF filtering in version 3.5, Seccomp inspects syscall numbers and arguments before execution. If a thread invokes a forbidden call, the kernel immediately intervenes. It can terminate the process, return an EPERM errno, or write an audit record to syslog.
Seccomp differs fundamentally from Mandatory Access Control systems like SELinux or AppArmor. Those tools evaluate path names, network endpoints, and security labels. Seccomp operates one layer deeper. It strips away the vocabulary a binary uses to speak to the kernel.
Installation
Modern enterprise distributions ship with Seccomp enabled in the kernel. You do not need third-party modules or custom drivers. You will, however, need user-space development headers, runtime libraries, and tracing tools to build and test profiles.
On Debian 12 and Ubuntu 22.04 or 24.04:
sudo apt-get update
sudo apt-get install -y libseccomp-dev seccomp strace auditd
On RHEL 9, Rocky Linux, or Fedora:
sudo dnf install -y libseccomp-devel strace audit
Verify that your active kernel has Seccomp support compiled in:
grep -i seccomp /boot/config-$(uname -r)
Ensure both CONFIG_SECCOMP=y and CONFIG_SECCOMP_FILTER=y appear in the output. Standard cloud images on AWS, GCP, and Azure enable both out of the box.
Configuration
You can deploy Seccomp at three distinct layers: compiled into C or Go binaries, enforced via systemd unit files, or applied as container profiles in Docker and containerd. Pick the layer that fits your operational model.
1. Native Application Filtering with libseccomp
Embedding filters directly into C or C++ services delivers portable security. The restrictions follow the binary anywhere it runs, even without container runtimes or systemd wrappers. The following program drops into a strict allowlist right after startup, allowing only standard I/O and process exit:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <seccomp.h>
#include <sys/prctl.h>
int main(void) {
/* Default to killing the entire process on any unlisted call */
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL_PROCESS);
if (!ctx) {
perror("seccomp_init");
exit(EXIT_FAILURE);
}
/* Explicitly permit necessary syscalls */
seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(exit), 0);
seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(exit_group), 0);
seccomp_rule_add(ctx, SCMP_ACT_PERMIT, SCMP_SYS(fstat), 0);
/* Load the compiled BPF program into the kernel */
if (seccomp_load(ctx) < 0) {
perror("seccomp_load");
seccomp_release(ctx);
exit(EXIT_FAILURE);
}
seccomp_release(ctx);
printf("Filter loaded. Standard write operations work.\n");
/* Attempt a blocked call: getpid() */
printf("Invoking getpid()...\n");
pid_t pid = getpid();
printf("PID: %d (Unreachable)\n", pid);
return 0;
}
Compile and run the test binary:
gcc -O2 -o seccomp_demo seccomp_demo.c -lseccomp
./seccomp_demo
The program writes its initial status line to standard output. Once it calls getpid(), the kernel instantly terminates the thread with Bad system call (core dumped). The process never reaches the final print statement.
2. Hardening Daemons with systemd
Legacy daemons and closed-source tools rarely expose internal hook points. For these workloads, systemd provides declarative filtering via the SystemCallFilter directive with near-zero performance overhead (under 1% in most benchmarks).
Save this unit to /etc/systemd/system/api-gateway.service:
[Unit]
Description=Internal API Gateway
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/local/bin/api-gateway --port 8080
Restart=always
# Enforce native architecture and syscall allowlists
SystemCallArchitectures=native
SystemCallFilter=@system-service @network-io
SystemCallFilter=~@privileged @resources @obsolete @mount @reboot
SystemCallErrorNumber=EPERM
[Install]
WantedBy=multi-user.target
Systemd bundles common syscalls into pre-defined sets like @system-service and @network-io. The tilde prefix (~) subtracts dangerous groups from that baseline. If the application attempts a blocked action, systemd returns EPERM rather than killing the worker ungracefully.
Apply the unit changes:
sudo systemctl daemon-reload
sudo systemctl restart api-gateway.service
3. Restricting Containerized Services with Docker Profiles
Docker blocks roughly 44 syscalls by default, including reboot, clock_settime, and kexec_load. That default balances broad compatibility with baseline security. For critical microservices, replace that broad net with a tailored allowlist.
Save the following policy as strict-profile.json:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64"
],
"syscalls": [
{
"names": [
"read",
"write",
"open",
"openat",
"close",
"fstat",
"futex",
"epoll_wait",
"epoll_ctl",
"exit_group"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
Start a container with the custom filter attached:
docker run --rm -it \
--security-opt seccomp=strict-profile.json \
alpine:latest sh
Commands that rely on unlisted system calls will now fail with permission errors directly inside the container shell.
Verification & Monitoring
Never push strict allowlists straight into production. If you guess wrong on required syscalls, your service will crash the moment it hits an uncommon edge case. Use this profiling and monitoring workflow to prevent outages.
Tracing Required Syscalls
Map the binary’s actual runtime activity before locking anything down. Run the service under strace during a synthetic benchmark run:
strace -c -f -S name /usr/local/bin/api-gateway --port 8080
Drive at least 5,000 to 10,000 real HTTP requests against the process. Exercise database connections, health endpoints, metrics scrapers, and error-handling routines. When you terminate strace with Ctrl+C, it outputs an execution table showing total calls, errors, and elapsed time per syscall. Build your allowlist directly from that report.
Checking Process Seccomp State
Confirm that your filter is actively protecting the target PID via the proc filesystem:
grep -i seccomp /proc/$(pgrep -f api-gateway)/status
The kernel reports one of three integer states for Seccomp:
- 0 (Disabled): No syscall filtering is active on this process.
- 1 (Strict): Legacy mode enabled via
prctl(PR_SET_SECCOMP, 1). The process may only useread,write,_exit, andsigreturn. - 2 (Filter): Active BPF filters are evaluating syscalls via
seccomp(SECCOMP_SET_MODE_FILTER, ...).
Monitoring Filter Violations in Audit Logs
During testing, set rule actions to SCMP_ACT_LOG. The kernel lets the call proceed while logging every violation, giving you clean telemetry without risking uptime.
Search recent audit records:
sudo ausearch -m SECCOMP -ts recent
Triggered filters generate log entries in /var/log/audit/audit.log:
type=SECCOMP msg=audit(1708701234.567:890): auid=1000 uid=33 gid=33 ses=1 \
subj=unconfined pid=1423 comm="api-gateway" exe="/usr/local/bin/api-gateway" \
syscall=101 compat=0 ip=0x7f9a1234 arch=c000003e code=0x0
Translate the raw decimal syscall index with ausyscall:
ausyscall x86_64 101
Syscall 101 translates to ptrace on x86_64. If an external library legitimately requires that operation, add it to your profile. If an unvetted dependency attempted process injection, your Seccomp filter successfully blocked the exploit at zero runtime cost.

