JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Sandboxing: seccomp, Capabilities, Privilege Separation

Guides 1 through 3 fought to stop the attacker from ever taking control. This one concedes that they might — and asks a different question: once code is running, how little can we let it actually do?

A different bet: assume the attacker gets in

Everything in this rung so far has been one long fight to stop the attacker from ever seizing control. Guides 1 and 2 showed how a corrupted pointer becomes control-flow hijacking and then a ROP chain; guide 3 showed the mitigations — ASLR, NX, canaries, CFI — that try to make that seizure impossible. Sandboxing takes a humbler and, frankly, more realistic stance. It assumes that despite all of that, one day a bug will win and attacker-chosen code will run inside your process. Given that, it asks a separate question: when that code runs, how small can we make the blast radius?

The reason that question even has teeth is that a running process is wildly more powerful than the job it was hired to do. Think of a PDF viewer: its honest task is to read bytes and draw pixels. Yet by default it inherits the full authority of the user who launched it — it can open any file you can open, delete your home directory, make network connections, spawn new programs. None of that is needed to render a page, but all of it is available the moment an attacker's ROP chain starts executing. That gap between authority granted and authority needed is the attacker's real prize. Sandboxing is the discipline of closing it.

Where authority actually lives: the syscall boundary

To shrink what code can do, you first have to ask where its power comes from. Recall from the operating-systems rung that a user process, on its own, can only shuffle bytes around inside its own memory — add registers, follow pointers, run gadgets. That is all harmless arithmetic. The instant it wants to touch anything outside itself — read a file, open a socket, fork a child, send a signal — it cannot just do it. It must ask the kernel by making a system call, crossing from user mode into kernel mode. Every real-world consequence a program can have flows through that one narrow gate.

This is the insight the whole field of sandboxing rests on, so let it land. Because all of a process's authority is exercised through system calls, the syscall boundary is the natural place to install a chokepoint. If you could stand at that gate and refuse the dangerous requests — "this process may read and write the file descriptors it already holds, but it may not open new files, may not connect to the network, may not exec another program" — then even an attacker with total control of the process is reduced to whatever the calls you did allow can accomplish. A ROP chain that hijacks rip is still stuck behind the same gate. It can compute anything in memory, but it cannot reach out into the world without asking, and the gatekeeper says no.

seccomp: a filter on the gate itself

seccomp (secure computing mode) is Linux's mechanism for putting exactly that filter on the syscall gate, enforced by the kernel itself. A process installs a small filter program — written in a restricted bytecode (classic BPF) — that the kernel runs on every system call the process makes, before the call is carried out. The filter inspects the syscall number (from the syscall table) and its argument registers, then returns a verdict: allow the call, deny it with an error like EPERM, kill the process outright, or trap to a handler. Because the kernel runs the filter, there is no wrapper to bypass — the check sits under the attacker, not beside them.

The discipline that makes a seccomp filter strong is the default-deny rule, and it is worth stating plainly because the opposite is a common, dangerous mistake. A good sandbox does not list the calls it forbids — that list is endless and you will miss one. Instead it starts from deny everything and then explicitly allows the tiny set the program genuinely needs. A media decoder, once it has its input and output file descriptors open, might allow only read(), write(), and exit() and forbid literally everything else. Then an attacker who lands a perfect arbitrary read/write still cannot open a file, cannot call execve to launch a shell, cannot open a network socket — the kernel refuses the syscall before it begins.

seccomp filter, conceptually (default-deny):

  for each syscall the process makes:
      if   nr == read   -> ALLOW
      elif nr == write  -> ALLOW
      elif nr == exit   -> ALLOW
      else              -> KILL the process

  result: a hijacked process can still
  compute in memory, but execve("/bin/sh")
  never reaches the kernel -> attack stalls.
A seccomp filter as a tiny default-deny gatekeeper: it runs in the kernel on every syscall, lets through only the handful the job needs, and kills anything else — so even total control of the process buys the attacker no new authority.

Be honest about the limits, though, because seccomp is sharp but narrow. It filters on the syscall number and the raw register values of the arguments — and crucially it cannot safely dereference a pointer argument, because that pointer lives in the process's memory which the attacker may have just rewritten (a classic time-of-check-to-time-of-use hazard). So seccomp can say "no openat at all," but it cannot reliably say "openat is fine only for files under /tmp" — the path is behind a pointer it must not trust. Coarse, structural rules are its strength; fine-grained, content-dependent policy is a job for other tools.

Capabilities: splitting the all-powerful root

seccomp narrows which calls a process may make. POSIX capabilities narrow something different — how much privileged authority it carries. The problem they solve is the old Unix all-or-nothing split: a process is either an ordinary user, or it is root (user id 0) and can do essentially anything. Many programs need just one root-level power — a web server needs to bind to port 80, a ping tool needs to send raw network packets — but historically they had to run as full root to get it, which meant an attacker who broke in got all of root's power, not just the one piece the program needed.

