Sandbox and isolation
Every request runs in a sandbox built from Linux namespaces, cgroup v2, a seccomp filter, rlimits and an unprivileged identity. This page describes the sandbox from the point of view of the code running in it. The threat model lists what is verified and how.
Processes
Section titled “Processes”faber (API process, holds the API key and the file store)└── jailer one per request; the faber binary re-executed ├── supervisor one per task │ ├── init PID 1 of the task's PID namespace │ └── task PID 2 there └── supervisor ├── init └── task- The jailer is a fresh, single-threaded process started for each request. It receives only the request, not the service’s memory, environment or open files, and it is killed if the service dies.
- Each task has a supervisor outside its PID namespace and an init (PID 1) inside it that reaps orphaned descendants. The supervisor starts the task after the init, in the same namespace; anything the task abandons is reparented to the init. The task cannot signal the init, and it cannot see or signal other tasks or host processes.
- The jailer and supervisors run in a
supervisorcgroup of the request with a fixed memory allowance and are the OOM killer’s last choice there.
Namespaces
Section titled “Namespaces”| Namespace | Scope | Effect |
|---|---|---|
| Mount | Request, plus a copy per task | The task sees a minimal root built for the request, not the image’s filesystem. Its copy only replaces /proc and /sys |
| Network | Request | No interfaces that reach the host or the internet, no DNS configuration |
| UTS, IPC | Request | Own hostname and System V IPC objects |
| PID | Task | Each task sees only its own process tree |
| User | Task | Inside: UID/GID 65534 (nobody). Outside: a host UID/GID leased to the request |
Filesystem
Section titled “Filesystem”| Path | What it is |
|---|---|
/faber | The workspace: a 128 MiB tmpfs, writable, shared by every task of the request. Tasks start here unless they set working_dir |
/tmp | A 128 MiB tmpfs, writable, shared by every task of the request. Not captured by the step cache |
/usr, /bin, /lib, /lib64 | Bind-mounted read-only from the image, so the toolchains you installed are available |
/etc | Only alternatives, ld.so.cache, passwd and group by default. No resolver configuration, hosts file or service configuration |
/proc | A procfs of the task’s own PID namespace, mounted with subset=pid: no /proc/cmdline, meminfo, stat, /proc/sys |
/sys | An empty read-only tmpfs |
/dev | null, zero, full, random, urandom; the request’s mqueue; links fd, stdin, stdout, stderr, and shm pointing into /tmp |
The read-only paths come from SANDBOX_READONLY_PATHS. If a toolchain keeps
files elsewhere (the JDK under /etc/java-17-openjdk, for example), add the
path there; see the configuration reference.
Both tmpfs mounts are nodev,nosuid. Files written by a task are owned by the
task user. /faber and /tmp disappear with the request.
Identity and privileges
Section titled “Identity and privileges”- Each running request leases one host UID/GID from the range
SANDBOX_IDENTITY_BASE..+ SANDBOX_IDENTITY_COUNT(100000-165535 by default). Concurrent requests never share one, so per-user kernel limits are charged to the request that caused them. - Tasks have no capabilities, no supplementary groups, and
NoNewPrivsset. setuid binaries do not elevate. chroot, mounting, changing the hostname, rewriting the user namespace map and creating new namespaces all fail.
System calls
Section titled “System calls”Before exec, every task installs a seccomp filter chosen by its
sandbox_profile:
- A denied syscall (namespace creation, mounts, kernel modules, keyrings,
BPF, ptrace and similar) kills the process that made it with
SIGSYS, which it cannot catch. When that is the task’s own process, the result hasoutcome: policy_violation. - Under the
v2profiles (the default), a syscall that is neither listed nor denied fails withENOSYS, as it would on an older kernel. nativeprofiles additionally denyclone,forkand sockets, so the task cannot start threads or processes.
Choosing sandbox profiles explains which to use.
Environment
Section titled “Environment”A task’s environment is exactly the env map of the task, plus
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin if you do
not set PATH. Nothing else is inherited: not the image’s ENV, not HOME,
not the service’s variables.
Some toolchains need a writable home or cache directory. Point them at /tmp
or the workspace:
{ "cmd": "go", "args": ["build", "-o", "main", "main.go"], "env": { "HOME": "/tmp", "GOCACHE": "/tmp/go-cache", "GOFLAGS": "-buildvcs=false" }}What tasks share
Section titled “What tasks share”Tasks of one request share the workspace, /tmp, the network and IPC
namespaces, and the request’s host identity (and so that identity’s per-user
kernel limits). They do not share a PID namespace or a cgroup.
Different requests share none of these. A request cannot observe or affect another request’s files, processes or limits.
Cleanup
Section titled “Cleanup”When the request ends, normally or not (timeout, OOM, client disconnect, service shutdown), every process of the request is killed through its cgroup, and the cgroups, mounts and root directory are removed. On startup, Faber also removes anything a crashed previous instance left behind.
Go deeper
Section titled “Go deeper”- Architecture and process model: the jailer, supervisors and inits, and which namespaces and cgroups each lives in.
- Isolation layers: every mechanism in the order it is applied, with what it stops and what it leaves open.
- The filesystem a task sees: every mount, and how submitted files are written.