Back to posts
Post

Isolating Dangerous Commands with systemd-nspawn and seccomp

Use systemd-nspawn with a custom seccomp profile to block risky syscalls and test untrusted commands safely in a minimal container.

Linuxsystemd-nspawnseccompcontainer securitysandboxing

When you need to run something you don’t fully trust — a script from the internet, a binary from an unvetted source, or a student’s assignment in a lab — you don’t want it touching your host. Full VMs are heavy; Docker can be overkill if you just need process isolation. That’s where systemd-nspawn shines. It’s lightweight, built into systemd, and lets you run a container with almost no overhead. Combined with a tight seccomp profile, you can block dangerous system calls like ptrace, mount, or raw I/O, effectively sandboxing the process.

I use this pattern often when testing packages from the AUR or evaluating build scripts. Instead of throwing them into a disposable VM, I spin up a minimal Debian or Ubuntu container with debootstrap, apply a seccomp filter, and run the command inside. If it tries something nasty, the kernel kills it immediately — no harm to the host.

First, create a minimal container. I prefer Debian for its small footprint:

# debootstrap --variant=minbase stable /var/lib/container/test http://deb.debian.org/debian

This gives you a barebones system in about 100 MB. No extra services, just a shell and basic tools. Then, you can enter it with:

# systemd-nspawn -D /var/lib/container/test

You’re now root inside the container. But root inside a container is still root — if the container has access to dangerous syscalls, it can still do harm. That’s where seccomp comes in.

seccomp (secure computing mode) lets you filter which system calls a process can make. You can define a profile that allows only what’s needed and blocks everything else. I start with a default deny list and then add exceptions for essential calls like read, write, exit, and brk.

Here’s a minimal seccomp profile I use for testing untrusted binaries:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_X86"
  ],
  "syscalls": [
    {
      "name": "read",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "write",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "exit",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "exit_group",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "brk",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "mmap",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "mprotect",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "munmap",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "openat",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "close",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "fstat",
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "name": "getdents64",
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Save this as /etc/systemd/nspawn/test.seccomp. Then launch the container with the profile enforced:

# systemd-nspawn -D /var/lib/container/test --seccomp-profile=/etc/systemd/nspawn/test.seccomp

Now, if a process inside tries to call ptrace, mount, open of /dev/mem, or even clone with CLONE_NEWUSER, the kernel will return an error (by default, EPERM) and the call fails. The process might crash, but your host is safe.

You can test this easily. Inside the container, run:

# ptrace(PTRACE_TRACEME, 0, NULL, NULL)

You’ll get a segmentation fault or an error — not because the program is buggy, but because the kernel blocked the syscall. That’s the point.

One gotcha: if you’re running something that needs more syscalls (like a shell that runs scripts), you’ll need to adjust the profile. I often start with the default profile from man seccomp.filter or extract one from strace -f of a known-good run, then tighten it.

Warning: Don’t rely on seccomp alone for multi-tenant security. It’s a layer, not a fence. Combine it with user namespaces (--private-users-pid), read-only mounts (--bind-ro), and dropping capabilities if you need stronger isolation. But for quick testing of untrusted commands? systemd-nspawn + seccomp is fast, clean, and effective.

As I mentioned before in my post about auditd monitoring /etc/passwd, defense in depth matters. Here, seccomp is the first line — stopping the bad call before it even happens. No need to log and react; the kernel prevents it outright.

This setup takes two minutes to bootstrap and adds virtually no overhead. If you’re doing any kind of untrusted code evaluation — whether in a lab, a CI pipeline, or just curious testing — give it a try. It’s one of those quiet tools that saves you from a bad day.


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