Back to posts
Post

Using systemd-nspawn seccomp profiles for risky command testing

Learn how to safely run dangerous commands in isolated containers using Capabilities= and SystemCallFilter= for better security than chroot.

Linuxsystemdsecuritysandboxseccomp

When I need to test risky commands or scripts, I avoid running them directly on production hosts. Instead, I use systemd-nspawn with seccomp profiles to create a tightly controlled sandbox. This approach gives me more security than a basic chroot because I can drop specific Linux capabilities and filter system calls at the kernel level.

Why systemd-nspawn over chroot

A chroot only changes the root filesystem view. It doesn't limit what processes can do inside that jail. A compromised process can still escalate privileges if it has the right capabilities or can make dangerous syscalls. systemd-nspawn, by contrast, runs in a full container context with built-in resource isolation and integrates directly with systemd's security features.

I often mention that defense in depth starts with reducing the attack surface. As I covered in my post about limiting OOM scores for critical services, the same principle applies here: don't give a process more access than it needs.

Setting up a minimal sandbox

I start by creating a minimal directory tree for the container. You can use debootstrap or pacstrap depending on your distro, but for quick testing, I sometimes just copy a few binaries.

mkdir -p /var/container/test
mkdir -p /var/container/test/{bin,lib,lib64,usr/bin,usr/lib}
cp /bin/bash /var/container/test/bin/
cp /bin/ls /var/container/test/bin/
# Copy required libraries using ldd

Then I launch the container with strict defaults:

systemd-nspawn \
  --directory=/var/container/test \
  --capability=none \
  --system-call-filter=@system-service \
  --private-network \
  --bind=/tmp:/tmp

The --capability=none flag drops all Linux capabilities. Even if the process runs as root inside the container, it cannot perform privileged operations like loading kernel modules or modifying network interfaces.

Filtering system calls with SystemCallFilter=

The real power comes from SystemCallFilter=. Instead of allowing all syscalls, I whitelist only those needed for basic operation. The @system-service group is a good starting point—it includes calls like read, write, exit, and getpid but blocks dangerous ones.

To see what's blocked, I test with a command that tries to use a restricted syscall:

systemd-nspawn --directory=/var/container/test --capability=none --system-call-filter=@system-service bash -c 'echo 1 > /proc/sys/kernel/sysrq'

This fails with a permission denied error because writing to /proc/sys requires the sys_admin capability, which we've dropped, and the underlying open/write may still be allowed, but the kernel enforces the restriction.

For more control, I define a custom filter. For example, to allow only file I/O and process control:

systemd-nspawn --directory=/var/container/test \
  --capability=none \
  --system-call-filter=read,write,open,close,exit,exit_group,fork,clone \
  --bind=/tmp:/tmp

This setup prevents the container from making network-related syscalls like socket, connect, or bind, which stops reverse shells or port scans even if the binary is present.

Testing risky commands safely

Now I can run something like curl http://internal-service:8080/admin or rm -rf / without fear. The container might allow the command to start, but it will fail when it tries to make a disallowed syscall.

I once tested a script that attempted to modify /etc/shadow. Even though it ran as root inside the container, the open syscall for writing to that file was blocked because the container's root filesystem is private and read-only by default unless explicitly bound.

Limitations and gotchas

This isn't a full VM replacement. If you need to test kernel modules or hardware access, you'll need a different approach. Also, overly strict syscall filtering can break legitimate binaries in unexpected ways. I recommend starting with @system-service and gradually adding calls only when you see SIGSYS errors in the journal.

You can monitor violations with:

journalctl -u [email protected] -f

Look for lines like SECCOMP or SIGSYS to see which syscall was blocked.

When to use this

I use this pattern for:

  • Testing third-party scripts before deployment
  • Running untrusted input processors
  • Isolating legacy tools with unknown dependencies

It's faster to spin up than a VM and lighter than a full container runtime like Docker when you don't need images or layers.

Final thoughts

systemd-nspawn with seccomp filtering gives me a practical way to apply the principle of least privilege to temporary workloads. It's not foolproof, but combined with capability dropping and read-only binds, it creates a barrier that's significantly harder to escape than a chroot.

If you're already using systemd on your host, this tool is already installed and ready to go. Start small, test your filters, and treat every sandbox as a temporary boundary—not a permanent security guarantee.


Cover image: personalgraphic.official · CC0 (Openverse / kamu malı) · https://www.flickr.com/photos/198895458@N04/53097628210