Securing service units with systemd's built-in seccomp sandboxing: SystemCallFilter, SystemCallArchitectures, and RestrictAddressFamilies. Combined with NoNewPrivileges and ProtectSystem, hardening is done from the unit file without changing application code.

In episode 8 you locked down containers with JSON profiles. But not every application lives in a container — many important daemons (databases, small services, internal tools) run directly on the host as systemd services. For those, writing a JSON profile isn't practical; what you need is a way to lock down syscalls directly from the unit file.
Episode 9 covers systemd's built-in seccomp sandboxing: SystemCallFilter, SystemCallArchitectures, and RestrictAddressFamilies, combined with NoNewPrivileges and ProtectSystem. The advantage is clear: you tighten a service without changing application code, purely from the unit file.
systemd doesn't send syscalls itself — it translates unit directives into BPF filters loaded into the kernel when the service starts. SystemCallFilter=@system-service isn't just text; behind the scenes systemd builds the exact same BPF program you'd write with libseccomp, then applies it to the service's processes.
There's one fundamental key that makes all this sandboxing removable: no_new_privs. systemd sets it via NoNewPrivileges=true — a kernel flag ensuring the process can't raise its privileges (for example via setuid binaries) and simultaneously making the seccomp filter remain valid without special privileges. Without it, a filter that denies setuid could be bypassed.
Note
Note: systemd applies a number of syscalls considered harmful to all services by default, even before you touch SystemCallFilter. This directive isn't an optional feature that's "sometimes active" — it works continuously, and you extend it as needed.
The SystemCallFilter directive accepts a list of syscalls and/or groups prefixed with @. Without the ~ prefix, the directive means an allow-list (only those syscalls are allowed); with the ~ prefix, it means a deny-list:
[Service]
# Allow-list: only syscalls in this group may be called
SystemCallFilter=@system-service
# Deny-list: everything allowed, except groups marked with ~
SystemCallFilter=~@reboot @swap @obsoleteGroups with the @ prefix make life easier — one name represents dozens of syscalls. The most commonly used:
@system-service — the set of syscalls reasonable for a common service; a safe starting point for most daemons.@file-system — filesystem operations like openat, unlink, rename.@network-io — network syscalls like socket, connect, accept.@privileged — syscalls requiring privileges, most of which a normal service doesn't need.@obsolete, @reboot, @swap — prime candidates for blocking.When a syscall is blocked, the service receives the EPERM errno by default. You can change it with SystemCallErrorNumber=. The default is sensible for most cases.
Tip
Start with SystemCallFilter=@system-service as an allow-list, then run the service in staging. If the service calls a syscall not in the group, systemd logs the failure in the service log — read it with journalctl -u nama.service and add the missing syscall or group. This log-first-then-expand pattern is exactly the episode 7 flow.
The SystemCallArchitectures directive controls which syscall architectures a service may use. Its default value is native — only the kernel's native architecture is allowed. This matters because a 64-bit kernel can accept syscalls from 32-bit binaries (compat mode), and holes in that compat path have been sources of security bugs:
[Service]
SystemCallArchitectures=nativeAs long as your service doesn't need to run 32-bit binaries, keep this value. Any syscall from another architecture is rejected immediately regardless of SystemCallFilter rules — the outer fence before the inner fence.
Back to episode 5: seccomp can filter syscall arguments, and socket() has a first argument — the address family. systemd abstracts this with RestrictAddressFamilies:
[Service]
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6A service can only open sockets in the listed families. AF_PACKET (raw packets), AF_NETLINK (communication with the kernel), and any AF_UNIX not allowed are all blocked. For a daemon serving TCP only, this short list is more than enough — and it cuts off data exfiltration paths through other protocols.
Now let's combine all the seccomp directives with systemd's filesystem sandboxing in one realistic unit:
[Unit]
Description=Hardened webapp service
[Service]
ExecStart=/usr/local/bin/webapp
User=webapp
Group=webapp
Restart=on-failure
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
RestrictSUIDSGID=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@reboot @swap @obsolete
SystemCallErrorNumber=EPERM
[Install]
WantedBy=multi-user.targetReading this unit layer by layer:
NoNewPrivileges=true — the master key; without it, part of the seccomp directives can't be guaranteed.ProtectSystem=strict — the whole / becomes read-only for the service; only explicitly allowed paths can be written.ProtectHome=true — home directories are invisible.RestrictAddressFamilies, SystemCallArchitectures, SystemCallFilter — the three seccomp layers: socket families, architecture, and syscall list.Note what doesn't change: ExecStart is exactly as usual, the application knows nothing. All the hardening comes from the unit file — that's the core strength of the systemd approach.
Claiming a unit is more secure is easy; proving it takes tools. systemd-analyze security assesses a unit's exposure and shows which directives are applied:
systemd-analyze security webapp.serviceExample output (truncated):
NAME DESCRIPTION EXPOSURE
PrivateNetwork=no Service has access to network 0.5
RestrictAddressFamilies=yes Address family restricted 0.1
SystemCallFilter=yes System call allowlisted 0.1
NoNewPrivileges=yes Privilege escalation prevented 0.1
→ Overall exposure level for webapp.service: 1.2The lower the exposure score, the more closed-off the service. Also verify the service's actual status with systemctl show webapp.service to make sure the directives are truly active, not just written.
Warning
systemd sandboxing isn't a reason to ignore other seccomp. ProtectSystem and SystemCallFilter check different dimensions: one limits filesystem access, the other limits actions against the kernel. Combine both for defense in depth — like installing a door lock and an alarm together, not choosing one.
A few things that will save you in production:
SystemCallFilter one level per release. A service that stops after tightening is a signal of a missing syscall, not a reason to give up.strace -f -c before composing SystemCallFilter — the result is a map of the actual syscalls.@system-service plus additions) is safer. A deny-list (~@reboot etc.) is only for patching what's clearly dangerous.In episode 9 you understood that systemd translates unit directives into seccomp filters: SystemCallFilter for syscall lists and @ groups, SystemCallArchitectures=native to close the compat architecture path, and RestrictAddressFamilies to lock down socket families. Combined with NoNewPrivileges and ProtectSystem, a single unit file can turn an ordinary daemon into a service far harder to breach — without touching application code.
The keys to take home:
SystemCallArchitectures=native is an almost-always-free outer fence.systemd-analyze security, not just claims in the unit file.All the layers so far are applied per-machine: per-container, per-service. How do you manage it when there are hundreds of machines? In episode 10, we move into Seccomp in Kubernetes: the seccompProfile field in securityContext, the evolution from alpha annotations, Pod Security Standards, and distributing custom profiles across nodes.