Capabilities chop that monolithic root power into dozens of independent pieces that can be granted one at a time. CAP_NET_BIND_SERVICE is the right to bind low-numbered ports; CAP_NET_RAW is the right to craft raw packets; CAP_SYS_ADMIN is a notoriously broad grab-bag; and so on. You can hand a program exactly the one capability it needs and nothing else. The web server gets CAP_NET_BIND_SERVICE and runs otherwise as an unprivileged user — so if it is compromised, the attacker inherits the right to bind a port and not one bit more. This is the principle of least privilege made operational: grant the minimum, and a breach stays small.

Capabilities are a real improvement but not a clean partition, and pretending otherwise misleads. Some capabilities are so broad they are effectively root by another name — CAP_SYS_ADMIN alone grants such a sprawl of powers that holding it is close to game over, and several others can be chained to escalate back to full root. So capabilities sharply reduce risk for the narrow, well-chosen cases (bind a port, set the clock) but are not a magic decomposition of root into safely-separable atoms. Granting one is a real decision: read what it actually permits, and never reach for CAP_SYS_ADMIN as a convenience.

Privilege separation: split the program, not just the rules

seccomp and capabilities both shrink the authority of a single process. Privilege separation is a structural idea that goes one level up: split the program itself into two or more processes, with the dangerous work and the powerful work deliberately held apart. The reasoning is simple and forceful. The code most likely to be exploited is the code that touches untrusted input — the parser chewing on a malicious PDF, the protocol handler reading bytes off a hostile network. The authority an attacker most wants is the powerful stuff — opening files as root, holding the private key. Privilege separation puts those two in different processes so that breaking the first does not hand you the second.

The canonical design splits into a small, trusted monitor that holds the privilege, and a large, untrusted worker (often called the unprivileged child) that does all the risky parsing. The famous example is OpenSSH: a tiny privileged process owns the host key and authentication; a separate, heavily restricted child handles the raw bytes coming off the wire. When the child needs something privileged done, it does not do it — it sends a narrow, specific request to the monitor over a pipe or a socket, and the monitor, which checks each request against a fixed policy, performs it on the child's behalf. The worker can be utterly compromised and still only ask; it can never reach the key directly.

Now see how cleanly the three techniques compose, because in practice they are layered together, not chosen between. The unprivileged worker starts up, opens the file descriptors and sockets it will need, then drops every capability it does not require and installs a tight default-deny seccomp filter — after which it can never regain authority even if hijacked. The privileged monitor keeps the one capability that matters and exposes a deliberately minimal request interface. The result is an attacker who lands a perfect exploit in the worker and finds: no capabilities to abuse, almost no syscalls permitted, and the only powerful actor reachable solely through a tiny, validated message channel. Each layer covers a gap the others leave.

  1. Identify the trust boundary. Find where untrusted input first enters — the parser, the network reader — and where the real authority lives — the private key, the root-only operation. Those are the two ends a trust boundary must run between.
  2. Split into processes. Put the risky parsing in an unprivileged worker and the authority in a small monitor, so a breach of one is not a breach of the other. This is the core move of privilege separation.
  3. Drop privilege in the worker. As early as possible, after it has opened what it needs, shed every capability it can and lower its credentials to an unprivileged user — authority you have already given up cannot be stolen.
  4. Clamp the syscalls. Install a default-deny seccomp filter allowing only the few calls the worker still legitimately needs, so a hijacked worker cannot open files, exec a shell, or reach the network.
  5. Make the monitor paranoid. Have the privileged side validate every request from the worker against a fixed, minimal policy and assume the worker is hostile — the monitor is now the entire remaining attack surface, so keep it tiny.

What sandboxing does and does not buy you

It is worth ending on an honest accounting, because a sandbox oversold is a dangerous thing. These mechanisms are the foundation of how real isolation is built: browser tabs, container runtimes, and modern app stores all rest on seccomp, capabilities, and namespaces — and the same least-authority idea reappears, more cleanly, in capability-based security designs and in WebAssembly runtimes like the WASI sandbox. When done well, a sandbox genuinely turns a full code-execution exploit into a frustrating dead end, which is an enormous, real win.

But never mistake a sandbox for a wall. It does not stop the bug from existing or the process from being hijacked — it only limits what the hijacker can reach. And the boundary it draws is exactly as good as it is small: a sandbox that permits one too many syscalls, or grants one capability too broad, or whose monitor mishandles one request, has a hole, and attackers hunt those holes for a living. Worse, the kernel that enforces the sandbox is itself a vast surface — a bug in a permitted syscall's implementation can let a sandboxed process escape, which is why sandbox escapes are prized vulnerabilities.

Hold the right mental model and the whole rung clicks into place. Guides 1 and 2 are how the attacker gets in; guide 3 raises the price of getting in; this guide shrinks the prize once they do; and guide 5 will show a category — side channels like Spectre — that leaks secrets without making any forbidden syscall at all, slipping under the syscall gate entirely. Every layer assumes the one before it can fail. That assumption is not pessimism; it is the entire reason defense in depth works, and it is the most important habit of mind a systems-security engineer can carry.