sbe — Sandbox Exec
Run package managers and build tools with a kernel-enforced filesystem, process, and network policy. SBE supports macOS Seatbelt/SBPL and Linux Landlock plus seccomp.
# macOS: strict domain-filtered proxy mode
# Linux: the current kernel can only enforce the proxy destination port.
# This explicit compatibility option acknowledges that limitation.
SBE is designed for repositories, dependencies, build scripts, compiler plugins, and install hooks that may be malicious. The kernel and the installed SBE executable remain trusted.
Security boundary
SBE 0.4 fails closed when a requested guarantee cannot be enforced.
| Capability | macOS | Linux |
|---|---|---|
| Filesystem writes | SBPL allowlist | Landlock allowlist |
| Secret-path reads | SBPL deny rules | Descriptor-based read rules with denied descendants carved out |
| Executable paths | SBPL allow/deny rules | Landlock allowlist; broad privilege-bearing directories are rejected |
| Ambient environment | Cleared, then rebuilt from a small positive allowlist | Same |
| Ambient file descriptors | CLOEXEC before target exec |
close_range(CLOEXEC) with bounded fallback |
| Domain-filtered HTTPS | Enforced through the exact authenticated proxy port | Not currently enforceable; strict mode refuses to run |
| Restricted TCP/UDP | SBPL network policy | Landlock TCP plus seccomp Internet datagram/raw-socket rules |
| Same-user signals and Unix sockets | SBPL signal/network rules | Landlock ABI v6 signal and abstract-socket scopes; ABI v9 pathname-socket mediation |
| Persistent W^X | Validated before launch | Validated before launch, including existing symlink aliases |
| Private temporary storage | Per-run root; other shared temp roots denied | Per-run root; other temp paths omitted from Landlock grants |
| Violation audit stream | Reported unavailable unless a correlatable source exists | Reported unavailable until kernel-domain correlation is verifiable |
Important Linux distinction: Landlock ABI v4 can authorize a destination port, not a destination IP address. A malicious process that knows the proxy's random port can connect to a different host listening on that port. SBE therefore does not call this domain confinement. A proxy profile either:
- refuses by default; or
- runs only after
--allow-insecure-linux-network, with a warning explaining the bypass.
Linux intentionally exposes only curated public procfs nodes such as
/proc/cpuinfo; it does not grant /proc/self. A rule opened for the launcher's
/proc/self inode would not follow spawned descendants to their different proc
inodes and would create a false guarantee. On kernels before Landlock ABI v6,
same-user signal and abstract Unix-socket isolation is unavailable; pathname
Unix-socket mediation requires ABI v9.
--allow-all-network remains an explicit request to remove network isolation.
--no-proxy selects direct-TCP-443 compatibility mode and is never described
as domain-filtered. On Linux it also permits Internet datagram sockets so libc
can resolve DNS; use proxy mode when UDP egress must remain blocked.
See the security hardening design for the threat model, findings, and adversarial verification plan.
Install
For development from this repository:
Requirements:
- the current Rust stable toolchain for source builds;
- macOS with
/usr/bin/sandbox-exec; or - Linux 5.13 or newer for Landlock filesystem enforcement, with Landlock ABI v4 or newer required for TCP destination-port enforcement.
No exact Rust patch version is pinned. CI installs stable.
GitHub Actions
Release 0.4.0 and newer publishes SHA-256 checksums, an SPDX SBOM, and GitHub
build-provenance attestations. The composite action verifies both the checksum
and the attestation before installing the binary. The action defaults to the
audited 0.4.0 release; pass version: latest only when intentionally opting
into automatic release upgrades.
permissions:
contents: read
steps:
- uses: actions/checkout@<full-commit-sha>
- uses: tyrchen/sbe@sbexec-v0.4.0
with:
version: '0.4.0'
- run: sbe --version
- run: sbe run --allow-insecure-linux-network -- cargo build
For high-assurance workflows, replace the SBE release tag in uses: with the
full commit SHA belonging to that tag. Supported release targets are
x86_64-unknown-linux-musl, aarch64-unknown-linux-musl, and
aarch64-apple-darwin.
The action accepts 0.4.0, v0.4.0, or sbexec-v0.4.0 and reports the
resolved sbexec-v0.4.0 tag through its version output. Releases older than
0.4.0 are rejected because they do not provide the required checksum and
provenance artifacts.
Quick start
# Detect the ecosystem from the command or project files
# Linux proxy compatibility (required for current networked defaults)
# Explicit profile
# Add or remove domains
# Permit a build-time downloader and its destination
# Inspect the effective policy without executing the command
# List built-in profiles
Environment grants
The child does not inherit the complete parent environment. SBE keeps only a
small CLI baseline such as PATH, HOME, locale, terminal, and selected tool
home variables. Credential variables and agent sockets are absent unless the
user grants them explicitly:
Proxy, temporary-directory, and SBE-controlled build-output variables are
reserved and cannot be replaced through configuration or CLI flags. inspect
prints effective variable names and origins, but all values are redacted.
On current stable Cargo, SBE keeps final Rust artifacts in $PWD/target while
placing intermediate artifacts and executable build scripts in the private
per-run tree through CARGO_BUILD_BUILD_DIR. This preserves persistent W^X
without discarding the final build output. Commands that execute target
artifacts—including cargo test, cargo run, and cargo nextest—instead use
the private executable CARGO_TARGET_DIR.
Node dependency installation keeps node_modules writable but non-executable.
Commands that explicitly run already-installed tools, such as npm test,
npm exec, npx, and corresponding Yarn/pnpm/Bun forms, switch the built-in
dependency-tree grant to read/execute and remove its write grant for that
invocation. Modern Yarn PnP installs pre-create .pnp.cjs (and the optional
ESM loader when configured)
only when .yarnrc.yml or packageManager identifies a PnP-capable Yarn;
Classic Yarn and the node-modules linker remain lockfile-only. Mutating Bun
commands receive only their own bun.lock output, plus yarn.lock when Bun's
--yarn compatibility output is requested. SBE walks bounded, no-follow
workspace metadata up to the Git boundary, including workspace roots between
the current directory and repository root, and grants/pre-creates outputs at
exactly one active Node workspace root. npm --no-package-lock and
--package-lock=false never create an empty lockfile. Package-manager options
that relocate the project
(--prefix, --cwd, --dir, --directory, --project, and equivalent short
forms) are rejected before policy preparation; change directory before running
SBE instead.
Python installation and synchronization commands similarly keep project
.venv/venv directories writable but non-executable. Run/test commands,
including uv run, poetry run, activated entry points, and direct
.venv/bin/... paths, switch those built-in grants to read/execute without
write. This mode is for an already-installed environment; synchronize it in a
separate invocation before running tools.
Persistent outputs and package caches are still attacker-controlled data after an untrusted build. W^X prevents direct execution during that invocation; it does not make generated binaries, scripts, dynamic libraries, or interpreted packages trustworthy. Review or discard them before running them outside SBE.
Stdin, stdout, and stderr are intentional capabilities. For example,
sbe run -- tool < secret.txt explicitly gives that file's contents to the
tool even though all other inherited descriptors are closed.
Configuration trust
Configuration sources are merged in this order:
- built-in platform/ecosystem policy;
~/.config/sbe/config.yaml;- an auto-discovered project
.sbe.yamlor.sbe.yml; - an explicitly named
--configfile; and - CLI grants.
An automatically discovered project file is untrusted. By default it may add
denyRead, denyExec, or denyDomains, and may turn off a previously granted
allow-all/degradation setting. It may not add read, write, execute, network,
environment, fetch, or degradation authority.
Review a repository before granting expansion for one invocation:
Global, explicitly named, and CLI policy are trusted user choices. All schemas
reject unknown fields, oversized input, invalid environment names, malformed
IDNA domains, unsafe paths, missing bases, and cyclic extends chains.
Example trusted configuration:
profiles:
node:
allowWrite:
- "$PWD/dist/"
allowDomains:
- "api.example.com"
denyDomains:
- "github.com"
allowFetch:
- "downloads.example.com"
env:
NODE_ENV: production
Every permission-bearing resolved entry retains its built-in, global, project, explicit-file, CLI, parent-environment, or runtime origin.
Filesystem and process policy
Built-in profiles grant specific outputs and lockfiles rather than the entire
working tree. Source, .git, workflow definitions, manifests, and SBE policy
remain non-writable unless explicitly granted. Shared system temporary roots
are not writable; every invocation gets a canonical private root used for
TMPDIR, TMP, TEMP, and XDG_RUNTIME_DIR.
Persistent write and execute grants may not overlap. The only default W+X
exception is the private per-run root, which is deleted when the invocation
finishes. Toolchains such as ~/.rustup are executable/readable but not
writable. Mutable caches are readable/writable but not executable. Before
launch, SBE also rejects a writable regular file unless all of its hard-link
pathnames are contained in writable roots. This prevents a writable cache
alias from mutating executable tools, source, workflows, or any other protected
path. Hard links wholly contained in writable roots remain valid.
On Linux, paths are opened with descriptor-relative openat2 resolution using
RESOLVE_BENEATH, RESOLVE_NO_SYMLINKS, and
RESOLVE_NO_MAGICLINKS. Writable directories are created component by
component with directory FDs. Root-owned immutable distribution symlinks are
the only symlink exception. Because Landlock authorizes inodes rather than
pathnames, SBE fails closed if a denied regular file—or a file below a denied
directory—has multiple hard links.
On macOS, secret read denials include both the configured pathname and its
canonical target. A symlinked ~/.ssh, .aws, or similar protected directory
therefore cannot escape the Seatbelt deny rule through pathname resolution.
Hex 2.5.x currently extracts packages through an unpredictable tmp_*
directory in the project root and exposes no temp-directory setting. Strict
SBE therefore blocks mix deps.get rather than granting project-wide write
access. Fetch dependencies before entering SBE, then run
sbe run -- mix compile; dependency build code still executes inside the
sandbox.
Network proxy
The local CONNECT proxy:
- requires a random 256-bit per-run Basic-authentication token;
- accepts only CONNECT over HTTP/1.0 or HTTP/1.1;
- bounds request lines, individual and total headers, header count, concurrent connections, resolved addresses, and task lifetime;
- enforces total header, DNS, connect, aggregate bidirectional idle, and maximum tunnel timeouts;
- canonicalizes lowercase IDNA names and matches wildcards on label boundaries;
- permits port 443 by default;
- rejects IP literals and any resolution containing loopback, private, link-local, unspecified, multicast, documentation, or other selected special-use addresses; and
- resolves once and connects only to a validated returned address.
If the proxy exits while the sandboxed command is alive, SBE terminates the command instead of silently leaving it without the requested mediator.
JVM profiles receive SBE-owned proxy system properties and a private per-run
authentication agent. The agent reads the one-time token from a reserved
environment variable and answers authentication challenges only for SBE's
exact loopback proxy endpoint. The token is deliberately excluded from
JAVA_TOOL_OPTIONS, which the JVM prints at startup. Maven and sbt therefore
use the same domain allowlist as the other ecosystems. Gradle daemon IPC may
require additional local-service authority and is not enabled broadly by
default.
Linux launcher
Linux policy installation does not run complex Rust code in a multithreaded
pre_exec closure. The parent serializes a bounded policy into an anonymous
descriptor and starts an internal single-threaded launcher before constructing
any runtime in that process. The launcher:
- parses and revalidates the policy;
- safely opens Landlock rule paths;
- applies
PR_SET_NO_NEW_PRIVS, Landlock, and seccomp; - marks ambient descriptors close-on-exec; and
- executes the target.
A dedicated status descriptor distinguishes launcher setup failure from the target itself returning exit code 126.
Limitations
- Linux has no strict domain-egress backend yet. The explicit port-only mode is bypassable and should be combined with a network namespace, firewall, or trusted CI egress controls when the repository may be malicious.
- On Linux kernels before Landlock ABI v4, restricted TCP modes refuse unless the same insecure compatibility option is supplied; in that case TCP confinement is unavailable and SBE says so.
- Path-based Unix-socket mediation depends on newer Landlock ABIs. Local IPC is a separate capability from Internet egress.
- macOS retains an allow-most/read-deny model for compatibility. The curated secret paths, Keychain service, shared temp roots, environment, and inherited descriptors are protected, but this is not a complete home-directory read allowlist.
- Correlated kernel violation streaming is currently reported as unavailable.
Ordinary denials still surface as
EACCES,EPERM, or command failure. - SBE does not defend against kernel vulnerabilities, a compromised SBE binary, CPU/memory exhaustion by the target, timing/cache side channels, or explicit capabilities supplied through stdio.
Supported ecosystems
| Ecosystem | Commands | Project files |
|---|---|---|
| Node.js | node, npm, npx, yarn, pnpm, bun |
package.json |
| Rust | cargo, rustc, rustup |
Cargo.toml |
| Python | python, pip, uv, poetry, pdm, rye |
pyproject.toml, requirements.txt, and related files |
| Elixir | mix, elixir, iex |
mix.exs |
| Java/Scala | java, javac, mvn, gradle, sbt, scala |
Maven, Gradle, and sbt descriptors |
CLI
Run sbe run --help and sbe inspect --help for the complete current
reference. Security-relevant options include:
--allow-insecure-linux-network Explicit port-only Linux compatibility
--allow-all-network Remove network confinement
--no-proxy Direct-TCP-443 compatibility
--trust-project-config Let auto-discovered project policy add grants
--keep-env NAME Preserve one parent variable
--env NAME=VALUE Add one explicit variable
--allow-write PATH Add a writable path
--deny-read PATH Add a protected read path
--allow-exec PATH Add an executable path
--allow-fetch DOMAIN Add downloader execution and proxy destination
--dry-run Render policy without target execution
SBE passes through normal target exit codes. Its own errors use 125; sandbox setup/exec failure uses 126.
Development
The release workflow uses the Rust stable channel, pinned third-party action
SHAs, least-privilege job permissions, draft-first publication, SHA-256
checksums, SPDX SBOMs, and GitHub artifact attestations.
License
MIT. See LICENSE.md.
Copyright 2025–2026 Tyr Chen