Skip to content
shellhacks.dev
Security

How to Use seccomp and Namespaces to Limit a Process

Limit a Linux process with namespaces and seccomp: unshare, systemd sandbox options, SystemCallFilter, no_new_privs, and what each layer protects against.

6 min read
Summarize in AI
Illustration for "How to Use seccomp and Namespaces to Limit a Process": a terminal session, a process monitor, file permission bits.

A service that is exploited gets the full authority of its user. Sandboxing limits that authority: it can see fewer files, fewer processes and fewer kernel interfaces. Containers use these features, and you can use them directly for a script or a systemd service without a container runtime. It builds on least privilege for scripts and hardening containers. The commands assume Linux 6.x, util-linux 2.40 and systemd 255.

Namespaces by hand

Shell
sudo unshare --pid --fork --mount-proc bash -c 'ps aux | head -3; hostname sandbox; hostname'
sudo unshare --net bash -c 'ip -brief addr'
unshare --user --map-root-user id
Text
USER  PID %CPU %MEM COMMAND
root    1  0.0  0.0 bash -c ps aux | head -3; hostname sandbox; hostname
root    2  0.0  0.0 ps aux
sandbox
lo    DOWN   127.0.0.1/8 ::1/128
uid=0(root) gid=0(root) groups=0(root)

The new PID namespace shows only the processes inside it, with the shell as PID 1. Changing the hostname there does not touch the host (UTS namespace). In a network namespace there is only a downed loopback interface, so the process cannot talk to anything. A user namespace maps your user to root inside only, an unprivileged way to experiment. Other namespaces cover mounts, IPC, cgroups and time. Containers are these combined.

Seccomp: limit system calls

Shell
grep Seccomp /proc/self/status
docker run --rm alpine grep Seccomp /proc/self/status
Text
Seccomp:	0
Seccomp:	2

Mode 2 means a filter is active. Docker applies a default profile that blocks around 40 dangerous system calls. A seccomp filter is a list of allowed (or denied) calls, and a call outside the list returns an error or kills the process. Writing one by hand is error-prone, so use presets. Setting no_new_privs first (via NoNewPrivileges=yes in systemd) is required for unprivileged filters and blocks setuid escalation.

The practical route: a systemd unit

INI
[Service]
ExecStart=/usr/local/bin/myapp
User=myapp
DynamicUser=yes
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictNamespaces=yes
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
SystemCallArchitectures=native
CapabilityBoundingSet=
ReadWritePaths=/var/lib/myapp

Each line removes a capability: a private /tmp, no real devices, a read-only file system except listed paths, no kernel tuning, no new namespaces, only IP and Unix sockets and a syscall allow list for typical services. DynamicUser=yes allocates a throwaway user. Add lines gradually and restart the service after each, because over-restriction shows up as obscure failures. Read the denied calls in the journal; SystemCallFilter=~@privileged is a gentler start.

Score the result

Shell
systemd-analyze security myapp.service --no-pager | tail -15
Text
→ Overall exposure level for myapp.service: 2.1 OK šŸ™‚

The tool rates the unit from 0 (safe) to 10 (unsafe) and lists each directive that is missing, with its weight. Use it as a checklist, not a goal; a score of 1.0 for a service that needs network and disk access is often not achievable. The example score is illustrative.

Without systemd

Shell
bwrap --ro-bind /usr /usr --symlink usr/lib /lib --symlink usr/lib64 /lib64 \
  --proc /proc --dev /dev --tmpfs /tmp --unshare-all --die-with-parent \
  /usr/bin/python3 untrusted.py

Bubblewrap and firejail wrap an arbitrary command in namespaces with a few flags, handy for running a downloaded script. They are the engines behind Flatpak and some browser sandboxes. They are not a perfect jail: a kernel bug can still escape, and sharing the network or a writable directory shares the risk. For code you do not trust at all, use a virtual machine.

Thinking in capabilities, not users

Traditional Unix security asks who the process is. Sandboxing asks what the process can do, and then removes everything it does not need. A web server needs to bind a port, read its content and write a log. It does not need to load kernel modules, read other users' files, create network namespaces or call ptrace. Each unneeded ability is a step an attacker can take after exploiting a flaw. Listing what the service needs, then denying the rest, is the central exercise, and the systemd directives map nearly one to one onto that list.

The kernel offers several mechanisms and they complement each other. Namespaces limit what the process can see. Capabilities split root's powers into pieces, so a process can bind a low port without being able to change file ownership. Seccomp limits which system calls reach the kernel. Mandatory access control, such as AppArmor or SELinux, applies policy by file path or label. A service that uses several of them requires an attacker to defeat each in turn, and the combination is what makes a sandbox meaningful.

Building a profile iteratively

Start tight and loosen based on evidence, not the other way round. Apply the strict directives, start the service and read the journal for denials. A service that fails with "permission denied" on a path tells you exactly which directory to allow with a read-write setting. A service killed by a seccomp rule tells you which system call it needed through the audit log. Add only what is demonstrated, and write each addition in a comment with the reason. After a few iterations the unit holds a precise description of what the service requires, which is documentation as well as protection.

Test the unhappy paths too. A restricted service may work in normal operation and fail during log rotation, a reload, an update or a backup because those paths touch files the daily path never does. Exercise them in staging after each tightening, and keep the previous unit file so you can roll back quickly.

Limits you should know

Sandboxes reduce the damage of a compromise and do not prevent it. Kernel vulnerabilities can break out of any namespace-based isolation, so the kernel must be kept updated, and workloads that must resist hostile code need stronger isolation such as a virtual machine. A sandbox can also give false confidence: a service that has network access and credentials can still do harm by using them. Pair sandboxing with narrow credentials, outbound network filtering and monitoring, so that a compromised service has little to steal and no quiet way to send it out.

Applying this to small tasks

You do not need a service manager to benefit. Running an unfamiliar script with a few flags of a wrapper tool, a read-only view of the system, a temporary home and no network, costs seconds and shows what the script tries to do. If it fails because it wanted the network, you learned something worth knowing before running it for real. Make the wrapper a shell function and the habit becomes cheap.

Where this does not apply

Sandboxing is layered on top of fixing vulnerabilities and updating packages, not instead of it. It adds failure modes when applications need unexpected access. In containers, check the existing profile before replacing it. See the namespaces manual page, the seccomp manual page and the systemd sandboxing options.

For a quick review of how a particular process is confined, read its status files: grep -E "Seccomp|NoNewPrivs|CapEff" /proc/PID/status and ls -l /proc/PID/ns/ list the filter mode, the privilege flag and the namespaces it belongs to. Comparing these between a sandboxed service and an ordinary one makes the effect of each directive visible. The non-root containers post covers the user-level counterpart, and file permissions the layer below.

Also inLinux
Taggedprocesses
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.