DEV Community

Cover image for Practical Linux seccomp Sandboxing for Services with systemd
Lyra
Lyra

Posted on

Practical Linux seccomp Sandboxing for Services with systemd

Practical Linux seccomp Sandboxing for Services with systemd

Linux services often run with far more privileges than they need. One of the most effective ways to shrink the attack surface is syscall filtering with seccomp-BPF.

seccomp (secure computing mode) lets you define an allowlist (or denylist) of system calls a process is permitted to make. Violations result in the process being killed or receiving an error—before the kernel even executes the dangerous call.

This post shows how to apply it practically with systemd's built-in support and the libseccomp library.

Why seccomp matters

Modern Linux kernels expose hundreds of syscalls. A typical web server or database only needs a small subset. By restricting everything else you:

  • Block many privilege-escalation and container-escape techniques
  • Make exploits that rely on execve, ptrace, mount, or keyctl fail immediately
  • Get auditable violations via the kernel's audit subsystem

Docker and Podman already ship default seccomp profiles. systemd brings the same power to native services.

Using SystemCallFilter= in systemd (easiest path)

systemd exposes seccomp through the SystemCallFilter= directive (and related settings). No code changes required.

Basic example

Create a drop-in for an existing service (e.g., nginx):

sudo systemctl edit nginx.service
Enter fullscreen mode Exit fullscreen mode

Add:

[Service]
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
NoNewPrivileges=yes
Enter fullscreen mode Exit fullscreen mode

@system-service is a predefined group that covers the common needs of most daemons (see systemd-analyze syscall-filter for the full list).

Tighter, profiled allowlist

For a more restrictive profile, first observe what the service actually uses:

sudo strace -f -e trace=all -o /tmp/nginx.trace nginx -g 'daemon off;'
# or use:
sudo systemd-analyze syscall-filter --json | jq
Enter fullscreen mode Exit fullscreen mode

Then create a minimal filter:

[Service]
SystemCallFilter=~@obsolete @cpu-emulation @debug @keyring @mount @raw-io @reboot @swap @obsolete
SystemCallFilter=@system-service
SystemCallArchitectures=native
SystemCallErrorNumber=EPERM
Enter fullscreen mode Exit fullscreen mode

The ~ prefix means "deny these groups". Combine with an explicit allowlist for defense-in-depth.

Verification

After reload:

systemctl daemon-reload
systemctl restart nginx
journalctl -u nginx -xe | grep -i seccomp
Enter fullscreen mode Exit fullscreen mode

Successful violations appear as SECCOMP audit messages.

Using libseccomp for custom applications

When you control the source, libseccomp gives fine-grained control and works inside the application itself (after dropping privileges).

Minimal C example

#include <seccomp.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL_PROCESS);
    if (!ctx) return 1;

    // Allow only the syscalls we actually need
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(brk), 0);
    seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(mmap), 0);

    if (seccomp_load(ctx) != 0) {
        seccomp_release(ctx);
        return 1;
    }

    printf("Hello from a seccomp-restricted process\n");
    seccomp_release(ctx);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Compile with -lseccomp.

Any attempt to call open, socket, execve, etc. will immediately terminate the process with "Bad system call".

Production patterns

  • Apply the filter after initialization and privilege dropping, but before entering the main event loop.
  • Use SCMP_ACT_ERRNO(EPERM) for some calls instead of KILL so glibc fallbacks don't crash the process.
  • Log violations via auditd (journalctl _AUDIT_TYPE_NAME=SECCOMP).

See the excellent write-up at https://thomastrapp.com/blog/securing-a-cpp-websocket-server-with-libseccomp/ for a real-world websocket server example.

Combining with other systemd hardening

seccomp works beautifully alongside:

  • CapabilityBoundingSet= and AmbientCapabilities=
  • ProtectSystem=, ProtectHome=, PrivateTmp=
  • NoNewPrivileges=yes
  • RestrictAddressFamilies=, RestrictRealtime=

Run systemd-analyze security yourservice.service before and after to see the score improvement.

Sources & further reading

Start with SystemCallFilter=@system-service on a non-critical service, measure the impact, then tighten further. The reduction in exposed kernel surface is immediate and measurable.

Top comments (0)