Sandbox Reference
One sectioned schema shared by the CLI, the SDKs, and TOML profiles. Unless noted, every field is optional, and omitting one means no restriction beyond Sandlock's default seccomp blocklist, which is always applied.
Sections are named for the concern they cover: config, determinism, program, filesystem, network, http, syscalls, and limits. The Python Sandbox dataclass exposes the same fields as keyword arguments.
Where a Python name differs from its TOML key, both are listed. The differences are concentrated in [filesystem] and [limits], which drop the fs_ and max_ prefixes the dataclass carries, because inside a named section the prefix is redundant.
Synopsis
Python
from sandlock import Sandbox, BranchAction
sandbox = Sandbox(
# [config]
http_ca=None, http_key=None,
fs_storage=None, workdir=None,
# [determinism]
random_seed=None, time_start=None,
deterministic_dirs=False, no_randomize_memory=False,
# [program] (process knobs only; exec/args are arguments to .run/.cmd)
env={}, cwd=None, uid=None, gid=None,
clean_env=False, no_coredump=False, no_huge_pages=False,
no_supervisor=False,
# [filesystem]
fs_readable=(), fs_writable=(), fs_denied=(),
chroot=None, fs_mount={},
on_exit=BranchAction.COMMIT, on_error=BranchAction.ABORT,
# [network]
net_allow_bind=(), net_allow=(), port_remap=False,
# [http]
http_ports=(), http_allow=(), http_deny=(),
# [syscalls]
extra_allow_syscalls=(), extra_deny_syscalls=(),
# [limits]
max_memory=None, max_processes=64, max_open_files=None,
max_cpu=None, max_disk=None,
gpu_devices=None, cpu_cores=None, num_cpus=None,
# Runtime kwargs (not serialized as policy)
name=None, policy_fn=None, init_fn=None, work_fn=None,
# Advanced (internal; usually configured via the fields above)
notif_policy=None,
)
TOML profile
[config]
http_ca = "/path/to/ca.pem"
http_key = "/path/to/ca.key"
http_inject_ca = ["/etc/ssl/certs/ca-certificates.crt"]
http_ca_out = "/tmp/sandlock-ca.pem"
fs_storage = "/var/lib/sandlock"
workdir = "/opt/project"
[determinism]
random_seed = 42
time_start = "2026-01-01T00:00:00Z"
deterministic_dirs = true
no_randomize_memory = true
[program]
exec = "/usr/bin/make"
args = ["-j4"]
env = { CC = "gcc" }
cwd = "/work"
uid = 0
gid = 0
clean_env = true
no_coredump = true
no_huge_pages = true
no_supervisor = false
[filesystem]
read = ["/usr", "/lib"]
write = ["/tmp"]
deny = ["/proc/kcore"]
chroot = "/opt/rootfs"
mount = ["/work:/host/sandbox/work"]
on_exit = "commit" # "commit" | "abort" | "keep"
on_error = "abort"
[network]
allow_bind = [8080]
allow = ["api.example.com:443", "udp://1.1.1.1:53"]
port_remap = false
[http]
ports = [80]
allow = ["POST api.openai.com/v1/*"]
deny = ["* */admin/*"]
[syscalls]
extra_allow = ["sysv_ipc"]
extra_deny = []
[limits]
memory = "512M"
processes = 64
open_files = 256
cpu = 50
disk = "1G"
gpu_devices = [0]
cpu_cores = [0, 1]
num_cpus = 2
config
Top-level configuration for the supervisor and the COW workspace.
| Python | TOML | Type | Default | Description |
|---|---|---|---|---|
http_ca | http_ca | str | None | None | PEM CA certificate path for HTTPS interception. When set, port 443 is added to http_ports. |
http_key | http_key | str | None | None | PEM CA private key path. Required whenever http_ca is set. |
http_inject_ca | http_inject_ca | list[str] | [] | Trust bundle paths to splice the active CA's public cert into at open time. Without http_ca, generates an ephemeral CA (private key in memory only, never on disk) and intercepts port 443. Requires at least one HTTP rule. |
http_ca_out | http_ca_out | str | None | None | Writes the active CA's public certificate (PEM) here; never the private key. Requires at least one HTTP rule. |
fs_storage | fs_storage | str | None | None | Separate storage directory for the seccomp COW upper layer and deltas. |
workdir | workdir | str | None | None | COW root directory. Controls which directory COW tracks; does not set the child's working directory. |
HTTPS interception is opt-in. Without http_ca or http_inject_ca, port 443 is not intercepted and net_allow host:443 permits raw TLS with no content inspection. When http_ca is set, the CA must be one the caller generated and installed into the sandbox's trust store, typically under /etc/ssl/certs/.
File injection covers tools that read a trust file from disk: curl, git, the OpenSSL CLI, Go, Python's stdlib ssl, and Python requests and httpx through certifi's cacert.pem if you name that path. Runtimes with a compiled-in CA list, such as Node and Java, are not reachable by file injection; use http_ca_out and point the runtime's own environment variable at the exported certificate.
Credential injection
--credential NAME=SOURCE loads a secret into the supervisor, where SOURCE is env:VAR (the variable is also stripped from the child's environment), file:PATH, or fd:N. --http-auth "METHOD HOST/PATH AUTHSPEC NAME [replace|add-only]" then attaches that credential to a matching request in the proxy, strictly after the ACL check, so the child process never carries the value.
AUTHSPEC is one of bearer, basic:<user>, header:<name>, apikey:<name>, or query:<param>. The default replace overwrites an existing credential of that shape; add-only leaves a caller-supplied one in place.
Injection requires an HTTP ACL proxy, and injecting into an HTTPS host additionally requires http_ca or http_inject_ca. Over cleartext HTTP the secret reaches the upstream in plaintext, so Sandlock emits a one-per-run warning. --credential and --http-auth are CLI and builder flags, not [config] profile keys. Full examples are on the HTTP ACL page.
determinism
Knobs that pin sources of non-determinism in the child process.
| Field | Type | Default | Description |
|---|---|---|---|
random_seed | int | None | None | Seed for deterministic getrandom(). Identical seeds yield identical byte streams. |
time_start | float | str | None | None | Frozen start time as a Unix timestamp or an RFC 3339 / ISO 8601 string. Time then advances at real speed from that epoch. |
deterministic_dirs | bool | False | Sort readdir() entries lexicographically, so ls, glob, and os.listdir return a stable order. |
no_randomize_memory | bool | False | Disable ASLR via personality(ADDR_NO_RANDOMIZE). |
program
Process-level knobs applied to the child. In a TOML profile, exec and args also live here; in the Python SDK those are arguments to run() or cmd() and are not fields on Sandbox.
| Field | Type | Default | Description |
|---|---|---|---|
env | Mapping[str, str] | {} | Variables to set or override in the child. Applied after clean_env. |
cwd | str | None | None | Child working directory. Independent of workdir. |
uid | int | None | None | UID to map the child to inside a user namespace, for example 0 for fake root. Must be set together with gid. The child retains no host privileges regardless of the mapped UID. Requires user namespaces to be available. |
gid | int | None | None | GID to map inside the user namespace. Must be set together with uid. An unprivileged user namespace maps a single id, so supplementary groups are unavailable. |
clean_env | bool | False | Start from a minimal environment (PATH, HOME, USER, TERM, LANG) instead of inheriting the parent's. |
no_coredump | bool | False | prctl(PR_SET_DUMPABLE, 0). Disables core dumps and restricts other processes' access to /proc/<pid>. Breaks gdb, strace, and perf. |
no_huge_pages | bool | False | Disable transparent huge pages via prctl(PR_SET_THP_DISABLE). |
no_supervisor | bool | False | Skip the seccomp user-notification supervisor. The sandbox runs with Landlock plus a kernel-only deny filter, without IP allowlisting, resource limits, COW, chroot mediation, /proc virtualization, or custom handlers. Required when nesting inside another sandlock, since the kernel allows one SECCOMP_FILTER_FLAG_NEW_LISTENER per task. |
filesystem
Landlock filesystem rules plus chroot, mount mapping, and COW isolation.
| Python | TOML | Type | Default | Description |
|---|---|---|---|---|
fs_readable | read | Sequence[str] | () | Paths the sandbox may read, in addition to fs_writable. |
fs_writable | write | Sequence[str] | () | Paths the sandbox may read and write. |
fs_denied | deny | Sequence[str] | () | Paths explicitly denied, even where implied by a broader rule. |
chroot | chroot | str | None | None | Path to chroot into before applying the rest of the confinement. |
fs_mount | mount | Mapping[str, str] | {} | Map virtual paths inside the chroot to host directories. Python form {"/work": "/host/sandbox/work"}; TOML form a list of "VIRTUAL:HOST" strings. See the read-only note below. |
on_exit | on_exit | BranchAction | COMMIT | Branch action on normal sandbox exit. |
on_error | on_error | BranchAction | ABORT | Branch action on sandbox error or exception. |
Read-only mounts. A trailing :ro (or the default :rw) selects a read-only mount. The CLI honours it in --fs-mount and in profiles, and sandlock inspect --toml writes it back out. The Python SDK rejects such entries with PolicyError, since its mapping type cannot express the flag; load the profile with sandlock run --profile-file <path>, or use the C ABI's sandlock_sandbox_builder_fs_mount_ro.
Landlock rules are kernel-evaluated and TOCTOU-immune.
network
Outbound allowlist, bind allowlist, and port virtualization. Each net_allow entry is a rule naming protocol, host, and port. Rules are OR'd, and an empty net_allow denies all outbound traffic.
Protocol gating falls out of rule presence: without a UDP rule, UDP socket creation is denied at the seccomp layer; without an ICMP rule, kernel ping socket creation is denied. A scheme-less rule counts for both TCP and UDP; ICMP always needs icmp://. Raw ICMP (SOCK_RAW + IPPROTO_ICMP) is never exposed.
Rule shapes
host:port[,port,...]: no scheme prefix, covers TCP and UDP.tcp://host:port: TCP only.udp://host:port: UDP only.udp://*:*opens any UDP destination.icmp://host: kernel ping socket (SOCK_DGRAM+IPPROTO_ICMP).icmp://*opens any echo destination.
| Python | TOML | Type | Default | Description |
|---|---|---|---|---|
net_allow | allow | Sequence[str] | () | Outbound endpoint allowlist. Empty denies all outbound. |
net_allow_bind | allow_bind | Sequence[int | str] | () | TCP ports the sandbox may bind or listen on; a default-deny allowlist. Each entry is a port or a "lo-hi" range; "*" allows any port and cannot be mixed with port entries. Landlock ABI v4+, TCP only (UDP bind() is not separately gated). Mutually exclusive with net_deny_bind. |
net_deny_bind | deny_bind | Sequence[int | str] | () | TCP ports the sandbox may not bind; a default-allow denylist. Same port syntax, enforced on the on-behalf bind() path with Landlock's BIND_TCP relaxed. Mutually exclusive with net_allow_bind. |
port_remap | port_remap | bool | False | Transparent TCP port virtualization. Each sandbox gets an independent virtual port space; conflicting binds are remapped to unique real ports via pidfd_getfd. |
Hostnames are resolved once at sandbox creation and pinned via a synthetic /etc/hosts, which is injected only when at least one rule references a concrete host. Rules made purely of :port, udp://*:*, or icmp://* leave the host's real DNS configuration visible. The complete grammar is on the network model page.
http
HTTP-level access control through a transparent MITM proxy.
| Python | TOML | Type | Default | Description |
|---|---|---|---|---|
http_allow | allow | Sequence[str] | () | Allow rules of the form "METHOD host/path" with glob path matching. |
http_deny | deny | Sequence[str] | () | Deny rules, checked before allow rules. Same format. |
http_ports | ports | Sequence[int] | () | TCP ports to intercept. Defaults to [80]; 443 is added when http_ca is set. |
When either list is non-empty, the supervisor spawns the proxy and redirects matching ports to it. HTTP rules with concrete hosts auto-extend net_allow with the corresponding TCP entry on each http_ports value, and on 443 when http_ca is set. Wildcard hosts auto-add :80 and, with a CA, :443. All auto-added entries are TCP.
syscalls
Adjustments to Sandlock's default seccomp-bpf blocklist. The blocklist is applied unconditionally; these fields alter it.
| Python | TOML | Type | Default | Description |
|---|---|---|---|---|
extra_allow_syscalls | extra_allow | Sequence[str] | () | Syscall group names to re-allow. Groups: "sysv_ipc". Unknown groups and individual syscall names are rejected. |
extra_deny_syscalls | extra_deny | Sequence[str] | () | Additional syscall or group names to block on top of the default blocklist. Groups expand to their member syscalls. |
limits
Resource caps and visibility limits. The TOML schema drops the max_ prefix the Python names carry; the GPU and CPU placement fields keep their names.
| Python | TOML | Type | Default | Description |
|---|---|---|---|---|
max_memory | memory | str | int | None | None | Memory limit. Accepts "512M", "1G", or an integer byte count. |
max_processes | processes | int | 64 | Maximum concurrent processes (peak, not lifetime; threads do not count). Also enables the fork interception used by checkpoint freeze. |
max_open_files | open_files | int | None | None | Maximum open descriptors, enforced via RLIMIT_NOFILE. See the notes below. |
max_cpu | cpu | int | None | None | CPU throttle as a percentage of one core, 1 to 100. Applied to the whole process group by cycling SIGSTOP and SIGCONT. |
max_disk | disk | str | None | None | COW storage quota, for example "1G". Returned as ENOSPC when the upper layer exceeds it. |
gpu_devices | gpu_devices | Sequence[int] | None | None | GPU device indices to expose. None denies GPU access entirely; [] exposes every GPU; a list exposes only those devices. Adds Landlock rules for /dev/nvidia* and /dev/dri/* and sets CUDA_VISIBLE_DEVICES and ROCR_VISIBLE_DEVICES. |
cpu_cores | cpu_cores | Sequence[int] | None | None | CPU cores to pin the sandbox to via sched_setaffinity in the child. |
num_cpus | num_cpus | int | None | None | Visible CPU count in /proc/cpuinfo, renumbered 0..N-1. Also virtualizes /proc/meminfo when max_memory is set. |
Notes on max_open_files
This one has enough sharp edges to be worth spelling out.
It is a budget, not confinement. Both the soft and hard limits are lowered, and descendants inherit the cap. But the value is clamped to both limits Sandlock itself inherited, so it is an upper bound and never a grant: a request above the inherited soft limit gives the guest the inherited limit, not more. Raise the limit on Sandlock itself with prlimit or systemd's LimitNOFILE= if a guest genuinely needs a bigger budget. And lowering the hard limit is one-way only for an unprivileged Sandlock: a sandbox launched by root, or with CAP_SYS_RESOURCE, can raise it back, because Sandlock does not drop capabilities.
It must cover process startup. The limit has to accommodate stdio, the dynamic loader's per-library descriptors, and under chroot the injected exec descriptor. Too low a value fails the exec and exits 127, reporting EMFILE on a plain exec but EIO under chroot. Past startup the errno depends on who services the open: EMFILE from the kernel, EACCES when the supervisor mediates it (chroot, COW, procfs virtualization). The measured floor for a trivial command is about 4, plain exec or chroot; programs linking more libraries need more.
Runtime kwargs (Python only)
These are not part of the policy serialization and have no TOML counterpart.
| Field | Type | Default | Description |
|---|---|---|---|
name | str | None | None | Sandbox name and virtual hostname inside the sandbox. Auto-generated as sandbox-{pid} when omitted. Maximum 64 bytes; must not contain NUL. |
policy_fn | Callable | None | None | Per-event dynamic policy callback. See Dynamic Policy. |
init_fn | Callable | None | None | Callback invoked once in the template process before the COW fork. |
work_fn | Callable | None | None | Callback invoked in each COW clone; receives clone_id. |
control_socket | bool | True | Enable the per-sandbox control socket for introspection (sandlock ps, sandlock inspect). When False, no runtime directory, pid file, or control-socket task is created and the sandbox is invisible to both. no_supervisor sandboxes create one only when this is True. |
Advanced
| Field | Type | Default | Description |
|---|---|---|---|
notif_policy | NotifPolicy | None | None | Seccomp user-notification policy for /proc and /sys virtualization. Usually configured implicitly by the other fields; advanced use only. |
Protection opt-out
By default Sandlock enforces every Landlock protection the host kernel supports and refuses to start when a required protection is unavailable. Two builder methods on SandboxBuilder relax the strict default per protection:
allow_degraded(Protection::P): enforcePwhere the host kernel supports it, silently skip it where it does not. Use this when deploying across a mixed fleet of kernels.disable(Protection::P): never enforceP, even on a kernel that supports it. Use this when the workload legitimately needs the capability the protection blocks, for example signalling a sibling process thatSignalScopewould otherwise prevent.
Calling neither leaves the protection in its default Strict state. The methods are last-wins per protection.
| Protection | CLI name | Landlock ABI floor |
|---|---|---|
FsRefer | fs-refer | v2 |
FsTruncate | fs-truncate | v3 |
NetTcp | net-tcp | v4 |
FsIoctlDev | fs-ioctl-dev | v5 |
SignalScope | signal-scope | v6 |
AbstractUnixSocketScope | abstract-unix-socket-scope | v6 |
use sandlock_core::{Protection, Sandbox};
let sb = Sandbox::builder()
.fs_read("/data")
.fs_write("/tmp")
.allow_degraded(Protection::SignalScope)
.allow_degraded(Protection::AbstractUnixSocketScope)
.build()?;
The two calls let this sandbox build on kernels below 6.12, where the v6 IPC scopes are unavailable; on a kernel that supports them, the scopes remain enforced.
sandlock check reports each protection's availability against the host's Landlock ABI, and Sandbox::active_protections() returns the resolved status of a constructed sandbox: Active, Degraded, Disabled, or Unavailable. The protection policy is part of the checkpoint, so a saved sandbox restores with the exact posture it was built with.
From the CLI the equivalents are --allow-degraded <PROTECTION> and --disable <PROTECTION>, both repeatable. --disable fs-refer is rejected: the kernel denies REFER by default when the rule is unhandled, so disabling it would only tighten the sandbox and the flag would be misleading.
Kernel Protections covers this in full: what each protection stops, which kernel version provides it, how to read the resolved posture back, and what each waiver costs.
Enumerations and result types
class BranchAction(Enum):
COMMIT = "commit" # Merge branch writes into the parent branch.
ABORT = "abort" # Discard all branch writes.
KEEP = "keep" # Leave the branch as-is; caller decides.
@dataclass(frozen=True)
class Change:
kind: str # "A" = added, "M" = modified, "D" = deleted.
path: str # Path relative to workdir.
@dataclass
class DryRunResult:
success: bool
exit_code: int
stdout: bytes
stderr: bytes
changes: list[Change]
error: str | None
Helpers
from sandlock import parse_ports
parse_ports([80, "443", "8000-8005"])
# => [80, 443, 8000, 8001, 8002, 8003, 8004, 8005]
Behavioural notes
- Default-deny network.
net_allow=(), the default, denies all outbound traffic. Protocol gating is a function of rule presence: the seccomp layer denies UDP and ICMP socket creation when no rule of that protocol is configured. - Seccomp COW with
workdir. Whenworkdiris set, the COW path intercepts writes underneath it and stages them in an upper layer, committed or aborted on exit according toon_exitandon_error. - HTTP host auto-expansion. HTTP rules referencing concrete hosts auto-add corresponding TCP entries on
http_ports, and on 443 whenhttp_cais set. Wildcard hosts add the equivalent any-IP entries. All auto-added entries are TCP. - TOCTOU and
policy_fn. Path strings are never exposed on policy events, because seccomp user notification re-reads user-memory pointers after a continue verdict. Path-based control belongs in static Landlock rules, or inctx.deny_path()for runtime additions.event.argvis exposed and is TOCTOU-safe: the supervisor freezes peer tasks before exposing it.