Skip to content

DatanoiseTV/sandkasten

Repository files navigation

sandkasten

A fast, kernel-enforced application sandbox for macOS and Linux. Describe what a program may touch in TOML; sandkasten enforces it in the kernel.

profile.toml ──▶ sandkasten ──▶ fork ─▶ sandbox_init() ─▶ execve(target)
                                          │
                                          ├─ macOS: Seatbelt (MACF, kernel)
                                          └─ Linux: user+mount+pid+ipc+uts[+net] namespaces
                                                    + Landlock LSM
                                                    + seccomp-BPF
                                                    + PR_SET_NO_NEW_PRIVS
                                                    + resource limits (setrlimit)

Written in Rust. Single ~2 MB release binary. No daemon, no service, no setuid. Unprivileged — sandkasten itself never requires root.

At a glance

  • Kernel enforcement. macOS calls sandbox_init, Linux unshares namespaces and installs Landlock + seccomp. All decisions happen in the kernel; zero userspace interposition overhead after policy is applied.
  • Portable profiles. One TOML file works on both platforms. The generators pick the right primitive per OS and warn when something's unexpressible.
  • Default deny. Filesystem, network, Mach services, sysctl, IOKit, IPC — all off unless the profile opts in. Templates (strict, minimal-cli, self, dev, browser, electron, network-client) provide sane starts.
  • Privilege-elevation guardrails. process.block_privilege_elevation = true denies exec of sudo / su / doas / pkexec / runuser / visudo across macOS and Linux (incl. Homebrew, Linuxbrew, Snap, and /usr/local/bin/... installs). process.block_setid_syscalls = true seccomp-denies every setuid/setgid-family syscall on Linux so shellcode that skips the named binary can't gain creds either.
  • Interactive OR scripted learning. sandkasten learn -- <cmd> runs the target with full permissions while capturing every operation it performs, applies heuristics (subtree collapsing, sensitive-path flagging, preset detection), and interactively proposes a tight profile. Use --yes for a non-interactive mode that accepts every bucket (except sensitive paths, which always stay default-deny).
  • Honest limits. Failure modes and platform asymmetries are documented inline in the generated policy and in this README. See Limits, below.

🚀 New here? Start with the Quick Start — installs, first sandboxed command, templates at a glance, writing a profile, hardening knobs, CI/CD. This README is the full reference.

Install

Homebrew (macOS + Linuxbrew)

brew tap DatanoiseTV/sandkasten
brew install sandkasten

The formula installs from prebuilt per-arch tarballs from the GitHub release — ~2 s wall-clock, no Rust toolchain required. Shell completions for bash/zsh/fish are installed automatically on the native triples (arm64-macos, x86_64-linux).

Direct download

Each release ships tarballs for every {aarch64,x86_64}-{apple-darwin, unknown-linux-gnu} combo plus a versionless alias so generic URLs work across version bumps. Grab the one for your platform from https://github.com/DatanoiseTV/sandkasten/releases/latest or one-liner it:

# Linux x86_64 — latest release, auto-resolved server-side, no version pin:
curl -sSL https://github.com/DatanoiseTV/sandkasten/releases/latest/download/sandkasten-x86_64-unknown-linux-gnu.tar.gz \
  | tar -xz && sudo install sandkasten-*/sandkasten /usr/local/bin/

# macOS Apple Silicon:
curl -sSL https://github.com/DatanoiseTV/sandkasten/releases/latest/download/sandkasten-aarch64-apple-darwin.tar.gz \
  | tar -xz && sudo install sandkasten-*/sandkasten /usr/local/bin/

Swap the triple for aarch64-unknown-linux-gnu (Linux arm64) or x86_64-apple-darwin (Intel Macs). Pin a specific release by replacing latest/download/ with download/<tag>/ and adding the <tag>- prefix to the filename.

From source

cargo install --path .
# or
cargo build --release   # → target/release/sandkasten

Runtime dependencies: none on either platform — the prebuilt binary is statically self-contained. Linux optionally benefits from pasta (from the passt package) or slirp4netns for external network connectivity under a private netns with per-IP nftables filtering, and strace for sandkasten learn. sandkasten doctor prints distro-tailored install commands for anything missing.

60-second tour

# See what's available
sandkasten templates
sandkasten doctor

# Run /bin/cat sandboxed — only the current directory is writable
sandkasten run self -- /bin/cat README.md

# Write a tight profile interactively by observing what an app does
sandkasten learn --auto-system -o my-tool.toml -- ./my-tool --help

# Pre-flight review before running: explain in plain English
sandkasten explain my-tool.toml

# Structural diff between two profiles
sandkasten diff self dev

# Launch a Chromium-based browser in a throwaway sandbox
sandkasten run browser -- \
  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \
  --no-sandbox --password-store=basic

# Web UI for editing profiles (local, token-gated)
sandkasten ui

Example use cases

Run untrusted code from the internet

You just cloned a repo and want to npm install without letting it read ~/.ssh or exfiltrate your cloud credentials.

# untrusted.toml
name = "untrusted-npm"
extends = "self"
[filesystem]
read_write = ["${CWD}"]
[network]
allow_dns = true
presets = ["https"]         # TCP 443 outbound for registry
[process]
allow_fork = true
allow_exec = true
[env]
pass = ["PATH", "HOME", "NODE_PATH", "NPM_CONFIG_REGISTRY"]
[limits]
wall_timeout_seconds = 600   # cap install at 10 minutes
memory_mb = 4096
sandkasten run ./untrusted.toml -- npm install

~/.ssh, ~/.aws, ~/.gnupg, keychains, shell history, TCC database are all inherited-denied from the self template. The package script can't reach them even if it tries — sandbox returns EPERM.

Block re-exec through sudo / su (defense against cached creds)

# harder.toml — most hosts have NOPASSWD: ALL sudoers entries for the
# user account at some point. A compromised sandboxed tool could call
# `sudo sh -c 'curl ... | sh'` and escalate to host-root before the
# user notices. This flag denies exec of every named elevation binary
# and, on Linux, also seccomp-denies the setuid-family syscalls so
# shellcode that skips the binary still can't flip creds.
extends = "dev"
[process]
block_privilege_elevation = true   # implies block_setid_syscalls
sandkasten run harder.toml -- ./untrusted-tool
# Inside: `sudo whoami` → sandkasten: execve failed: /usr/bin/sudo errno=1
# `/usr/bin/python3 -c 'import os; os.setuid(0)'` → OSError: EPERM

Works symmetrically on macOS (Seatbelt (deny process-exec ...)) and Linux (Landlock exclusion + seccomp). The binary list covers standard /usr/bin/, Homebrew on Apple Silicon, Linuxbrew, Snap, and /usr/local/bin/... for locally compiled installs — not just the macOS paths.

