Processes & Privileges

Seccomp

Filtering the syscalls a process may make.

The system call interface is the entire attack surface between userspace and the kernel. Every privilege escalation that starts from an unprivileged process goes through a syscall, and most kernel vulnerabilities are reachable only via specific, often obscure, calls. A typical web server needs perhaps 60 of the roughly 400 syscalls Linux offers; the other 340 are pure attack surface it will never use. Seccomp (secure computing mode) lets a process voluntarily lock itself down to a syscall allowlist, so even after a full code-execution compromise the attacker cannot reach keyctl, ptrace, bpf, or whatever else the exploit chain needs. It is one of the highest-leverage hardening controls available.

Why the syscall surface matters

Reducing syscall surface is defence in depth aimed squarely at the kernel. Consider a memory-corruption bug in your service: the attacker gains arbitrary code execution in your process. With a seccomp filter that permits only the ~60 calls the service uses, their exploit primitives shrink dramatically. They cannot mount, cannot load a BPF program, cannot use unshare to create a privileged namespace, cannot ptrace a sibling. Many published container escapes rely on a single unusual syscall that a good seccomp profile would have blocked outright.

Seccomp modes

Seccomp has two modes, and only the second is used in practice today.

ModeNameBehaviour
Mode 1StrictOnly read, write, _exit, sigreturn allowed; anything else kills the process. Too restrictive for real programs.
Mode 2Filter (seccomp-bpf)A BPF program you supply inspects each syscall and its arguments and returns an action.

Mode 2 is the flexible one. You attach a small classic-BPF program that, for each syscall, decides an action:

  • SECCOMP_RET_ALLOW: permit the call.
  • SECCOMP_RET_ERRNO: fail it with a chosen errno (often EPERM), letting the program continue. Gentler than killing.
  • SECCOMP_RET_KILL_PROCESS: terminate the whole process immediately. The safest default for a deny.
  • SECCOMP_RET_LOG / SECCOMP_RET_TRACE: log or hand to a tracer, useful while profiling.

A hard rule: seccomp filters are one way. Once installed they cannot be relaxed, and they are inherited across fork and exec. That is why a service installs its filter as late as possible during startup, after it has finished doing setup work that might need broader syscalls. Installing a filter also requires either CAP_SYS_ADMIN or, far more commonly, setting NoNewPrivs first (see the Process Model page), which is why the two almost always appear together.

Hands-on: block a syscall by hand

You do not need to write C to see seccomp work. systemd-run can apply a filter to any command via SystemCallFilter, letting you watch a blocked call fail.

# Allow a default set but deny the socket-creation syscalls, then try to use the network
sudo systemd-run --pty -p SystemCallFilter='~@network-io' \
  curl -s https://example.com

Expected output:

Running as unit: run-r41a.scope
curl: (7) Couldn't connect to server

The ~ means “deny this set.” curl could not create a socket because the socket family of calls was filtered, even though it ran with the same UID and network access as before. The syscall wall stopped it, not any permission check.

Compare a positive check with the kernel’s own view. After a process installs a filter, /proc/<pid>/status reports it.

# Confirm a running service actually has a filter loaded
pid=$(pgrep -x systemd-timesyncd | head -1)
grep -E 'Seccomp|NoNewPrivs' /proc/"$pid"/status

Expected output:

Seccomp:	2
Seccomp_filters:	1
NoNewPrivs:	1

Seccomp: 2 is filter mode, Seccomp_filters: 1 is the count of installed filters, and NoNewPrivs: 1 is the flag that let an unprivileged install happen. This is the fingerprint of a hardened daemon.

How runtimes and systemd apply it for you

Almost nobody writes raw BPF for seccomp. Three layers do it for you:

  • libseccomp turns a high-level allow/deny list into the BPF program. Tools and language bindings build on it, so you express intent as syscall names, not bytecode.
  • systemd exposes SystemCallFilter= on units, with convenient named groups like @system-service (a sane default allowlist), @network-io, @mount, and @privileged. A single line, SystemCallFilter=@system-service, gives most daemons a strong baseline. Pair it with SystemCallArchitectures=native to block the x32 and 32-bit compat entry points, which are a classic filter bypass.
  • Container runtimes ship a default seccomp profile. Docker’s default blocks around 40 dangerous syscalls; containerd and CRI-O do similar. In the Kubernetes Security foundation this is the seccompProfile: RuntimeDefault (or a custom Localhost profile) in a pod’s securityContext, and it is exactly the same seccomp-bpf mechanism described here.

A solid systemd hardening block combines all three ideas:

[Service]
NoNewPrivileges=yes
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
SystemCallArchitectures=native

Practical Guidance

  1. Turn on RuntimeDefault seccomp for every container. It is off by default in vanilla Kubernetes unless you set it, and it blocks the syscalls most escapes rely on.
  2. For systemd services, start from SystemCallFilter=@system-service and deny @privileged and @mount; add SystemCallArchitectures=native to close the compat-ABI bypass.
  3. Prefer killing over erroring (SECCOMP_RET_KILL_PROCESS) for genuinely unexpected syscalls; a service that silently gets EPERM can behave in surprising ways, while a hard kill fails loud.
  4. Profile before you tighten. Run the workload under a logging action (systemd’s SystemCallLog= or strace -f -c) to learn the real syscall set instead of guessing and breaking it in production.
  5. Always set NoNewPrivileges=yes alongside a filter. It is what lets an unprivileged process install seccomp and it blocks setuid escalation at the same time.