# 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.
```bash
# macOS: strict domain-filtered proxy mode
sbe run -- cargo build
# Linux: the current kernel can only enforce the proxy destination port.
# This explicit compatibility option acknowledges that limitation.
sbe run --allow-insecure-linux-network -- cargo build
```
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.
| 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](specs/security-hardening-design.md) for the
threat model, findings, and adversarial verification plan.
## Install
```bash
cargo install sbexec --locked
```
For development from this repository:
```bash
cargo install --path apps/cli --locked --force
```
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.
```yaml
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
```bash
# Detect the ecosystem from the command or project files
sbe run -- npm install
sbe run -- cargo build
# Linux proxy compatibility (required for current networked defaults)
sbe run --allow-insecure-linux-network -- npm install
# Explicit profile
sbe run --profile python -- uv build
# Add or remove domains
sbe run --allow-domain api.example.com -- npm install
sbe run --deny-domain github.com -- npm install
# Permit a build-time downloader and its destination
sbe run --allow-fetch downloads.example.com -- cargo build
# Inspect the effective policy without executing the command
sbe inspect -- cargo build
# List built-in profiles
sbe 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:
```bash
sbe run --keep-env MY_REQUIRED_TOKEN -- cargo build
sbe run --env BUILD_MODE=release -- cargo build
```
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:
1. built-in platform/ecosystem policy;
2. `~/.config/sbe/config.yaml`;
3. an auto-discovered project `.sbe.yaml` or `.sbe.yml`;
4. an explicitly named `--config` file; and
5. 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:
```bash
sbe inspect --trust-project-config -- npm install
sbe run --trust-project-config -- npm install
```
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:
```yaml
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:
1. parses and revalidates the policy;
2. safely opens Landlock rule paths;
3. applies `PR_SET_NO_NEW_PRIVS`, Landlock, and seccomp;
4. marks ambient descriptors close-on-exec; and
5. 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
| 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:
```text
--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
```bash
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo audit
cargo deny check advisories bans licenses sources
cargo machete
```
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](LICENSE.md).
Copyright 2025–2026 Tyr Chen