Sandbox an AI coding agent (Claude Code, opencode, aider, …)

Agentic CLI tools run shell commands on your behalf — npm install, git push, pytest, gh pr create, sometimes things you didn't quite anticipate. By default they inherit your full shell environment: ~/.ssh, ~/.aws, GITHUB_TOKEN, the credential helpers behind git push, the cached sudo timestamp. A prompt-injected tool call or a compromised dependency can quietly walk off with any of those.

Wrapping the agent in sandkasten gives it exactly what it needs and no more — and because sandbox restrictions inherit through fork()

  • execve() (verified earlier in this README's Threat model section), every shell command the agent kicks off lives inside the same sandbox automatically.

A ready-made profile lives at examples/ai-agent.toml. On Homebrew installs it's already on the search path; on direct- install systems run sandkasten install-profiles --user once.

# Set the model API key in your shell (NOT cached in Keychain — the
# profile's hard-deny on ~/Library/Keychains is what stops an agent
# from walking off with creds from your other apps).
export ANTHROPIC_API_KEY="sk-ant-..."
# or:
export OPENAI_API_KEY="sk-..."

# One-off launch:
sandkasten run ai-agent -- claude
sandkasten run ai-agent -- opencode
sandkasten run ai-agent -- aider

# Or alias it so the original command name "just works":
alias claude='sandkasten run ai-agent -- claude'
alias opencode='sandkasten run ai-agent -- opencode'

If claude (or any agent that defaults to OAuth → macOS Keychain) hangs at startup with no TUI rendering, it's almost certainly the auth gate: the agent is waiting on a Keychain lookup that's denied. Set ANTHROPIC_API_KEY in the outer shell — the profile's env.pass whitelist passes it through. Run sandkasten -vvv run ai-agent -- claude to see kernel denials.

If you can't use a model API key (no key handy, OAuth-only provider, etc.), there's an opt-in variant ai-agent-keychain which permits ~/Library/Keychains so OAuth login can persist a token. The trade-off is real — a compromised agent can read every Keychain entry the user owns — so ai-agent (with ANTHROPIC_API_KEY) remains the recommended default. Run sandkasten run ai-agent-keychain -- claude instead, then on first launch complete /login once.

What the profile (extends = "minimal-cli") actually does:

  • Reads anywhere — agents legitimately grep through deps, read system headers, etc.
  • Writes only the project (${CWD}), the agent's own state directory (~/.config/claude, ~/.claude, ~/Library/Application Support/Claude, plus opencode/aider/… equivalents), ~/.cache, and $TMPDIR.
  • Hard-denies ~/.ssh, ~/.aws, ~/.gnupg, ~/.docker, ~/.kube, ~/.netrc, ~/.password-store, ~/.config/gcloud, shell history, macOS Keychains + TCC + Cookies + Mail + Messages, Linux keyrings, KeePass.
  • Outbound restricted to a curated list of model APIs (Anthropic, OpenAI, Gemini, OpenRouter, Mistral, Groq, Together, DeepSeek, Cohere, Fireworks, Azure OpenAI), GitHub, and the major package registries. On Linux this is enforced per-host via nftables inside the pasta/slirp4netns netns. On macOS Seatbelt widens specific hostnames to *:443 (a documented kernel limit); combine with [network.proxy] + mitmproxy / Squid for true semantic filtering on macOS.
  • block_privilege_elevationsudo / su / doas / pkexec / runuser / visudo are denied at exec, even if the host user has NOPASSWD: ALL or a still-cached password.
  • block_setid_syscalls — Linux seccomp denies the entire setuid family so shellcode can't drop or gain creds without going through a named elevation binary.
  • env.pass whitelisted — the agent sees its own model API keys (ANTHROPIC_API_KEY / OPENAI_API_KEY / etc.) but not GITHUB_TOKEN, AWS_*, KUBECONFIG, NPM_TOKEN, PYPI_TOKEN.
  • No [limits] block for the interactive case. Hard CPU / wall-clock / memory caps kill long agent sessions at arbitrary times, and on macOS RLIMIT_NPROC is per-real-user (not per-process), so any cap you set covers your whole logged-in session and Bun-based agents will EAGAIN on posix_spawn as soon as they fork their worker pool. If you're driving the agent from CI / batch, copy the profile and add a [limits] block tuned to that workload.

If you want stricter network posture: drop everything from outbound_tcp except the model API actually in use; the agent will fail any package install, which is often what you want.

If you want stricter filesystem posture: change read = ["/"] to a narrower list (typically ${CWD}, /usr/lib, /usr/share, /Library/Apple/System, /private/var/db/dyld) so even the agent can't read other projects on your laptop.

Sandbox a server application (HTTP server, API, database, worker)

Four bundled profiles cover the common production server shapes. All four reduce the blast radius of a code-injection or supply-chain compromise to roughly "what the listed network endpoints + writable paths allow", which is usually a much narrower set than the host the process otherwise has access to.

# HTTP / reverse proxy — bind 80/443, write only logs, optional
# outbound to upstream backends (edit examples/web-server.toml).
sandkasten run web-server -- /usr/sbin/nginx -g "daemon off;"
sandkasten run web-server -- /usr/local/bin/caddy run

# Application API — bind one port, strict outbound to DB + upstream
# APIs only, no exec by default (no shelling out for ImageMagick /
# ffmpeg / git unless you opt in).
sandkasten run api-server -- node /srv/api/dist/server.js
sandkasten run api-server -- gunicorn -b 0.0.0.0:8000 myapp.wsgi:app

# Database daemon — bind one port, NO outbound, write only the data
# dir + WAL + log dir. memory_mb sized for the buffer pool, not a
# generic small number.
sandkasten run database -- /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/main

# Background worker / queue consumer — no inbound, narrow outbound
# to broker + DB + APIs.
sandkasten run worker -- bundle exec sidekiq -q default
sandkasten run worker -- celery -A myapp worker -l info

What's locked down across all four:

  • No cpu_seconds, no wall_timeout_seconds — daemons run forever; both rlimits are footguns when set ("0" is "kill now", not "unlimited").
  • block_privilege_elevation + block_setid_syscalls — even a fully-RCE'd process can't sudo or call setuid() to gain another user's permissions.
  • allow_exec = false by default — no shelling out. A SQL injection that pivots to RCE can't exec bash, nc, wget, curl. Flip per-profile when your app legitimately invokes helpers (CGI, ImageMagick, ffmpeg).
  • no_w_x = true for AOT engines (nginx/caddy/Postgres/Redis), off for JIT runtimes (Node/Bun/JVM/V8). The profile sets the right default for its expected workload.
  • env.pass whitelist — the app gets DATABASE_URL and STRIPE_API_KEY, NOT AWS_*, KUBECONFIG, GITHUB_TOKEN. A log line of process.env can only spill what's whitelisted.

Profiles to copy and adapt: examples/web-server.toml, examples/api-server.toml, examples/database.toml, examples/worker.toml. Each has the upstream/inbound list commented as a starting point — edit the entries to match your topology before deploying.

Sandbox a Chromium-family browser for a one-off session

sandkasten run browser -- \
  "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \
  --no-sandbox --password-store=basic

The browser template grants a broad FS read (so rendering, extensions, file pickers work), narrow writes (only caches, preferences, Downloads, Desktop, Documents), every Mach service the browser needs, and hard denies Keychains, SSH keys, cookies, shell history, Mail/Messages stores, and other browsers' profile directories.

--no-sandbox disables Chromium's own per-renderer-process sandbox. On macOS this is currently required under sandkasten (without it, Chromium fails to initialise with "sandbox initialization failed: Operation not permitted" — our outer Seatbelt blocks the MAC-policy registration calls and helper-process Mach IPC Chromium needs to nest its own sandbox inside ours).

That's a real trade-off worth understanding: Chromium's inner sandbox normally isolates renderers from each other (one tab can't read another tab's memory or files), and we lose that. A malicious site's renderer gets whatever FS scope our profile grants the parent process — by default that's a broad read so rendering and file pickers work. Treat the browser profile as protection from "what the browser process accidentally pokes at" (Keychains, SSH keys, cookies, shell history, other browsers' profiles), NOT as a replacement for Chromium's per-tab isolation.

If your threat model needs per-tab isolation, use a separate macOS user account or a VM; sandkasten's outer Seatbelt-on-Chromium story is a coarser, all-or-nothing layer.

--password-store=basic silences the "Encryption is not available" warning that appears when the browser can't reach the keychain (because we intentionally denied it).

Jail SSH logins

In /etc/ssh/sshd_config:

Match User sandboxed
    ForceCommand /usr/local/bin/sandkasten sshd dev

Every interactive login by sandboxed runs $SHELL -l under the dev profile. ssh sandboxed@host 'some command' runs the command through /bin/sh -c under the same sandbox — $SSH_ORIGINAL_COMMAND is picked up by sandkasten sshd.

Hook an app that dials hard-coded IPs onto a local service (Linux)

The app pings 1.2.3.4:443 and you want it to hit your local development server without modifying the binary:

[[network.redirects]]
from = "1.2.3.4:443"
to   = "127.0.0.1:8443"
protocol = "tcp"

[network]
allow_localhost = true

Applied via nftables DNAT inside the sandbox's private netns. The host's network stack is untouched. For hostname-based apps, prefer [network.hosts_entries] — it works cross-platform and survives TLS SNI.

Route sandboxed traffic through a VPN (Linux)

sandkasten can join an existing network namespace instead of creating its own. If you've set up WireGuard (or OpenVPN, or any tunnel) in a named netns, point the profile at it and every byte the sandbox sends rides the tunnel:

# one-off setup (root, host)
ip netns add vpn
ip link add wg0 type wireguard
ip link set wg0 netns vpn
ip netns exec vpn wg setconf wg0 /etc/wireguard/wg0.conf
ip netns exec vpn ip addr add 10.0.0.2/24 dev wg0
ip netns exec vpn ip link set wg0 up
ip netns exec vpn ip route add default dev wg0
# profile.toml
[network]
netns_path = "/run/netns/vpn"
allow_dns = true
outbound_tcp = ["*:443"]
sandkasten run profile.toml -- curl https://ifconfig.me
# → reports the VPN endpoint's IP, not yours

Sandbox applies as usual on top — Landlock, seccomp, resource limits — but the kernel routes connect() through the VPN. No LD_PRELOAD, no userspace proxy. Per-IP nftables rules inside this netns still work.

Controlled hardware identity for testing

A compatibility test suite wants to see a specific CPU, machine-id, DMI serial, and kernel version:

[spoof]
cpu_count       = 4                 # sched_setaffinity pins to 4 cores
cpuinfo_synth   = true
cpuinfo_model   = "Intel(R) Xeon(R) E5-2697 v4 @ 2.30GHz"
hostname        = "test-rig-07"
machine_id      = "deadbeefcafebabe0123456789abcdef"
kernel_version  = "Linux version 6.12.0-stable #1 SMP"
kernel_release  = "6.12.0-stable"
os_release      = """
NAME="FleetOS"
VERSION="2025.10"
ID=fleetos
"""

[spoof.dmi]
product_serial = "FLEET-00042"
sys_vendor     = "AcmeCo"
board_name     = "Fleetboard R7"

[[spoof.files]]
path = "/sys/class/net/lo/address"
content = "00:de:ad:be:ef:01"

Verified: nproc returns 4, /etc/machine-id reads the spoofed value, /proc/cpuinfo shows "Sandkasten CPU" (or your override), host files untouched. See Limits for what the kernel syscall uname will and won't let us spoof.

USB / libusb in a sandbox

[hardware]
usb    = true
serial = true   # also /dev/ttyUSB* /dev/ttyACM*

Linux: grants read+write on /dev/bus/usb and read on the udev bits libusb consults. macOS: grants IOKit + the USB driver family Mach services.

Camera / video-device control

[hardware]
camera = true             # V4L2 (Linux) / AVFoundation (macOS)
screen_capture = true     # PipeWire screencast (Linux) / ScreenCaptureKit (macOS)

[hardware.video]
# Only /dev/video0 is visible; every other /dev/video*, /dev/media*,
# /dev/v4l-subdev* is hidden via an empty bind-mount so enumeration
# returns nothing rather than EPERM.
devices = ["/dev/video0"]

# Redirect: inside the sandbox /dev/video0 actually resolves to the
# host's /dev/video5. Useful for v4l2loopback pipes (feed a fake camera
# stream from a file or another process into /dev/video5, the sandbox
# sees /dev/video0).
redirect = { "/dev/video0" = "/dev/video5" }

Linux implements both via the same mount-namespace bind-mount primitive used by DNS overrides; see [[filesystem.rewire]] / [[filesystem.hide]] if you want the raw form. macOS uses the CoreMediaIO + ScreenCaptureKit Mach services — AVFoundation doesn't route through device nodes, so the allowlist/redirect is Linux-only there (documented in the emitted policy).

Isolated packet capture / port scanning

extends = "minimal-cli"
[network]
presets = ["nmap"]            # allow_raw_sockets + ICMP + DNS
allow_localhost = true

Inside a private netns with CAP_NET_RAW you can run tcpdump or nmap against loopback or any veth you've plumbed in, without that activity being visible on the host's interfaces.

Isolate a CI/CD step (GitHub Actions example)

Dependency installs (npm install, pip install, cargo fetch, …), untrusted PR test code, and build steps that execute scripts from third-party packages are the classic supply-chain attack surface on a CI runner. Wrapping them in sandkasten keeps them off the runner's credentials, the tokens in ~/.aws / ~/.docker, and the rest of the workspace.

# .github/workflows/sandboxed-install.yml
name: sandboxed-install
on: [push]

jobs:
  build:
    runs-on: ubuntu-22.04    # 24.04 ships an AppArmor profile that
                             # blocks unprivileged userns — either use
                             # 22.04, or add `sudo aa-teardown`.
    steps:
      - uses: actions/checkout@v6

      - name: Install sandkasten (prebuilt binary, ~2s — tracks latest)
        run: |
          # Versionless alias resolved server-side → this step stays
          # green across version bumps with no CI edits. Pin a
          # specific release by swapping `latest/download/` for
          # `download/v0.4.0/` and prefixing the filename with the
          # tag, if you want reproducible runs.
          curl -sSL \
            https://github.com/DatanoiseTV/sandkasten/releases/latest/download/sandkasten-x86_64-unknown-linux-gnu.tar.gz \
            | tar -xz
          sudo install sandkasten-*/sandkasten /usr/local/bin/
          # slirp4netns → real outbound + per-IP nftables filtering
          # inside the sandbox. Without it, network-client falls back
          # to host netns (still works, just loses per-IP enforcement).
          sudo apt-get update -qq && sudo apt-get install -y -qq slirp4netns

      - name: `npm install` under a hardened sandbox
        run: |
          # Fresh profile in the workspace dir — no access to host HOME,
          # no ~/.ssh / ~/.aws / ~/.npmrc leakage, only outbound to the
          # npm registry.
          cat > ci.toml <<'EOF'
          name = "ci-npm"
          extends = "network-client"
          [filesystem]
          read_write = [ "${CWD}" ]
          [network]
          outbound_tcp = [
            "*:443",             # registry.npmjs.org et al.
          ]
          [process]
          block_privilege_elevation = true
          block_setid_syscalls      = true
          no_w_x                    = true   # Linux 6.3+; safe for npm
          EOF
          sandkasten run ci.toml -- npm ci --no-audit --no-fund

      - name: Run tests under the same profile
        run: sandkasten run ci.toml -- npm test

What this gives you on a standard GitHub hosted runner:

  • package.json post-install scripts can't reach ~/.npmrc / ~/.aws / the GITHUB_TOKEN env var the runner auto-exports (it's not in the profile's env.pass).
  • Outbound is restricted to TCP 443 — a compromised install can't exfiltrate to curl http://attacker:8080/ or SSH tunnel out.
  • process.block_privilege_elevation neuters sudo even if the runner has a passwordless sudoers entry (GitHub's does).
  • no_w_x blocks the classic "write shellcode into an RW page, mprotect it executable, jump to it" pattern.

Self-hosted runners get the same guarantees plus full per-IP outbound filtering (pasta or slirp4netns plumbs the private netns). On hosted runners the network-client base falls back to host netns when pasta/slirp4netns isn't installed — network is still reachable, but per-IP filtering isn't kernel-enforced.

Isolate a CI/CD step (GitLab CI example)

sandboxed-tests:
  image: ubuntu:22.04
  before_script:
    - apt-get update -qq && apt-get install -y -qq curl slirp4netns ca-certificates
    # Versionless alias auto-resolved to the current release — no
    # pipeline bumps needed when sandkasten updates.
    - curl -sSL https://github.com/DatanoiseTV/sandkasten/releases/latest/download/sandkasten-x86_64-unknown-linux-gnu.tar.gz | tar -xz
    - install sandkasten-*/sandkasten /usr/local/bin/
  script:
    - |
      cat > ci.toml <<'EOF'
      extends = "network-client"
      [filesystem]
      read_write = [ "${CWD}" ]
      [process]
      block_privilege_elevation = true
      block_setid_syscalls      = true
      EOF
    - sandkasten run ci.toml -- ./run-untrusted-tests.sh

Note on GitLab/self-hosted runners: the unprivileged_userns_clone sysctl must be set to 1 (default on most recent distros). sandkasten doctor reports the value and the distro-specific one-liner to enable it.

Drop a throwaway overlay for ephemeral experiments (Linux)

[overlay]
lower = "/opt/bigapp"                          # read-only base
upper = "~/.sandkasten/overlay/bigapp"         # writes land here
# mount = "/opt/bigapp"  ← default, in-place

[workspace]
path  = "~/.sandkasten/work/bigapp"
chdir = true

Writes to /opt/bigapp/* don't touch the real base — they land in upper. Snapshot any time:

sandkasten snap save bigapp before-experiment
# ... do dangerous things inside the sandbox ...
sandkasten snap load bigapp before-experiment  # instant rewind
sandkasten snap list bigapp

Previous state is moved aside to <upper>.bak-<ts> — nothing is ever deleted silently.

HTTP method / URL filtering, header rewrites

sandkasten's enforcement is L3/L4 — the kernel sees addresses and ports, not HTTP. For L7 rules (block DELETE, rewrite the Host header, add X-Forwarded-For, return a synthetic 403 on /api/admin/*) pair sandkasten with a userland proxy. Pattern:

[network]
allow_dns = true

[network.proxy]
url    = "http://127.0.0.1:8080"     # your mitmproxy / squid / caddy
bypass = ["127.0.0.1", "localhost"]
# restrict_outbound = true           # default — sandbox can ONLY talk
                                     # to the proxy + bypass hosts

With restrict_outbound on, outbound_tcp is auto-narrowed to just the proxy's host:port plus each bypass entry. HTTP_PROXY / HTTPS_PROXY / ALL_PROXY / NO_PROXY (and their lowercase forms) are set in the sandbox's env. Every URL library the app uses — curl, libcurl, Go's net/http, Python's requests, Node's http — honours those env vars.

Then on the proxy side (example mitmproxy addon):

# save as rewrite.py; run: mitmproxy -s rewrite.py --listen-port 8080
from mitmproxy import http

class Rewrite:
    def request(self, flow: http.HTTPFlow) -> None:
        # Block dangerous HTTP verbs.
        if flow.request.method in ("DELETE", "PUT"):
            flow.response = http.Response.make(403, b"blocked by sandkasten+mitmproxy")
            return
        # Rewrite Host + add X-Forwarded-For.
        if "api.prod.example.com" in flow.request.pretty_host:
            flow.request.host = "api.staging.example.com"
        flow.request.headers["X-Forwarded-For"] = "10.0.0.1"

addons = [Rewrite()]

The kernel sandbox guarantees the app can't route around the proxy; the proxy enforces the application-layer policy.

Command reference

sandkasten run <profile> [--timeout 30s] [--verify] [-C <cwd>] -- <cmd> [args...]
sandkasten shell <profile>                 # interactive sandboxed shell, $SANDKASTEN_PROFILE set
sandkasten sshd <profile>                  # for sshd ForceCommand — see Use cases
sandkasten init [--template <name>] [-o <path>]
sandkasten install-profiles [--system|--user] [--force] [-s <src-dir>]
sandkasten learn [--base <tpl>] [-o <out.toml>] [--auto-system] [--yes|-y] -- <cmd>
sandkasten check <profile>                 # validate without running
sandkasten render <profile>                # print generated policy (+ policy-hash trailer)
sandkasten explain <profile>               # plain-English summary
sandkasten diff <profile> <profile>        # structural diff between two profiles
sandkasten verify <profile>                # minisign signature check
sandkasten snap save|load|list <profile> <name>   # overlay upperdir snapshots
sandkasten list                            # user profiles + built-in templates
sandkasten templates                       # built-in templates + descriptions
sandkasten doctor                          # environment / dependency check
sandkasten ui [--port 4173]                # local web UI

Verbosity: default is silent, -v adds lifecycle, -vv adds a compact rule summary, -vvv adds the full generated policy plus post-run kernel denial capture (macOS).

Profile resolution

When you write sandkasten run <name> -- …, sandkasten resolves <name> against this search order, first hit wins:

  1. An explicit path<name> contains / or ends in .toml, read literally.
  2. ./<name>.toml — current working directory.
  3. User profile dir$XDG_CONFIG_HOME/sandkasten/profiles/ on Linux, ~/Library/Application Support/sandkasten/profiles/ on macOS.
  4. System profile dirs, in order:
    • /etc/sandkasten/profiles/ (admin overrides; Linux convention)
    • /Library/Application Support/sandkasten/profiles/ (admin overrides; macOS convention)
    • /opt/homebrew/share/sandkasten/profiles/ (Homebrew on Apple Silicon)
    • /usr/local/share/sandkasten/profiles/ (Homebrew on Intel, hand-built make install)
    • /home/linuxbrew/.linuxbrew/share/sandkasten/profiles/ (Linuxbrew)
    • /usr/share/sandkasten/profiles/ (Linux distro packaging)

Earlier entries shadow later ones, so a per-user copy wins over a system one and an /etc override wins over a Homebrew-shipped default. sandkasten list enumerates everything visible from the current process's view.

brew install sandkasten drops the bundled example profiles (currently just ai-agent.toml) into <HOMEBREW_PREFIX>/share/sandkasten/profiles/ so sandkasten run ai-agent -- claude works out of the box.

For non-Homebrew installs, drop bundled profiles in by hand:

sandkasten install-profiles            # writes to user dir, no sudo needed
sudo sandkasten install-profiles --system   # writes to /etc or /Library
sandkasten install-profiles -s ./my-org/profiles --user  # add a custom dir

Profile schema

A profile is TOML. Everything is optional. extends inherits from a built-in template; list-valued fields concatenate, scalars prefer the child, and path variables (${CWD}, ${HOME}, ${EXE_DIR}, ~, any env var) are expanded at run time. To narrow an inherited field (replace it with the child's value rather than union with the parent), list its dotted path under top-level clear:

extends = "browser"
clear   = [
    "network.outbound_tcp",   # throw out parent's wide outbound list
    "network.allow_dns",      # parent set true → child can now turn it off
]

[network]
allow_dns    = false
outbound_tcp = []             # actually empty, not unioned with parent

Without clear, a child can only widen — never narrow — its parent. Unknown paths in clear are a load-time error so typos don't silently no-op a security tightening.

name        = "my-profile"
description = "What this profile is for"
extends     = "self"

# ── FILESYSTEM ──────────────────────────────────────────────────────────
[filesystem]
allow_metadata_read = true
read             = ["/usr/lib", "/System"]
read_write       = ["${CWD}", "/tmp"]
read_files       = ["/etc/hosts"]
read_write_files = ["/dev/null", "/dev/tty"]
deny             = ["${HOME}/.ssh"]
hide             = ["/etc/shadow"]      # Linux: tmpfs/dev-null bind-mount
                                         # macOS: emits SBPL deny

# Fine-grained ops per path. Tokens: read, write, create, delete, rename,
# chmod, chown, xattr, ioctl, exec, all, write-all.
[[filesystem.rules]]
path    = "${CWD}/important.log"
literal = true
allow   = ["read", "write"]
deny    = ["delete", "chmod"]

# Linux: symbolic-path substitution via bind-mount in the mount namespace.
[[filesystem.rewire]]
from = "/etc/resolv.conf"
to   = "${CWD}/my-resolv.conf"

# ── NETWORK ─────────────────────────────────────────────────────────────
[network]
allow_localhost    = true
allow_dns          = true
allow_inbound      = false
allow_icmp         = false
allow_icmpv6       = false
allow_sctp         = false
allow_dccp         = false
allow_udplite      = false
allow_raw_sockets  = false     # AF_INET/SOCK_RAW — packet-crafting
allow_unix_sockets = true      # AF_UNIX — Chromium/Electron/docker need this
outbound_tcp       = ["*:443", "example.com:8080", "10.0.0.5:22"]
outbound_udp       = []
inbound_tcp        = []
inbound_udp        = []
extra_protocols    = []        # additional `meta l4proto X` on Linux
presets            = ["https", "ssh", "postgres"]   # see table below

[network.dns]
servers = ["1.1.1.1", "9.9.9.9"]
search  = ["corp.internal"]
options = ["edns0", "rotate"]

[network.hosts_entries]
"api.test.lan" = "127.0.0.1"

# Linux-only DNAT
[[network.redirects]]
from = "1.2.3.4:443"
to   = "127.0.0.1:8443"
protocol = "tcp"

# Outbound blocks. Linux: nftables REJECT. macOS: SBPL deny (Seatbelt
# grammar widens specific hosts to `*:PORT` — documented in the render).
[[network.blocks]]
host = "tracking.example.com"
port = "*"

# ── PROCESS / SYSTEM / ENV ──────────────────────────────────────────────
[process]
allow_fork                 = true
allow_exec                 = true
allow_signal_self          = true
# Block exec of sudo/su/doas/pkexec/runuser/visudo/sudoedit from inside
# the sandbox. Useful when the host user has `NOPASSWD: ALL` sudoers or
# cached credentials — without this, a compromised tool inside the
# sandbox could re-exec through sudo and escape back to host-root. The
# binary list covers the standard *nix paths (`/usr/bin/sudo`,
# `/usr/sbin/visudo`, `/usr/libexec/doas`, …) and the common extras:
# Homebrew (macOS), Linuxbrew and Snap (Linux), and `/usr/local/bin/…`
# for locally-compiled installs. Implies `block_setid_syscalls`.
block_privilege_elevation  = false
# Block the setuid-family syscalls (setuid/setgid/setreuid/setregid/
# setresuid/setresgid/setfsuid/setfsgid/setgroups) via seccomp on Linux.
# Defense against shellcode that tries to change credentials directly
# without invoking a named elevation binary. Linux-only; macOS is
# already prevented from honouring setuid bits inside the sandbox at
# the kernel MAC layer.
block_setid_syscalls       = false
# Memory W^X: forbid mprotect(..., PROT_EXEC) on any page that was
# ever writable (Linux 6.3+, PR_SET_MDWE). Blocks the entire "write
# shellcode, flip to executable, jump to it" exploit class. Breaks
# JITs (V8, LuaJIT, Java HotSpot, PHP JIT, ...) — opt-in.
no_w_x                     = false
# Force-disable indirect branch speculation (Spectre v2) and
# speculative store bypass (Spectre v4 / SSBD) for the sandboxed
# process via PR_SET_SPECULATION_CTRL. Mitigates speculative side
# channels reachable from inside the sandbox. Costs ~2-5% CPU. Opt-in.
mitigate_spectre           = false

[system]
allow_sysctl_read = true
allow_iokit       = false
allow_ipc         = false
allow_mach_all    = false     # macOS: broad; needed by browsers/Electron
mach_services     = ["com.apple.system.logger"]

[env]
pass_all = false
pass     = ["PATH", "HOME", "LANG"]
set      = { }                # { KEY = "value" } to override

# ── RESOURCE LIMITS (POSIX setrlimit + wall-clock watchdog) ─────────────
[limits]
cpu_seconds          = 60
memory_mb            = 1024
file_size_mb         = 100
open_files           = 512
processes            = 64
stack_mb             = 8
core_dumps           = false
wall_timeout_seconds = 300

# ── HARDWARE ACCESS ─────────────────────────────────────────────────────
[hardware]
usb    = true     # /dev/bus/usb + udev (Linux) / USB Mach services (macOS)
serial = true     # /dev/tty* nodes
audio  = true     # ALSA / PulseAudio (Linux), CoreAudio (macOS)
gpu    = true     # /dev/dri (Linux), Metal (macOS)
camera = true     # V4L2 (Linux), AVFoundation (macOS)

# ── IDENTITY SPOOFING (Linux fully, macOS limited) ──────────────────────
[spoof]
cpu_count        = 4
cpuinfo_synth    = true
cpuinfo_model    = "CustomCPU 2.0"
cpuinfo_mhz      = 3200
hostname         = "rig-42"
machine_id       = "deadbeefcafe1234deadbeefcafe5678"
kernel_version   = "Linux version 6.12.0-stable #1 SMP"
kernel_release   = "6.12.0-stable"
os_release       = """NAME="FleetOS"\nVERSION="2025.10"\nID=fleetos\n"""
issue            = "Welcome to FleetOS\n"
hostid_hex       = "deadbeef"
timezone         = "Etc/UTC"
efi_platform_size = 64
efi_enabled       = false   # hide /sys/firmware/efi entirely
temperature_c     = 42      # bind-mount millicelsius over all thermal/hwmon temps

[spoof.dmi]
product_serial = "ABC123"
sys_vendor     = "AcmeCo"
board_name     = "Fleetboard R7"

[[spoof.files]]
path    = "/sys/class/net/lo/address"
content = "00:de:ad:be:ef:01"

# ── OVERLAY / WORKSPACE / MOCKS ─────────────────────────────────────────
[workspace]
path  = "~/.sandkasten/work/${NAME}"   # auto-created, added to rw,
                                        # exposed as $SANDKASTEN_WORKSPACE
chdir = true

[overlay]                # Linux kernel ≥5.11 (unprivileged overlayfs)
lower = "/opt/myapp"
upper = "~/.sandkasten/overlay/myapp"
# mount = "/opt/myapp"   ← default

[mocks]                  # v1: content sidecar via $SANDKASTEN_MOCKS
files = { "config.json" = '{"api":"local"}' }

Built-in templates

template what it gives you
self Default. Read across /, read+write only ${CWD}, hard-deny secrets
strict Near-zero permissions — minimal base every dynamically-linked binary needs
minimal-cli strict + /usr/bin /bin /sbin /usr/local /opt + CWD readable
network-client minimal-cli + outbound TCP 80/443 + DNS + $TMPDIR + /var/run/resolv.conf.
dev Permissive. Read /, write CWD/TMP, HTTPS/SSH/DNS + localhost. Denies user secrets.
browser Chromium-family browsers (macOS + Linux). Pair with --no-sandbox.
electron Electron apps (VS Code, Slack, Discord, Obsidian, …). Grants write on ~/Library/Application Support (macOS).

Network presets

Named protocol/service bundles. Expand into concrete TCP/UDP outbound rules at profile-load time.

group presets
Web http, https, quic, web
Realtime rtp, sip, stun, webrtc
VPN wireguard, wireguard-all-udp, openvpn, tailscale, ipsec
Remote ssh, rdp, vnc
Mail smtp, smtps, imap, imaps, pop3, pop3s
Files ftp, ftps, sftp, git
Auth ldap, ldaps, kerberos
Databases mysql, postgres, redis, memcached, mongodb, cassandra, elastic
Chat irc, ircs, xmpp, matrix, mqtt, mqtts
Time ntp, mdns, dhcp, dns
Games minecraft, minecraft-bedrock, steam, source-engine, quake3, teamspeak, discord-voice, riot-games
Diag ping, tcpdump, pcap, wireshark, nmap

Web UI

sandkasten ui
╭─ sandkasten UI ─────────────────────────────────────────
│  http://127.0.0.1:46513/?t=<random-token>
│  profiles directory: ~/.config/sandkasten/profiles
│  Ctrl-C to stop.
╰─────────────────────────────────────────────────────────

Binds only to 127.0.0.1. 128-bit random bearer token required on every /api/* request. Mutating requests (PUT/DELETE) additionally require the Origin header to match the bound host — belt-and-braces CSRF guard on top of the token. Body size capped at 64 KB; path names restricted to [a-zA-Z0-9_-]+; writes confined to ~/.config/sandkasten/profiles/. Tight CSP, X-Frame-Options: DENY, no-sniff, no-referrer, Permissions-Policy disabling camera/mic/geo.

Features: structured form per profile section, TOML tab for raw edit, fine-grained rule editor, client-side validation (paths, endpoints, env names, Mach services), duplicate / save-as flow for built-in templates, non-system modal dialogs, toast notifications.

No run endpoint. The UI edits profiles only — you launch them from your shell. Keeps the attack surface small.

Profile signing

sandkasten verifies minisign ed25519 signatures — same format as Jedisct1's minisign CLI (brew install minisign, apt install minisign).

⚠️ Generate the key pair OUTSIDE any git working tree and keep the private file (sandkasten.key) on disk only — never commit it. The repo's .gitignore denies *.key / *.sec / *.pem / *.priv by pattern as a backstop, but the right habit is to put the key in ~/.config/sandkasten/private/ (mode 0600) and only ever copy the *.pub half into source control if you publish trusted-key bundles.

# One-off key pair, generated in your home dir (NOT inside the repo):
mkdir -p ~/.config/sandkasten/private && chmod 700 ~/.config/sandkasten/private
minisign -G -p ~/.config/sandkasten/private/sandkasten.pub \
            -s ~/.config/sandkasten/private/sandkasten.key

# Sign a profile (output: my.toml.minisig):
minisign -Sm my.toml -s ~/.config/sandkasten/private/sandkasten.key

# Install the public key as a trusted verifier:
mkdir -p ~/.config/sandkasten/trusted_keys
cp ~/.config/sandkasten/private/sandkasten.pub ~/.config/sandkasten/trusted_keys/

sandkasten verify my.toml
# → ok: my.toml verified against ~/.config/sandkasten/trusted_keys/sandkasten.pub

sandkasten run --verify my.toml -- my-cmd
# refuses to launch if the signature doesn't validate

Built-in templates ship inside the signed binary — they skip --verify.

Verifying the sandkasten binary itself

Distinct from profile signing above: every release ships sigstore keyless signatures (*.sig + *.cert.pem), GitHub build provenance (SLSA), and a CycloneDX SBOM alongside the SHA-256 hashes. See SIGNING.md for the full verification recipe and what each layer actually proves.

Security model

What sandkasten enforces

layer macOS Linux
Filesystem Seatbelt / MACF (kernel) Landlock LSM (5.13+) + mount-ns bind-mounts
Network (L4) Seatbelt network-outbound/inbound private netns (unshare) + nftables in-netns
Mach services mach-lookup predicate — (not applicable)
Syscalls seccomp-BPF deny-list
Process fork inherits sandbox user+pid+ipc+uts namespaces
Privilege inherited PR_SET_NO_NEW_PRIVS, PR_SET_DUMPABLE=0
Resources setrlimit setrlimit

Threat model — what it's for

  • Untrusted code (from strangers, the internet, third-party build scripts, CI jobs) running as your user.
  • Over-eager tools — build systems, package managers, test runners that might glob-delete or exfiltrate by accident.
  • Credential hygiene. Templates default-deny ~/.ssh, ~/.aws, ~/.gnupg, ~/.docker, ~/.kube, ~/.netrc, ~/.password-store, macOS Keychains, the TCC database, shell history, mail, messages, cookies, other browsers' profile dirs.

Threat model — what it is not for

sandkasten is kernel-enforced process isolation built on primitives the OS already ships. It is not a virtual machine, a hypervisor, or a hardware isolation layer. Concretely out-of-scope:

  • Kernel exploits. Anything that breaks out of MACF / Landlock / seccomp bypasses us too. If an attacker reaches a kernel bug through an allowed syscall surface, the sandbox ends at that point. Enabling process.no_w_x + process.mitigate_spectre + block_privilege_elevation shrinks the reachable surface but doesn't close it.
  • Root escalation. If the target finds a way to host-root, the sandbox ends. PR_SET_NO_NEW_PRIVS + capability bounding-set drop
    • seccomp block of setuid family (via block_setid_syscalls) rule out the usual suspects; novel kernel vulns are not in scope.
  • Side-channel leakage. Timing / power / cache-based covert channels, transient-execution attacks (Spectre family, Meltdown, L1TF, MDS, Downfall, GhostRace). process.mitigate_spectre turns on the kernel's process-local mitigations for v2 + SSBD; everything else is a host-level OS or firmware concern.
  • Rowhammer / memory-fault injection. Hardware-level bit flips are orthogonal to any process sandbox. Mitigation is a BIOS / memory- controller / DIMM problem.
  • Covert channels over allowed outbound. A profile that grants outbound HTTPS permits DNS tunnelling, OCSP-stuffing, TLS-SNI signalling, and every other "legitimate connection with side data" trick. The sandbox enforces destinations and ports, not semantic intent. Use [network.proxy] + an L7-filtering mitmproxy or Squid if you need HTTP-method / URL / header filtering.
  • Resource-exhaustion attacks against the host. [limits] caps CPU-seconds, memory, file-size, open files, processes, stack, and wall-clock for the sandboxed process tree — but a profile that doesn't set them defaults to OS-wide RLIMIT. Fork bombs, disk-fill via /tmp, and ptrace-storms can still DOS the host if the profile doesn't set limits.processes / limits.file_size_mb / similar.
  • TOCTOU windows on path-based rules. macOS SBPL and Linux Landlock both resolve paths at access time — an attacker who wins a race between "sandkasten built the ruleset" and "target opens the path" can exploit symlink swaps for files outside the sandbox's view. We mitigate by opening Landlock PathFds before fork and by blocking hardlink/symlink creation via seccomp; we don't eliminate the class.
  • Landlock "deny-inside-allow" enforcement. Landlock is allow-list only: a deny path that sits inside an enclosing read / read_write subtree can't be enforced on Linux. macOS SBPL supports true deny-overrides. sandkasten run warns at -v when a deny is unenforceable.
  • Airtight hardware-identity hiding. [spoof] replaces user-space views of /proc, /sys, /etc/* — it does not patch the CPUID instruction, uname(2) syscall fields the kernel fills, _SC_NPROCESSORS_ONLN (which reads /sys/devices/system/cpu/online unless num_cpus-style libraries honour affinity, which most do), or userland that reads /dev/kmsg. It's a faithful view for most tools; it's not a VM.
  • Compromise of the build chain that produced the sandkasten binary itself. Supply-chain hardening (SBOM, SLSA provenance, signed releases) covers the tarballs we publish; users who build from source inherit the integrity of their toolchain and crate cache. cosign verify-blob against the public key in SIGNING.md proves authenticity of a downloaded release artifact.
  • Correctness of the GUI / Web UI profile editors. The structured editors in swift-ui/ and src/ui/ emit TOML that's then parsed by the normal config loader — they can emit policies that don't match user intent if there's a UI bug. Always confirm with sandkasten render + sandkasten explain before trusting a profile generated interactively.

Anti-breakout measures

  • PR_SET_NO_NEW_PRIVS blocks setuid-elevation from within the sandbox.
  • PR_SET_DUMPABLE=0 disables core dumps (no memory spill on crash) and makes the process non-ptrace-attachable from peers.
  • Seccomp deny-list includes link/linkat/symlink/symlinkat (hardlink-into-writable-area escape), name_to_handle_at / open_by_handle_at (reopen via handle across mount ns), io_uring_* (high-churn attack surface), userfaultfd, clock-manipulation syscalls, kernel-admin syscalls (mount / pivot_root / chroot / unshare / setns / reboot / module ops), ptrace and process-memory introspection, keyctl / add_key / request_key, perf_event_open, bpf, NUMA memory-move primitives.
  • Landlock writes are path-based; hardlink creation is blocked so an attacker can't pull a denied file into the writable area.

Limits

Shipped honestly — nothing hidden.

  1. macOS sandbox_init is SPI. Undocumented by Apple but stable in practice — the mechanism every sandboxed macOS browser uses.
  2. Modern macOS Seatbelt grammar rejects IP literals and specific hostnames in remote tcp/udp — only localhost and * are accepted. sandkasten widens specific-host rules to *:PORT with an explicit NOTE in the rendered policy. Per-IP outbound filtering on macOS needs a userspace proxy.
  3. macOS kernel denial capture (the -vvv post-run summary) only surfaces default-deny fallthroughs — explicit (deny …) rules are silent by design in Seatbelt.
  4. Landlock is allow-list only. A deny inside a broader allow emits a warning and is not enforced on Linux; narrow the allow instead.
  5. Linux network plumbing. A fresh netns has no interfaces beyond lo, so for outbound profiles sandkasten auto-detects and uses pasta (from the passt package) or slirp4netns to bridge the private netns to the host network. nftables rules then enforce per-IP policy inside the plumbed netns without touching the host. If neither tool is installed (or pasta is AppArmor-confined on Debian/Ubuntu, which we detect), sandkasten falls back to sharing the host netns — internet still works, but per-IP filtering is not kernel-enforced. sandkasten render <profile> names the active mode explicitly.
  6. Mock mode v1 is a content sidecar. [mocks.files] materialises to $SANDKASTEN_MOCKS. Transparent path interposition (so a program opening /etc/hostname reads the mock without cooperation) requires an LD_PRELOAD / DYLD_INSERT_LIBRARIES shim — planned.
  7. FreeBSD support is not shipped. Unprivileged full-kernel sandboxing on FreeBSD really does require jail(2) + root.
  8. Overlay + Landlock interaction. Overlayfs mounts cleanly in a user namespace, but Landlock's pre-opened PathFds may target the lower-layer inode rather than the merged inode on some kernels. Auto-adding the mount-point path to read_write works on recent 6.x kernels; on older ones writes may still see EACCES.

Disclaimer

sandkasten is provided AS-IS, without warranty of any kind, express or implied, including but not limited to merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors be liable for any claim, damages, or other liability, whether in an action of contract, tort, or otherwise, arising from, out of, or in connection with the software or its use.

Use on systems and against data you are authorised to operate on. The network-filtering, redirection, packet-capture, identity-spoofing, and tracing features are offered for legitimate use — sandboxing untrusted code on your own machines, testing compatibility with custom identities in environments you control, hardening SSH sessions on hosts you administer, and similar. Deploying them against systems without authorisation, circumventing licence enforcement, impersonating customers or users, or concealing the provenance of network traffic for the purpose of abuse is explicitly not supported and may violate local law. The authors accept no responsibility for misuse.

sandkasten is not a substitute for a formally reviewed security product. Kernel vulnerabilities bypass MACF, Landlock and seccomp. Side channels are not addressed. [spoof] presents a plausible user-space view, not a virtualised environment; determined fingerprinting will still identify the real host via unspoofed channels (CPUID instruction, TSC behaviour, unspoofed /proc//sys entries, GPU capabilities, network RTT, etc.).

License

Dual-licensed under MIT or Apache-2.0 at your option.

Roadmap

  • Resource limits, --timeout, PR_SET_NO_NEW_PRIVS
  • Profile signing (minisign verify before apply)
  • Per-IP outbound on Linux via nftables inside the netns
  • DNS override + /etc/hosts pinning (transparent on Linux via bind-mount; sidecar on macOS)
  • Persistent [workspace] + Linux [overlay] + sandkasten snap
  • [spoof] — CPU, DMI, machine-id, kernel identity, thermal, EFI, arbitrary [[spoof.files]] bind-mounts
  • [hardware] — USB / serial / audio / GPU / camera presets
  • [[filesystem.rewire]], [[filesystem.hide]]
  • Protocol coverage: SCTP / DCCP / UDPLite + 35 service presets including WireGuard, Tailscale, Steam, Minecraft, Riot, etc.
  • sandkasten shell / sshd / diff / explain / doctor / snap
  • Reproducibility fingerprint in render
  • End-to-end Linux smoke test in CI
  • Bundled pasta / slirp4netns auto-integration for turnkey Linux outbound, with per-IP nftables filtering enforced inside the plumbed netns; AppArmor-aware fallback to host netns.
  • Homebrew tap published at DatanoiseTV/sandkasten; prebuilt per-arch binaries (~2 s install, no Rust build-dep).
  • Always-on TIOCSTI seccomp block (ioctl-arg conditional deny).
  • Opt-in process.no_w_x (PR_SET_MDWE memory W^X) and process.mitigate_spectre (PR_SET_SPECULATION_CTRL for Spectre v2 + SSBD) on Linux.
  • process.block_privilege_elevation + process.block_setid_syscalls (sudo/su/doas/pkexec exec deny across macOS + Linux + Homebrew + Linuxbrew + Snap; seccomp setid-family deny).
  • sandkasten learn --yes non-interactive capture for scripts / CI.
  • Weekly Dependabot-grouped dependency updates (cargo + swift + github-actions).
  • Transparent mock interposition via LD_PRELOAD / DYLD_INSERT_LIBRARIES.
  • Live policy reload (SIGHUP → re-apply; sandbox_init only narrows).

About

Fast, kernel-enforced application sandbox for macOS and Linux. Default-deny TOML profiles, Seatbelt + Landlock + seccomp + namespaces under the hood. Pasta/slirp4netns auto-plumbed network with per-IP nftables.

Topics

Resources

License

Unknown, MIT licenses found

Licenses found

Unknown
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors