{"id":49336245,"url":"https://github.com/datanoisetv/sandkasten","last_synced_at":"2026-04-27T01:02:00.126Z","repository":{"id":353452465,"uuid":"1219423911","full_name":"DatanoiseTV/sandkasten","owner":"DatanoiseTV","description":"A sandbox for running software in a more secure way. Research project.","archived":false,"fork":false,"pushed_at":"2026-04-24T01:29:18.000Z","size":269,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-24T01:35:18.246Z","etag":null,"topics":["cage","darwin","devops","ipc","isolation","macos","sandbox","sandboxing","security"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DatanoiseTV.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-23T21:32:51.000Z","updated_at":"2026-04-24T01:29:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/DatanoiseTV/sandkasten","commit_stats":null,"previous_names":["datanoisetv/sandkasten"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/DatanoiseTV/sandkasten","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DatanoiseTV%2Fsandkasten","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DatanoiseTV%2Fsandkasten/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DatanoiseTV%2Fsandkasten/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DatanoiseTV%2Fsandkasten/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DatanoiseTV","download_url":"https://codeload.github.com/DatanoiseTV/sandkasten/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DatanoiseTV%2Fsandkasten/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32318417,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T23:26:28.701Z","status":"ssl_error","status_checked_at":"2026-04-26T23:26:25.802Z","response_time":129,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cage","darwin","devops","ipc","isolation","macos","sandbox","sandboxing","security"],"created_at":"2026-04-27T01:01:59.374Z","updated_at":"2026-04-27T01:02:00.114Z","avatar_url":"https://github.com/DatanoiseTV.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# sandkasten\n\n\u003e A fast, kernel-enforced application sandbox for macOS and Linux.\n\u003e Describe what a program may touch in TOML; sandkasten enforces it in the kernel.\n\n```\nprofile.toml ──▶ sandkasten ──▶ fork ─▶ sandbox_init() ─▶ execve(target)\n                                          │\n                                          ├─ macOS: Seatbelt (MACF, kernel)\n                                          └─ Linux: user+mount+pid+ipc+uts[+net] namespaces\n                                                    + Landlock LSM\n                                                    + seccomp-BPF\n                                                    + PR_SET_NO_NEW_PRIVS\n                                                    + resource limits (setrlimit)\n```\n\nWritten in Rust. Single ~2 MB release binary. No daemon, no service, no setuid.\nUnprivileged — sandkasten itself never requires root.\n\n## At a glance\n\n- **Kernel enforcement.** macOS calls `sandbox_init`, Linux unshares namespaces\n  and installs Landlock + seccomp. All decisions happen in the kernel; zero\n  userspace interposition overhead after policy is applied.\n- **Portable profiles.** One TOML file works on both platforms. The generators\n  pick the right primitive per OS and warn when something's unexpressible.\n- **Default deny.** Filesystem, network, Mach services, sysctl, IOKit, IPC —\n  all off unless the profile opts in. Templates (`strict`, `minimal-cli`,\n  `self`, `dev`, `browser`, `electron`, `network-client`) provide sane starts.\n- **Privilege-elevation guardrails.** `process.block_privilege_elevation = true`\n  denies exec of `sudo` / `su` / `doas` / `pkexec` / `runuser` / `visudo`\n  across macOS and Linux (incl. Homebrew, Linuxbrew, Snap, and\n  `/usr/local/bin/...` installs). `process.block_setid_syscalls = true`\n  seccomp-denies every setuid/setgid-family syscall on Linux so shellcode\n  that skips the named binary can't gain creds either.\n- **Interactive OR scripted learning.** `sandkasten learn -- \u003ccmd\u003e` runs the\n  target with full permissions while capturing every operation it performs,\n  applies heuristics (subtree collapsing, sensitive-path flagging, preset\n  detection), and interactively proposes a tight profile. Use `--yes` for\n  a non-interactive mode that accepts every bucket (except sensitive paths,\n  which always stay default-deny).\n- **Honest limits.** Failure modes and platform asymmetries are documented\n  inline in the generated policy and in this README. See *Limits*, below.\n\n\u003e 🚀 **New here?** Start with the [Quick Start](docs/QUICKSTART.md) —\n\u003e installs, first sandboxed command, templates at a glance, writing a\n\u003e profile, hardening knobs, CI/CD. This README is the full reference.\n\n## Install\n\n### Homebrew (macOS + Linuxbrew)\n\n```sh\nbrew tap DatanoiseTV/sandkasten\nbrew install sandkasten\n```\n\nThe formula installs from prebuilt per-arch tarballs from the GitHub\nrelease — ~2 s wall-clock, no Rust toolchain required. Shell completions\nfor bash/zsh/fish are installed automatically on the native triples\n(arm64-macos, x86_64-linux).\n\n### Direct download\n\nEach release ships tarballs for every `{aarch64,x86_64}-{apple-darwin,\nunknown-linux-gnu}` combo plus a versionless alias so generic URLs\nwork across version bumps. Grab the one for your platform from\n\u003chttps://github.com/DatanoiseTV/sandkasten/releases/latest\u003e or one-liner it:\n\n```sh\n# Linux x86_64 — latest release, auto-resolved server-side, no version pin:\ncurl -sSL https://github.com/DatanoiseTV/sandkasten/releases/latest/download/sandkasten-x86_64-unknown-linux-gnu.tar.gz \\\n  | tar -xz \u0026\u0026 sudo install sandkasten-*/sandkasten /usr/local/bin/\n\n# macOS Apple Silicon:\ncurl -sSL https://github.com/DatanoiseTV/sandkasten/releases/latest/download/sandkasten-aarch64-apple-darwin.tar.gz \\\n  | tar -xz \u0026\u0026 sudo install sandkasten-*/sandkasten /usr/local/bin/\n```\n\nSwap the triple for `aarch64-unknown-linux-gnu` (Linux arm64) or\n`x86_64-apple-darwin` (Intel Macs). Pin a specific release by\nreplacing `latest/download/` with `download/\u003ctag\u003e/` and adding the\n`\u003ctag\u003e-` prefix to the filename.\n\n### From source\n\n```sh\ncargo install --path .\n# or\ncargo build --release   # → target/release/sandkasten\n```\n\nRuntime dependencies: none on either platform — the prebuilt binary\nis statically self-contained. Linux optionally *benefits* from `pasta`\n(from the `passt` package) or `slirp4netns` for external network\nconnectivity under a private netns with per-IP `nftables` filtering,\nand `strace` for `sandkasten learn`. `sandkasten doctor` prints\ndistro-tailored install commands for anything missing.\n\n## 60-second tour\n\n```sh\n# See what's available\nsandkasten templates\nsandkasten doctor\n\n# Run /bin/cat sandboxed — only the current directory is writable\nsandkasten run self -- /bin/cat README.md\n\n# Write a tight profile interactively by observing what an app does\nsandkasten learn --auto-system -o my-tool.toml -- ./my-tool --help\n\n# Pre-flight review before running: explain in plain English\nsandkasten explain my-tool.toml\n\n# Structural diff between two profiles\nsandkasten diff self dev\n\n# Launch a Chromium-based browser in a throwaway sandbox\nsandkasten run browser -- \\\n  \"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser\" \\\n  --no-sandbox --password-store=basic\n\n# Web UI for editing profiles (local, token-gated)\nsandkasten ui\n```\n\n## Example use cases\n\n### Run untrusted code from the internet\n\nYou just cloned a repo and want to `npm install` without letting it read\n`~/.ssh` or exfiltrate your cloud credentials.\n\n```toml\n# untrusted.toml\nname = \"untrusted-npm\"\nextends = \"self\"\n[filesystem]\nread_write = [\"${CWD}\"]\n[network]\nallow_dns = true\npresets = [\"https\"]         # TCP 443 outbound for registry\n[process]\nallow_fork = true\nallow_exec = true\n[env]\npass = [\"PATH\", \"HOME\", \"NODE_PATH\", \"NPM_CONFIG_REGISTRY\"]\n[limits]\nwall_timeout_seconds = 600   # cap install at 10 minutes\nmemory_mb = 4096\n```\n\n```sh\nsandkasten run ./untrusted.toml -- npm install\n```\n\n`~/.ssh`, `~/.aws`, `~/.gnupg`, keychains, shell history, TCC database are\nall inherited-denied from the `self` template. The package script can't\nreach them even if it tries — sandbox returns EPERM.\n\n### Block re-exec through sudo / su (defense against cached creds)\n\n```toml\n# harder.toml — most hosts have NOPASSWD: ALL sudoers entries for the\n# user account at some point. A compromised sandboxed tool could call\n# `sudo sh -c 'curl ... | sh'` and escalate to host-root before the\n# user notices. This flag denies exec of every named elevation binary\n# and, on Linux, also seccomp-denies the setuid-family syscalls so\n# shellcode that skips the binary still can't flip creds.\nextends = \"dev\"\n[process]\nblock_privilege_elevation = true   # implies block_setid_syscalls\n```\n\n```sh\nsandkasten run harder.toml -- ./untrusted-tool\n# Inside: `sudo whoami` → sandkasten: execve failed: /usr/bin/sudo errno=1\n# `/usr/bin/python3 -c 'import os; os.setuid(0)'` → OSError: EPERM\n```\n\nWorks symmetrically on macOS (Seatbelt `(deny process-exec ...)`) and\nLinux (Landlock exclusion + seccomp). The binary list covers standard\n`/usr/bin/`, Homebrew on Apple Silicon, Linuxbrew, Snap, and\n`/usr/local/bin/...` for locally compiled installs — not just the\nmacOS paths.\n\n### Sandbox an AI coding agent (Claude Code, opencode, aider, …)\n\nAgentic CLI tools run shell commands on your behalf — `npm install`,\n`git push`, `pytest`, `gh pr create`, sometimes things you didn't\nquite anticipate. By default they inherit your full shell\nenvironment: `~/.ssh`, `~/.aws`, `GITHUB_TOKEN`, the credential\nhelpers behind `git push`, the cached `sudo` timestamp. A\nprompt-injected tool call or a compromised dependency can quietly\nwalk off with any of those.\n\nWrapping the agent in sandkasten gives it exactly what it needs and\nno more — and because sandbox restrictions inherit through `fork()`\n+ `execve()` (verified earlier in this README's *Threat model*\nsection), every shell command the agent kicks off lives inside the\nsame sandbox automatically.\n\nA ready-made profile lives at [`examples/ai-agent.toml`](examples/ai-agent.toml).\nOn Homebrew installs it's already on the search path; on direct-\ninstall systems run `sandkasten install-profiles --user` once.\n\n```sh\n# Set the model API key in your shell (NOT cached in Keychain — the\n# profile's hard-deny on ~/Library/Keychains is what stops an agent\n# from walking off with creds from your other apps).\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\n# or:\nexport OPENAI_API_KEY=\"sk-...\"\n\n# One-off launch:\nsandkasten run ai-agent -- claude\nsandkasten run ai-agent -- opencode\nsandkasten run ai-agent -- aider\n\n# Or alias it so the original command name \"just works\":\nalias claude='sandkasten run ai-agent -- claude'\nalias opencode='sandkasten run ai-agent -- opencode'\n```\n\n\u003e If `claude` (or any agent that defaults to OAuth → macOS Keychain)\n\u003e hangs at startup with no TUI rendering, it's almost certainly the\n\u003e auth gate: the agent is waiting on a Keychain lookup that's denied.\n\u003e Set `ANTHROPIC_API_KEY` in the outer shell — the profile's\n\u003e `env.pass` whitelist passes it through. Run\n\u003e `sandkasten -vvv run ai-agent -- claude` to see kernel denials.\n\u003e\n\u003e **If you can't use a model API key** (no key handy, OAuth-only\n\u003e provider, etc.), there's an opt-in variant `ai-agent-keychain`\n\u003e which permits `~/Library/Keychains` so OAuth login can persist a\n\u003e token. The trade-off is real — a compromised agent can read every\n\u003e Keychain entry the user owns — so `ai-agent` (with\n\u003e `ANTHROPIC_API_KEY`) remains the recommended default. Run\n\u003e `sandkasten run ai-agent-keychain -- claude` instead, then on\n\u003e first launch complete `/login` once.\n\nWhat the profile (`extends = \"minimal-cli\"`) actually does:\n\n- **Reads** anywhere — agents legitimately grep through deps, read\n  system headers, etc.\n- **Writes** only the project (`${CWD}`), the agent's own state\n  directory (`~/.config/claude`, `~/.claude`,\n  `~/Library/Application Support/Claude`, plus opencode/aider/…\n  equivalents), `~/.cache`, and `$TMPDIR`.\n- **Hard-denies** `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.docker`,\n  `~/.kube`, `~/.netrc`, `~/.password-store`, `~/.config/gcloud`,\n  shell history, macOS Keychains + TCC + Cookies + Mail + Messages,\n  Linux keyrings, KeePass.\n- **Outbound** restricted to a curated list of model APIs\n  (Anthropic, OpenAI, Gemini, OpenRouter, Mistral, Groq, Together,\n  DeepSeek, Cohere, Fireworks, Azure OpenAI), GitHub, and the major\n  package registries. On Linux this is enforced per-host via\n  nftables inside the pasta/slirp4netns netns. On macOS Seatbelt\n  widens specific hostnames to `*:443` (a documented kernel limit);\n  combine with `[network.proxy]` + mitmproxy / Squid for true\n  semantic filtering on macOS.\n- **`block_privilege_elevation`** — `sudo` / `su` / `doas` / `pkexec`\n  / `runuser` / `visudo` are denied at exec, even if the host user\n  has `NOPASSWD: ALL` or a still-cached password.\n- **`block_setid_syscalls`** — Linux seccomp denies the entire\n  setuid family so shellcode can't drop or gain creds without going\n  through a named elevation binary.\n- **`env.pass` whitelisted** — the agent sees its own model API\n  keys (`ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / etc.) but not\n  `GITHUB_TOKEN`, `AWS_*`, `KUBECONFIG`, `NPM_TOKEN`, `PYPI_TOKEN`.\n- **No `[limits]` block** for the interactive case. Hard CPU /\n  wall-clock / memory caps kill long agent sessions at arbitrary\n  times, and on macOS `RLIMIT_NPROC` is per-real-user (not\n  per-process), so any cap you set covers your whole logged-in\n  session and Bun-based agents will EAGAIN on `posix_spawn` as soon\n  as they fork their worker pool. If you're driving the agent from\n  CI / batch, copy the profile and add a `[limits]` block tuned to\n  that workload.\n\nIf you want stricter network posture: drop everything from\n`outbound_tcp` except the model API actually in use; the agent will\nfail any package install, which is often what you want.\n\nIf you want stricter filesystem posture: change `read = [\"/\"]` to a\nnarrower list (typically `${CWD}`, `/usr/lib`, `/usr/share`,\n`/Library/Apple/System`, `/private/var/db/dyld`) so even the agent\ncan't read other projects on your laptop.\n\n### Sandbox a server application (HTTP server, API, database, worker)\n\nFour bundled profiles cover the common production server shapes.\nAll four reduce the blast radius of a code-injection or supply-chain\ncompromise to roughly \"what the listed network endpoints + writable\npaths allow\", which is usually a much narrower set than the host the\nprocess otherwise has access to.\n\n```sh\n# HTTP / reverse proxy — bind 80/443, write only logs, optional\n# outbound to upstream backends (edit examples/web-server.toml).\nsandkasten run web-server -- /usr/sbin/nginx -g \"daemon off;\"\nsandkasten run web-server -- /usr/local/bin/caddy run\n\n# Application API — bind one port, strict outbound to DB + upstream\n# APIs only, no exec by default (no shelling out for ImageMagick /\n# ffmpeg / git unless you opt in).\nsandkasten run api-server -- node /srv/api/dist/server.js\nsandkasten run api-server -- gunicorn -b 0.0.0.0:8000 myapp.wsgi:app\n\n# Database daemon — bind one port, NO outbound, write only the data\n# dir + WAL + log dir. memory_mb sized for the buffer pool, not a\n# generic small number.\nsandkasten run database -- /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/16/main\n\n# Background worker / queue consumer — no inbound, narrow outbound\n# to broker + DB + APIs.\nsandkasten run worker -- bundle exec sidekiq -q default\nsandkasten run worker -- celery -A myapp worker -l info\n```\n\nWhat's locked down across all four:\n\n- **No `cpu_seconds`, no `wall_timeout_seconds`** — daemons run\n  forever; both rlimits are footguns when set (\"0\" is \"kill now\",\n  not \"unlimited\").\n- **`block_privilege_elevation` + `block_setid_syscalls`** — even a\n  fully-RCE'd process can't `sudo` or call `setuid()` to gain\n  another user's permissions.\n- **`allow_exec = false` by default** — no shelling out. A SQL\n  injection that pivots to RCE can't exec `bash`, `nc`, `wget`,\n  `curl`. Flip per-profile when your app legitimately invokes\n  helpers (CGI, ImageMagick, ffmpeg).\n- **`no_w_x = true`** for AOT engines (nginx/caddy/Postgres/Redis),\n  off for JIT runtimes (Node/Bun/JVM/V8). The profile sets the\n  right default for its expected workload.\n- **`env.pass` whitelist** — the app gets `DATABASE_URL` and\n  `STRIPE_API_KEY`, NOT `AWS_*`, `KUBECONFIG`, `GITHUB_TOKEN`. A\n  log line of `process.env` can only spill what's whitelisted.\n\nProfiles to copy and adapt:\n[`examples/web-server.toml`](examples/web-server.toml),\n[`examples/api-server.toml`](examples/api-server.toml),\n[`examples/database.toml`](examples/database.toml),\n[`examples/worker.toml`](examples/worker.toml).\nEach has the upstream/inbound list commented as a starting point —\nedit the entries to match your topology before deploying.\n\n### Sandbox a Chromium-family browser for a one-off session\n\n```sh\nsandkasten run browser -- \\\n  \"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser\" \\\n  --no-sandbox --password-store=basic\n```\n\nThe `browser` template grants a broad FS read (so rendering, extensions,\nfile pickers work), narrow writes (only caches, preferences, Downloads,\nDesktop, Documents), every Mach service the browser needs, and **hard\ndenies** Keychains, SSH keys, cookies, shell history, Mail/Messages\nstores, and other browsers' profile directories.\n\n`--no-sandbox` disables Chromium's own per-renderer-process sandbox.\nOn macOS this is currently **required** under sandkasten (without it,\nChromium fails to initialise with \"sandbox initialization failed:\nOperation not permitted\" — our outer Seatbelt blocks the MAC-policy\nregistration calls and helper-process Mach IPC Chromium needs to\nnest its own sandbox inside ours).\n\nThat's a real trade-off worth understanding: Chromium's inner\nsandbox normally isolates renderers from each other (one tab can't\nread another tab's memory or files), and we lose that. A malicious\nsite's renderer gets whatever FS scope our profile grants the parent\nprocess — by default that's a broad read so rendering and file\npickers work. Treat the `browser` profile as protection from \"what\nthe browser process accidentally pokes at\" (Keychains, SSH keys,\ncookies, shell history, other browsers' profiles), NOT as a\nreplacement for Chromium's per-tab isolation.\n\nIf your threat model needs per-tab isolation, use a separate macOS\nuser account or a VM; sandkasten's outer Seatbelt-on-Chromium story\nis a coarser, all-or-nothing layer.\n\n`--password-store=basic` silences the \"Encryption is not available\"\nwarning that appears when the browser can't reach the keychain\n(because we intentionally denied it).\n\n### Jail SSH logins\n\nIn `/etc/ssh/sshd_config`:\n\n```\nMatch User sandboxed\n    ForceCommand /usr/local/bin/sandkasten sshd dev\n```\n\nEvery interactive login by `sandboxed` runs `$SHELL -l` under the `dev`\nprofile. `ssh sandboxed@host 'some command'` runs the command through\n`/bin/sh -c` under the same sandbox — `$SSH_ORIGINAL_COMMAND` is picked\nup by `sandkasten sshd`.\n\n### Hook an app that dials hard-coded IPs onto a local service (Linux)\n\nThe app pings `1.2.3.4:443` and you want it to hit your local development\nserver without modifying the binary:\n\n```toml\n[[network.redirects]]\nfrom = \"1.2.3.4:443\"\nto   = \"127.0.0.1:8443\"\nprotocol = \"tcp\"\n\n[network]\nallow_localhost = true\n```\n\nApplied via nftables DNAT inside the sandbox's private netns. The host's\nnetwork stack is untouched. For hostname-based apps, prefer\n`[network.hosts_entries]` — it works cross-platform and survives TLS SNI.\n\n### Route sandboxed traffic through a VPN (Linux)\n\nsandkasten can **join an existing network namespace** instead of creating\nits own. If you've set up WireGuard (or OpenVPN, or any tunnel) in a\nnamed netns, point the profile at it and every byte the sandbox sends\nrides the tunnel:\n\n```sh\n# one-off setup (root, host)\nip netns add vpn\nip link add wg0 type wireguard\nip link set wg0 netns vpn\nip netns exec vpn wg setconf wg0 /etc/wireguard/wg0.conf\nip netns exec vpn ip addr add 10.0.0.2/24 dev wg0\nip netns exec vpn ip link set wg0 up\nip netns exec vpn ip route add default dev wg0\n```\n\n```toml\n# profile.toml\n[network]\nnetns_path = \"/run/netns/vpn\"\nallow_dns = true\noutbound_tcp = [\"*:443\"]\n```\n\n```sh\nsandkasten run profile.toml -- curl https://ifconfig.me\n# → reports the VPN endpoint's IP, not yours\n```\n\nSandbox applies as usual on top — Landlock, seccomp, resource limits —\nbut the kernel routes `connect()` through the VPN. No LD_PRELOAD, no\nuserspace proxy. Per-IP nftables rules inside this netns still work.\n\n### Controlled hardware identity for testing\n\nA compatibility test suite wants to see a specific CPU, machine-id, DMI\nserial, and kernel version:\n\n```toml\n[spoof]\ncpu_count       = 4                 # sched_setaffinity pins to 4 cores\ncpuinfo_synth   = true\ncpuinfo_model   = \"Intel(R) Xeon(R) E5-2697 v4 @ 2.30GHz\"\nhostname        = \"test-rig-07\"\nmachine_id      = \"deadbeefcafebabe0123456789abcdef\"\nkernel_version  = \"Linux version 6.12.0-stable #1 SMP\"\nkernel_release  = \"6.12.0-stable\"\nos_release      = \"\"\"\nNAME=\"FleetOS\"\nVERSION=\"2025.10\"\nID=fleetos\n\"\"\"\n\n[spoof.dmi]\nproduct_serial = \"FLEET-00042\"\nsys_vendor     = \"AcmeCo\"\nboard_name     = \"Fleetboard R7\"\n\n[[spoof.files]]\npath = \"/sys/class/net/lo/address\"\ncontent = \"00:de:ad:be:ef:01\"\n```\n\nVerified: `nproc` returns 4, `/etc/machine-id` reads the spoofed value,\n`/proc/cpuinfo` shows \"Sandkasten CPU\" (or your override), host files\nuntouched. See *Limits* for what the kernel syscall `uname` will and\nwon't let us spoof.\n\n### USB / libusb in a sandbox\n\n```toml\n[hardware]\nusb    = true\nserial = true   # also /dev/ttyUSB* /dev/ttyACM*\n```\n\nLinux: grants read+write on `/dev/bus/usb` and read on the udev bits\nlibusb consults. macOS: grants IOKit + the USB driver family Mach\nservices.\n\n### Camera / video-device control\n\n```toml\n[hardware]\ncamera = true             # V4L2 (Linux) / AVFoundation (macOS)\nscreen_capture = true     # PipeWire screencast (Linux) / ScreenCaptureKit (macOS)\n\n[hardware.video]\n# Only /dev/video0 is visible; every other /dev/video*, /dev/media*,\n# /dev/v4l-subdev* is hidden via an empty bind-mount so enumeration\n# returns nothing rather than EPERM.\ndevices = [\"/dev/video0\"]\n\n# Redirect: inside the sandbox /dev/video0 actually resolves to the\n# host's /dev/video5. Useful for v4l2loopback pipes (feed a fake camera\n# stream from a file or another process into /dev/video5, the sandbox\n# sees /dev/video0).\nredirect = { \"/dev/video0\" = \"/dev/video5\" }\n```\n\nLinux implements both via the same mount-namespace bind-mount primitive\nused by DNS overrides; see `[[filesystem.rewire]]` / `[[filesystem.hide]]`\nif you want the raw form. macOS uses the CoreMediaIO + ScreenCaptureKit\nMach services — AVFoundation doesn't route through device nodes, so the\nallowlist/redirect is Linux-only there (documented in the emitted\npolicy).\n\n### Isolated packet capture / port scanning\n\n```toml\nextends = \"minimal-cli\"\n[network]\npresets = [\"nmap\"]            # allow_raw_sockets + ICMP + DNS\nallow_localhost = true\n```\n\nInside a private netns with `CAP_NET_RAW` you can run `tcpdump` or\n`nmap` against loopback or any veth you've plumbed in, without that\nactivity being visible on the host's interfaces.\n\n### Isolate a CI/CD step (GitHub Actions example)\n\nDependency installs (`npm install`, `pip install`, `cargo fetch`, …),\nuntrusted PR test code, and build steps that execute scripts from\nthird-party packages are the classic supply-chain attack surface on a\nCI runner. Wrapping them in sandkasten keeps them off the runner's\ncredentials, the tokens in `~/.aws` / `~/.docker`, and the rest of the\nworkspace.\n\n```yaml\n# .github/workflows/sandboxed-install.yml\nname: sandboxed-install\non: [push]\n\njobs:\n  build:\n    runs-on: ubuntu-22.04    # 24.04 ships an AppArmor profile that\n                             # blocks unprivileged userns — either use\n                             # 22.04, or add `sudo aa-teardown`.\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Install sandkasten (prebuilt binary, ~2s — tracks latest)\n        run: |\n          # Versionless alias resolved server-side → this step stays\n          # green across version bumps with no CI edits. Pin a\n          # specific release by swapping `latest/download/` for\n          # `download/v0.4.0/` and prefixing the filename with the\n          # tag, if you want reproducible runs.\n          curl -sSL \\\n            https://github.com/DatanoiseTV/sandkasten/releases/latest/download/sandkasten-x86_64-unknown-linux-gnu.tar.gz \\\n            | tar -xz\n          sudo install sandkasten-*/sandkasten /usr/local/bin/\n          # slirp4netns → real outbound + per-IP nftables filtering\n          # inside the sandbox. Without it, network-client falls back\n          # to host netns (still works, just loses per-IP enforcement).\n          sudo apt-get update -qq \u0026\u0026 sudo apt-get install -y -qq slirp4netns\n\n      - name: `npm install` under a hardened sandbox\n        run: |\n          # Fresh profile in the workspace dir — no access to host HOME,\n          # no ~/.ssh / ~/.aws / ~/.npmrc leakage, only outbound to the\n          # npm registry.\n          cat \u003e ci.toml \u003c\u003c'EOF'\n          name = \"ci-npm\"\n          extends = \"network-client\"\n          [filesystem]\n          read_write = [ \"${CWD}\" ]\n          [network]\n          outbound_tcp = [\n            \"*:443\",             # registry.npmjs.org et al.\n          ]\n          [process]\n          block_privilege_elevation = true\n          block_setid_syscalls      = true\n          no_w_x                    = true   # Linux 6.3+; safe for npm\n          EOF\n          sandkasten run ci.toml -- npm ci --no-audit --no-fund\n\n      - name: Run tests under the same profile\n        run: sandkasten run ci.toml -- npm test\n```\n\nWhat this gives you on a standard GitHub hosted runner:\n\n- `package.json` post-install scripts can't reach `~/.npmrc` / `~/.aws` /\n  the `GITHUB_TOKEN` env var the runner auto-exports (it's not\n  in the profile's `env.pass`).\n- Outbound is restricted to TCP 443 — a compromised install can't\n  exfiltrate to `curl http://attacker:8080/` or SSH tunnel out.\n- `process.block_privilege_elevation` neuters `sudo` even if the\n  runner has a passwordless sudoers entry (GitHub's does).\n- `no_w_x` blocks the classic \"write shellcode into an RW page,\n  mprotect it executable, jump to it\" pattern.\n\nSelf-hosted runners get the same guarantees plus full per-IP\noutbound filtering (pasta or slirp4netns plumbs the private netns).\nOn hosted runners the `network-client` base falls back to host netns\nwhen pasta/slirp4netns isn't installed — network is still reachable,\nbut per-IP filtering isn't kernel-enforced.\n\n### Isolate a CI/CD step (GitLab CI example)\n\n```yaml\nsandboxed-tests:\n  image: ubuntu:22.04\n  before_script:\n    - apt-get update -qq \u0026\u0026 apt-get install -y -qq curl slirp4netns ca-certificates\n    # Versionless alias auto-resolved to the current release — no\n    # pipeline bumps needed when sandkasten updates.\n    - curl -sSL https://github.com/DatanoiseTV/sandkasten/releases/latest/download/sandkasten-x86_64-unknown-linux-gnu.tar.gz | tar -xz\n    - install sandkasten-*/sandkasten /usr/local/bin/\n  script:\n    - |\n      cat \u003e ci.toml \u003c\u003c'EOF'\n      extends = \"network-client\"\n      [filesystem]\n      read_write = [ \"${CWD}\" ]\n      [process]\n      block_privilege_elevation = true\n      block_setid_syscalls      = true\n      EOF\n    - sandkasten run ci.toml -- ./run-untrusted-tests.sh\n```\n\nNote on GitLab/self-hosted runners: the `unprivileged_userns_clone`\nsysctl must be set to 1 (default on most recent distros). `sandkasten\ndoctor` reports the value and the distro-specific one-liner to enable\nit.\n\n### Drop a throwaway overlay for ephemeral experiments (Linux)\n\n```toml\n[overlay]\nlower = \"/opt/bigapp\"                          # read-only base\nupper = \"~/.sandkasten/overlay/bigapp\"         # writes land here\n# mount = \"/opt/bigapp\"  ← default, in-place\n\n[workspace]\npath  = \"~/.sandkasten/work/bigapp\"\nchdir = true\n```\n\nWrites to `/opt/bigapp/*` don't touch the real base — they land in\n`upper`. Snapshot any time:\n\n```sh\nsandkasten snap save bigapp before-experiment\n# ... do dangerous things inside the sandbox ...\nsandkasten snap load bigapp before-experiment  # instant rewind\nsandkasten snap list bigapp\n```\n\nPrevious state is moved aside to `\u003cupper\u003e.bak-\u003cts\u003e` — nothing is\never deleted silently.\n\n### HTTP method / URL filtering, header rewrites\n\nsandkasten's enforcement is **L3/L4** — the kernel sees addresses and\nports, not HTTP. For L7 rules (block `DELETE`, rewrite the `Host`\nheader, add `X-Forwarded-For`, return a synthetic 403 on\n`/api/admin/*`) pair sandkasten with a userland proxy. Pattern:\n\n```toml\n[network]\nallow_dns = true\n\n[network.proxy]\nurl    = \"http://127.0.0.1:8080\"     # your mitmproxy / squid / caddy\nbypass = [\"127.0.0.1\", \"localhost\"]\n# restrict_outbound = true           # default — sandbox can ONLY talk\n                                     # to the proxy + bypass hosts\n```\n\nWith `restrict_outbound` on, `outbound_tcp` is auto-narrowed to just\nthe proxy's `host:port` plus each `bypass` entry. `HTTP_PROXY` /\n`HTTPS_PROXY` / `ALL_PROXY` / `NO_PROXY` (and their lowercase forms)\nare set in the sandbox's env. Every URL library the app uses — curl,\nlibcurl, Go's `net/http`, Python's `requests`, Node's `http` — honours\nthose env vars.\n\nThen on the proxy side (example mitmproxy addon):\n\n```python\n# save as rewrite.py; run: mitmproxy -s rewrite.py --listen-port 8080\nfrom mitmproxy import http\n\nclass Rewrite:\n    def request(self, flow: http.HTTPFlow) -\u003e None:\n        # Block dangerous HTTP verbs.\n        if flow.request.method in (\"DELETE\", \"PUT\"):\n            flow.response = http.Response.make(403, b\"blocked by sandkasten+mitmproxy\")\n            return\n        # Rewrite Host + add X-Forwarded-For.\n        if \"api.prod.example.com\" in flow.request.pretty_host:\n            flow.request.host = \"api.staging.example.com\"\n        flow.request.headers[\"X-Forwarded-For\"] = \"10.0.0.1\"\n\naddons = [Rewrite()]\n```\n\nThe kernel sandbox guarantees the app can't route around the proxy;\nthe proxy enforces the application-layer policy.\n\n## Command reference\n\n```\nsandkasten run \u003cprofile\u003e [--timeout 30s] [--verify] [-C \u003ccwd\u003e] -- \u003ccmd\u003e [args...]\nsandkasten shell \u003cprofile\u003e                 # interactive sandboxed shell, $SANDKASTEN_PROFILE set\nsandkasten sshd \u003cprofile\u003e                  # for sshd ForceCommand — see Use cases\nsandkasten init [--template \u003cname\u003e] [-o \u003cpath\u003e]\nsandkasten install-profiles [--system|--user] [--force] [-s \u003csrc-dir\u003e]\nsandkasten learn [--base \u003ctpl\u003e] [-o \u003cout.toml\u003e] [--auto-system] [--yes|-y] -- \u003ccmd\u003e\nsandkasten check \u003cprofile\u003e                 # validate without running\nsandkasten render \u003cprofile\u003e                # print generated policy (+ policy-hash trailer)\nsandkasten explain \u003cprofile\u003e               # plain-English summary\nsandkasten diff \u003cprofile\u003e \u003cprofile\u003e        # structural diff between two profiles\nsandkasten verify \u003cprofile\u003e                # minisign signature check\nsandkasten snap save|load|list \u003cprofile\u003e \u003cname\u003e   # overlay upperdir snapshots\nsandkasten list                            # user profiles + built-in templates\nsandkasten templates                       # built-in templates + descriptions\nsandkasten doctor                          # environment / dependency check\nsandkasten ui [--port 4173]                # local web UI\n```\n\nVerbosity: default is silent, `-v` adds lifecycle, `-vv` adds a compact\nrule summary, `-vvv` adds the full generated policy plus post-run\nkernel denial capture (macOS).\n\n### Profile resolution\n\nWhen you write `sandkasten run \u003cname\u003e -- …`, sandkasten resolves\n`\u003cname\u003e` against this search order, first hit wins:\n\n1. **An explicit path** — `\u003cname\u003e` contains `/` or ends in `.toml`,\n   read literally.\n2. **`./\u003cname\u003e.toml`** — current working directory.\n3. **User profile dir** —\n   `$XDG_CONFIG_HOME/sandkasten/profiles/` on Linux,\n   `~/Library/Application Support/sandkasten/profiles/` on macOS.\n4. **System profile dirs**, in order:\n   - `/etc/sandkasten/profiles/` (admin overrides; Linux convention)\n   - `/Library/Application Support/sandkasten/profiles/` (admin\n     overrides; macOS convention)\n   - `/opt/homebrew/share/sandkasten/profiles/` (Homebrew on Apple Silicon)\n   - `/usr/local/share/sandkasten/profiles/` (Homebrew on Intel,\n     hand-built `make install`)\n   - `/home/linuxbrew/.linuxbrew/share/sandkasten/profiles/` (Linuxbrew)\n   - `/usr/share/sandkasten/profiles/` (Linux distro packaging)\n\nEarlier entries shadow later ones, so a per-user copy wins over a\nsystem one and an `/etc` override wins over a Homebrew-shipped\ndefault. `sandkasten list` enumerates everything visible from the\ncurrent process's view.\n\n`brew install sandkasten` drops the bundled example profiles\n(currently just `ai-agent.toml`) into\n`\u003cHOMEBREW_PREFIX\u003e/share/sandkasten/profiles/` so\n`sandkasten run ai-agent -- claude` works out of the box.\n\nFor non-Homebrew installs, drop bundled profiles in by hand:\n\n```sh\nsandkasten install-profiles            # writes to user dir, no sudo needed\nsudo sandkasten install-profiles --system   # writes to /etc or /Library\nsandkasten install-profiles -s ./my-org/profiles --user  # add a custom dir\n```\n\n## Profile schema\n\nA profile is TOML. Everything is optional. `extends` inherits from a\nbuilt-in template; list-valued fields concatenate, scalars prefer the\nchild, and path variables (`${CWD}`, `${HOME}`, `${EXE_DIR}`, `~`, any\nenv var) are expanded at run time. To **narrow** an inherited field\n(replace it with the child's value rather than union with the parent),\nlist its dotted path under top-level `clear`:\n\n```toml\nextends = \"browser\"\nclear   = [\n    \"network.outbound_tcp\",   # throw out parent's wide outbound list\n    \"network.allow_dns\",      # parent set true → child can now turn it off\n]\n\n[network]\nallow_dns    = false\noutbound_tcp = []             # actually empty, not unioned with parent\n```\n\nWithout `clear`, a child can only widen — never narrow — its parent.\nUnknown paths in `clear` are a load-time error so typos don't silently\nno-op a security tightening.\n\n```toml\nname        = \"my-profile\"\ndescription = \"What this profile is for\"\nextends     = \"self\"\n\n# ── FILESYSTEM ──────────────────────────────────────────────────────────\n[filesystem]\nallow_metadata_read = true\nread             = [\"/usr/lib\", \"/System\"]\nread_write       = [\"${CWD}\", \"/tmp\"]\nread_files       = [\"/etc/hosts\"]\nread_write_files = [\"/dev/null\", \"/dev/tty\"]\ndeny             = [\"${HOME}/.ssh\"]\nhide             = [\"/etc/shadow\"]      # Linux: tmpfs/dev-null bind-mount\n                                         # macOS: emits SBPL deny\n\n# Fine-grained ops per path. Tokens: read, write, create, delete, rename,\n# chmod, chown, xattr, ioctl, exec, all, write-all.\n[[filesystem.rules]]\npath    = \"${CWD}/important.log\"\nliteral = true\nallow   = [\"read\", \"write\"]\ndeny    = [\"delete\", \"chmod\"]\n\n# Linux: symbolic-path substitution via bind-mount in the mount namespace.\n[[filesystem.rewire]]\nfrom = \"/etc/resolv.conf\"\nto   = \"${CWD}/my-resolv.conf\"\n\n# ── NETWORK ─────────────────────────────────────────────────────────────\n[network]\nallow_localhost    = true\nallow_dns          = true\nallow_inbound      = false\nallow_icmp         = false\nallow_icmpv6       = false\nallow_sctp         = false\nallow_dccp         = false\nallow_udplite      = false\nallow_raw_sockets  = false     # AF_INET/SOCK_RAW — packet-crafting\nallow_unix_sockets = true      # AF_UNIX — Chromium/Electron/docker need this\noutbound_tcp       = [\"*:443\", \"example.com:8080\", \"10.0.0.5:22\"]\noutbound_udp       = []\ninbound_tcp        = []\ninbound_udp        = []\nextra_protocols    = []        # additional `meta l4proto X` on Linux\npresets            = [\"https\", \"ssh\", \"postgres\"]   # see table below\n\n[network.dns]\nservers = [\"1.1.1.1\", \"9.9.9.9\"]\nsearch  = [\"corp.internal\"]\noptions = [\"edns0\", \"rotate\"]\n\n[network.hosts_entries]\n\"api.test.lan\" = \"127.0.0.1\"\n\n# Linux-only DNAT\n[[network.redirects]]\nfrom = \"1.2.3.4:443\"\nto   = \"127.0.0.1:8443\"\nprotocol = \"tcp\"\n\n# Outbound blocks. Linux: nftables REJECT. macOS: SBPL deny (Seatbelt\n# grammar widens specific hosts to `*:PORT` — documented in the render).\n[[network.blocks]]\nhost = \"tracking.example.com\"\nport = \"*\"\n\n# ── PROCESS / SYSTEM / ENV ──────────────────────────────────────────────\n[process]\nallow_fork                 = true\nallow_exec                 = true\nallow_signal_self          = true\n# Block exec of sudo/su/doas/pkexec/runuser/visudo/sudoedit from inside\n# the sandbox. Useful when the host user has `NOPASSWD: ALL` sudoers or\n# cached credentials — without this, a compromised tool inside the\n# sandbox could re-exec through sudo and escape back to host-root. The\n# binary list covers the standard *nix paths (`/usr/bin/sudo`,\n# `/usr/sbin/visudo`, `/usr/libexec/doas`, …) and the common extras:\n# Homebrew (macOS), Linuxbrew and Snap (Linux), and `/usr/local/bin/…`\n# for locally-compiled installs. Implies `block_setid_syscalls`.\nblock_privilege_elevation  = false\n# Block the setuid-family syscalls (setuid/setgid/setreuid/setregid/\n# setresuid/setresgid/setfsuid/setfsgid/setgroups) via seccomp on Linux.\n# Defense against shellcode that tries to change credentials directly\n# without invoking a named elevation binary. Linux-only; macOS is\n# already prevented from honouring setuid bits inside the sandbox at\n# the kernel MAC layer.\nblock_setid_syscalls       = false\n# Memory W^X: forbid mprotect(..., PROT_EXEC) on any page that was\n# ever writable (Linux 6.3+, PR_SET_MDWE). Blocks the entire \"write\n# shellcode, flip to executable, jump to it\" exploit class. Breaks\n# JITs (V8, LuaJIT, Java HotSpot, PHP JIT, ...) — opt-in.\nno_w_x                     = false\n# Force-disable indirect branch speculation (Spectre v2) and\n# speculative store bypass (Spectre v4 / SSBD) for the sandboxed\n# process via PR_SET_SPECULATION_CTRL. Mitigates speculative side\n# channels reachable from inside the sandbox. Costs ~2-5% CPU. Opt-in.\nmitigate_spectre           = false\n\n[system]\nallow_sysctl_read = true\nallow_iokit       = false\nallow_ipc         = false\nallow_mach_all    = false     # macOS: broad; needed by browsers/Electron\nmach_services     = [\"com.apple.system.logger\"]\n\n[env]\npass_all = false\npass     = [\"PATH\", \"HOME\", \"LANG\"]\nset      = { }                # { KEY = \"value\" } to override\n\n# ── RESOURCE LIMITS (POSIX setrlimit + wall-clock watchdog) ─────────────\n[limits]\ncpu_seconds          = 60\nmemory_mb            = 1024\nfile_size_mb         = 100\nopen_files           = 512\nprocesses            = 64\nstack_mb             = 8\ncore_dumps           = false\nwall_timeout_seconds = 300\n\n# ── HARDWARE ACCESS ─────────────────────────────────────────────────────\n[hardware]\nusb    = true     # /dev/bus/usb + udev (Linux) / USB Mach services (macOS)\nserial = true     # /dev/tty* nodes\naudio  = true     # ALSA / PulseAudio (Linux), CoreAudio (macOS)\ngpu    = true     # /dev/dri (Linux), Metal (macOS)\ncamera = true     # V4L2 (Linux), AVFoundation (macOS)\n\n# ── IDENTITY SPOOFING (Linux fully, macOS limited) ──────────────────────\n[spoof]\ncpu_count        = 4\ncpuinfo_synth    = true\ncpuinfo_model    = \"CustomCPU 2.0\"\ncpuinfo_mhz      = 3200\nhostname         = \"rig-42\"\nmachine_id       = \"deadbeefcafe1234deadbeefcafe5678\"\nkernel_version   = \"Linux version 6.12.0-stable #1 SMP\"\nkernel_release   = \"6.12.0-stable\"\nos_release       = \"\"\"NAME=\"FleetOS\"\\nVERSION=\"2025.10\"\\nID=fleetos\\n\"\"\"\nissue            = \"Welcome to FleetOS\\n\"\nhostid_hex       = \"deadbeef\"\ntimezone         = \"Etc/UTC\"\nefi_platform_size = 64\nefi_enabled       = false   # hide /sys/firmware/efi entirely\ntemperature_c     = 42      # bind-mount millicelsius over all thermal/hwmon temps\n\n[spoof.dmi]\nproduct_serial = \"ABC123\"\nsys_vendor     = \"AcmeCo\"\nboard_name     = \"Fleetboard R7\"\n\n[[spoof.files]]\npath    = \"/sys/class/net/lo/address\"\ncontent = \"00:de:ad:be:ef:01\"\n\n# ── OVERLAY / WORKSPACE / MOCKS ─────────────────────────────────────────\n[workspace]\npath  = \"~/.sandkasten/work/${NAME}\"   # auto-created, added to rw,\n                                        # exposed as $SANDKASTEN_WORKSPACE\nchdir = true\n\n[overlay]                # Linux kernel ≥5.11 (unprivileged overlayfs)\nlower = \"/opt/myapp\"\nupper = \"~/.sandkasten/overlay/myapp\"\n# mount = \"/opt/myapp\"   ← default\n\n[mocks]                  # v1: content sidecar via $SANDKASTEN_MOCKS\nfiles = { \"config.json\" = '{\"api\":\"local\"}' }\n```\n\n### Built-in templates\n\n| template         | what it gives you                                                         |\n|------------------|---------------------------------------------------------------------------|\n| `self`           | **Default.** Read across `/`, read+write only `${CWD}`, hard-deny secrets |\n| `strict`         | Near-zero permissions — minimal base every dynamically-linked binary needs|\n| `minimal-cli`    | `strict` + `/usr/bin /bin /sbin /usr/local /opt` + CWD readable           |\n| `network-client` | `minimal-cli` + outbound TCP 80/443 + DNS + `$TMPDIR` + `/var/run/resolv.conf`. |\n| `dev`            | Permissive. Read `/`, write CWD/TMP, HTTPS/SSH/DNS + localhost. Denies user secrets. |\n| `browser`        | Chromium-family browsers (macOS + Linux). Pair with `--no-sandbox`.       |\n| `electron`       | Electron apps (VS Code, Slack, Discord, Obsidian, …). Grants write on `~/Library/Application Support` (macOS). |\n\n### Network presets\n\nNamed protocol/service bundles. Expand into concrete TCP/UDP outbound\nrules at profile-load time.\n\n| group     | presets                                                          |\n|-----------|------------------------------------------------------------------|\n| Web       | `http`, `https`, `quic`, `web`                                   |\n| Realtime  | `rtp`, `sip`, `stun`, `webrtc`                                   |\n| VPN       | `wireguard`, `wireguard-all-udp`, `openvpn`, `tailscale`, `ipsec`|\n| Remote    | `ssh`, `rdp`, `vnc`                                              |\n| Mail      | `smtp`, `smtps`, `imap`, `imaps`, `pop3`, `pop3s`                |\n| Files     | `ftp`, `ftps`, `sftp`, `git`                                     |\n| Auth      | `ldap`, `ldaps`, `kerberos`                                      |\n| Databases | `mysql`, `postgres`, `redis`, `memcached`, `mongodb`, `cassandra`, `elastic` |\n| Chat      | `irc`, `ircs`, `xmpp`, `matrix`, `mqtt`, `mqtts`                 |\n| Time      | `ntp`, `mdns`, `dhcp`, `dns`                                     |\n| Games     | `minecraft`, `minecraft-bedrock`, `steam`, `source-engine`, `quake3`, `teamspeak`, `discord-voice`, `riot-games` |\n| Diag      | `ping`, `tcpdump`, `pcap`, `wireshark`, `nmap`                   |\n\n## Web UI\n\n```\nsandkasten ui\n╭─ sandkasten UI ─────────────────────────────────────────\n│  http://127.0.0.1:46513/?t=\u003crandom-token\u003e\n│  profiles directory: ~/.config/sandkasten/profiles\n│  Ctrl-C to stop.\n╰─────────────────────────────────────────────────────────\n```\n\nBinds only to `127.0.0.1`. 128-bit random bearer token required on every\n`/api/*` request. Mutating requests (PUT/DELETE) additionally require the\n`Origin` header to match the bound host — belt-and-braces CSRF guard on\ntop of the token. Body size capped at 64 KB; path names restricted to\n`[a-zA-Z0-9_-]+`; writes confined to `~/.config/sandkasten/profiles/`.\nTight CSP, `X-Frame-Options: DENY`, `no-sniff`, `no-referrer`,\n`Permissions-Policy` disabling camera/mic/geo.\n\nFeatures: structured form per profile section, TOML tab for raw edit,\nfine-grained rule editor, client-side validation (paths, endpoints,\nenv names, Mach services), duplicate / save-as flow for built-in\ntemplates, non-system modal dialogs, toast notifications.\n\n**No `run` endpoint.** The UI edits profiles only — you launch them\nfrom your shell. Keeps the attack surface small.\n\n## Profile signing\n\nsandkasten verifies minisign ed25519 signatures — same format as\nJedisct1's `minisign` CLI (`brew install minisign`, `apt install\nminisign`).\n\n\u003e ⚠️ **Generate the key pair OUTSIDE any git working tree** and keep\n\u003e the private file (`sandkasten.key`) on disk only — never commit it.\n\u003e The repo's `.gitignore` denies `*.key` / `*.sec` / `*.pem` / `*.priv`\n\u003e by pattern as a backstop, but the right habit is to put the key in\n\u003e `~/.config/sandkasten/private/` (mode 0600) and only ever copy the\n\u003e `*.pub` half into source control if you publish trusted-key bundles.\n\n```sh\n# One-off key pair, generated in your home dir (NOT inside the repo):\nmkdir -p ~/.config/sandkasten/private \u0026\u0026 chmod 700 ~/.config/sandkasten/private\nminisign -G -p ~/.config/sandkasten/private/sandkasten.pub \\\n            -s ~/.config/sandkasten/private/sandkasten.key\n\n# Sign a profile (output: my.toml.minisig):\nminisign -Sm my.toml -s ~/.config/sandkasten/private/sandkasten.key\n\n# Install the public key as a trusted verifier:\nmkdir -p ~/.config/sandkasten/trusted_keys\ncp ~/.config/sandkasten/private/sandkasten.pub ~/.config/sandkasten/trusted_keys/\n\nsandkasten verify my.toml\n# → ok: my.toml verified against ~/.config/sandkasten/trusted_keys/sandkasten.pub\n\nsandkasten run --verify my.toml -- my-cmd\n# refuses to launch if the signature doesn't validate\n```\n\nBuilt-in templates ship inside the signed binary — they skip `--verify`.\n\n### Verifying the sandkasten binary itself\n\nDistinct from profile signing above: every release ships **sigstore\nkeyless signatures** (`*.sig` + `*.cert.pem`), **GitHub build\nprovenance** (SLSA), and a **CycloneDX SBOM** alongside the SHA-256\nhashes. See [SIGNING.md](SIGNING.md) for the full verification\nrecipe and what each layer actually proves.\n\n## Security model\n\n### What sandkasten enforces\n\n| layer          | macOS                              | Linux                                          |\n|----------------|------------------------------------|------------------------------------------------|\n| Filesystem     | Seatbelt / MACF (kernel)           | Landlock LSM (5.13+) + mount-ns bind-mounts    |\n| Network (L4)   | Seatbelt `network-outbound/inbound`| private netns (unshare) + nftables in-netns    |\n| Mach services  | `mach-lookup` predicate            | — (not applicable)                             |\n| Syscalls       | —                                  | seccomp-BPF deny-list                          |\n| Process        | fork inherits sandbox              | user+pid+ipc+uts namespaces                    |\n| Privilege      | inherited                          | `PR_SET_NO_NEW_PRIVS`, `PR_SET_DUMPABLE=0`     |\n| Resources      | `setrlimit`                        | `setrlimit`                                    |\n\n### Threat model — what it's for\n\n- **Untrusted code** (from strangers, the internet, third-party build\n  scripts, CI jobs) running as your user.\n- **Over-eager tools** — build systems, package managers, test runners\n  that might glob-delete or exfiltrate by accident.\n- **Credential hygiene.** Templates default-deny `~/.ssh`, `~/.aws`,\n  `~/.gnupg`, `~/.docker`, `~/.kube`, `~/.netrc`, `~/.password-store`,\n  macOS Keychains, the TCC database, shell history, mail, messages,\n  cookies, other browsers' profile dirs.\n\n### Threat model — what it is **not** for\n\nsandkasten is kernel-enforced process isolation built on primitives the\nOS already ships. It is not a virtual machine, a hypervisor, or a\nhardware isolation layer. Concretely out-of-scope:\n\n- **Kernel exploits.** Anything that breaks out of MACF / Landlock /\n  seccomp bypasses us too. If an attacker reaches a kernel bug through\n  an allowed syscall surface, the sandbox ends at that point. Enabling\n  `process.no_w_x` + `process.mitigate_spectre` + `block_privilege_elevation`\n  shrinks the reachable surface but doesn't close it.\n- **Root escalation.** If the target finds a way to host-root, the\n  sandbox ends. `PR_SET_NO_NEW_PRIVS` + capability bounding-set drop\n  + seccomp block of setuid family (via `block_setid_syscalls`) rule\n  out the usual suspects; novel kernel vulns are not in scope.\n- **Side-channel leakage.** Timing / power / cache-based covert\n  channels, transient-execution attacks (Spectre family, Meltdown,\n  L1TF, MDS, Downfall, GhostRace). `process.mitigate_spectre` turns on\n  the kernel's process-local mitigations for v2 + SSBD; everything\n  else is a host-level OS or firmware concern.\n- **Rowhammer / memory-fault injection.** Hardware-level bit flips\n  are orthogonal to any process sandbox. Mitigation is a BIOS / memory-\n  controller / DIMM problem.\n- **Covert channels over allowed outbound.** A profile that grants\n  outbound HTTPS permits DNS tunnelling, OCSP-stuffing, TLS-SNI\n  signalling, and every other \"legitimate connection with side data\"\n  trick. The sandbox enforces destinations and ports, not semantic\n  intent. Use `[network.proxy]` + an L7-filtering mitmproxy or\n  Squid if you need HTTP-method / URL / header filtering.\n- **Resource-exhaustion attacks against the host.** `[limits]` caps\n  CPU-seconds, memory, file-size, open files, processes, stack, and\n  wall-clock for the sandboxed process tree — but a profile that\n  doesn't set them defaults to OS-wide RLIMIT. Fork bombs, disk-fill\n  via `/tmp`, and ptrace-storms can still DOS the host if the profile\n  doesn't set `limits.processes` / `limits.file_size_mb` / similar.\n- **TOCTOU windows on path-based rules.** macOS SBPL and Linux\n  Landlock both resolve paths at access time — an attacker who wins\n  a race between \"sandkasten built the ruleset\" and \"target opens the\n  path\" can exploit symlink swaps for files outside the sandbox's\n  view. We mitigate by opening Landlock `PathFd`s before fork and\n  by blocking hardlink/symlink creation via seccomp; we don't eliminate\n  the class.\n- **Landlock \"deny-inside-allow\" enforcement.** Landlock is\n  allow-list only: a `deny` path that sits inside an enclosing\n  `read` / `read_write` subtree can't be enforced on Linux. macOS\n  SBPL supports true deny-overrides. `sandkasten run` warns at\n  `-v` when a deny is unenforceable.\n- **Airtight hardware-identity hiding.** `[spoof]` replaces user-space\n  views of `/proc`, `/sys`, `/etc/*` — it does not patch the `CPUID`\n  instruction, `uname(2)` syscall fields the kernel fills,\n  `_SC_NPROCESSORS_ONLN` (which reads `/sys/devices/system/cpu/online`\n  unless `num_cpus`-style libraries honour affinity, which most do),\n  or userland that reads `/dev/kmsg`. It's a faithful view for most\n  tools; it's not a VM.\n- **Compromise of the build chain that produced the sandkasten binary\n  itself.** Supply-chain hardening (SBOM, SLSA provenance, signed\n  releases) covers the tarballs we publish; users who build from\n  source inherit the integrity of their toolchain and crate cache.\n  `cosign verify-blob` against the public key in `SIGNING.md`\n  proves authenticity of a downloaded release artifact.\n- **Correctness of the GUI / Web UI profile editors.** The structured\n  editors in `swift-ui/` and `src/ui/` emit TOML that's then parsed\n  by the normal config loader — they can emit policies that don't\n  match user intent if there's a UI bug. Always confirm with\n  `sandkasten render` + `sandkasten explain` before trusting a\n  profile generated interactively.\n\n### Anti-breakout measures\n\n- **`PR_SET_NO_NEW_PRIVS`** blocks setuid-elevation from within the sandbox.\n- **`PR_SET_DUMPABLE=0`** disables core dumps (no memory spill on\n  crash) and makes the process non-ptrace-attachable from peers.\n- Seccomp deny-list includes `link`/`linkat`/`symlink`/`symlinkat`\n  (hardlink-into-writable-area escape), `name_to_handle_at` /\n  `open_by_handle_at` (reopen via handle across mount ns), `io_uring_*`\n  (high-churn attack surface), `userfaultfd`, clock-manipulation\n  syscalls, kernel-admin syscalls (mount / pivot_root / chroot /\n  unshare / setns / reboot / module ops), `ptrace` and process-memory\n  introspection, `keyctl` / `add_key` / `request_key`,\n  `perf_event_open`, `bpf`, NUMA memory-move primitives.\n- Landlock writes are path-based; hardlink creation is blocked so an\n  attacker can't pull a denied file into the writable area.\n\n## Limits\n\nShipped honestly — nothing hidden.\n\n1. **macOS `sandbox_init` is SPI.** Undocumented by Apple but stable in\n   practice — the mechanism every sandboxed macOS browser uses.\n2. **Modern macOS Seatbelt grammar** rejects IP literals and specific\n   hostnames in `remote tcp/udp` — only `localhost` and `*` are\n   accepted. sandkasten widens specific-host rules to `*:PORT` with an\n   explicit NOTE in the rendered policy. Per-IP outbound filtering on\n   macOS needs a userspace proxy.\n3. **macOS kernel denial capture** (the `-vvv` post-run summary) only\n   surfaces default-deny fallthroughs — explicit `(deny …)` rules are\n   silent by design in Seatbelt.\n4. **Landlock is allow-list only.** A `deny` inside a broader allow\n   emits a warning and is not enforced on Linux; narrow the allow\n   instead.\n5. **Linux network plumbing.** A fresh netns has no interfaces beyond\n   `lo`, so for outbound profiles sandkasten auto-detects and uses\n   `pasta` (from the `passt` package) or `slirp4netns` to bridge the\n   private netns to the host network. `nftables` rules then enforce\n   per-IP policy inside the plumbed netns without touching the host.\n   If neither tool is installed (or `pasta` is AppArmor-confined on\n   Debian/Ubuntu, which we detect), sandkasten falls back to sharing\n   the host netns — internet still works, but per-IP filtering is\n   not kernel-enforced. `sandkasten render \u003cprofile\u003e` names the\n   active mode explicitly.\n6. **Mock mode v1 is a content sidecar.** `[mocks.files]` materialises\n   to `$SANDKASTEN_MOCKS`. Transparent path interposition (so a\n   program opening `/etc/hostname` reads the mock without\n   cooperation) requires an LD_PRELOAD / DYLD_INSERT_LIBRARIES shim —\n   planned.\n7. **FreeBSD support is not shipped.** Unprivileged full-kernel\n   sandboxing on FreeBSD really does require jail(2) + root.\n8. **Overlay + Landlock interaction.** Overlayfs mounts cleanly in a\n   user namespace, but Landlock's pre-opened PathFds may target\n   the lower-layer inode rather than the merged inode on some\n   kernels. Auto-adding the mount-point path to `read_write` works on\n   recent 6.x kernels; on older ones writes may still see EACCES.\n\n## Disclaimer\n\n**sandkasten is provided AS-IS, without warranty of any kind,** express\nor implied, including but not limited to merchantability, fitness for a\nparticular purpose, and non-infringement. In no event shall the authors\nbe liable for any claim, damages, or other liability, whether in an\naction of contract, tort, or otherwise, arising from, out of, or in\nconnection with the software or its use.\n\n**Use on systems and against data you are authorised to operate on.** The\nnetwork-filtering, redirection, packet-capture, identity-spoofing, and\ntracing features are offered for legitimate use — sandboxing untrusted\ncode on your own machines, testing compatibility with custom identities\nin environments you control, hardening SSH sessions on hosts you\nadminister, and similar. Deploying them against systems without\nauthorisation, circumventing licence enforcement, impersonating\ncustomers or users, or concealing the provenance of network traffic for\nthe purpose of abuse is explicitly not supported and may violate local\nlaw. The authors accept no responsibility for misuse.\n\n**sandkasten is not a substitute for a formally reviewed security\nproduct.** Kernel vulnerabilities bypass MACF, Landlock and seccomp.\nSide channels are not addressed. `[spoof]` presents a plausible\nuser-space view, not a virtualised environment; determined\nfingerprinting will still identify the real host via unspoofed\nchannels (CPUID instruction, TSC behaviour, unspoofed `/proc`/`/sys`\nentries, GPU capabilities, network RTT, etc.).\n\n## License\n\nDual-licensed under **MIT** or **Apache-2.0** at your option.\n\n- [`LICENSE-MIT`](LICENSE-MIT)\n- [`LICENSE-APACHE`](LICENSE-APACHE)\n\n## Roadmap\n\n- [x] Resource limits, `--timeout`, `PR_SET_NO_NEW_PRIVS`\n- [x] Profile signing (minisign verify before apply)\n- [x] Per-IP outbound on Linux via nftables inside the netns\n- [x] DNS override + `/etc/hosts` pinning (transparent on Linux via\n      bind-mount; sidecar on macOS)\n- [x] Persistent `[workspace]` + Linux `[overlay]` + `sandkasten snap`\n- [x] `[spoof]` — CPU, DMI, machine-id, kernel identity, thermal, EFI,\n      arbitrary `[[spoof.files]]` bind-mounts\n- [x] `[hardware]` — USB / serial / audio / GPU / camera presets\n- [x] `[[filesystem.rewire]]`, `[[filesystem.hide]]`\n- [x] Protocol coverage: SCTP / DCCP / UDPLite + 35 service presets\n      including WireGuard, Tailscale, Steam, Minecraft, Riot, etc.\n- [x] `sandkasten shell / sshd / diff / explain / doctor / snap`\n- [x] Reproducibility fingerprint in `render`\n- [x] End-to-end Linux smoke test in CI\n- [x] Bundled `pasta` / `slirp4netns` auto-integration for turnkey\n      Linux outbound, with per-IP nftables filtering enforced inside\n      the plumbed netns; AppArmor-aware fallback to host netns.\n- [x] Homebrew tap published at `DatanoiseTV/sandkasten`; prebuilt\n      per-arch binaries (~2 s install, no Rust build-dep).\n- [x] Always-on TIOCSTI seccomp block (ioctl-arg conditional deny).\n- [x] Opt-in `process.no_w_x` (PR_SET_MDWE memory W^X) and\n      `process.mitigate_spectre` (PR_SET_SPECULATION_CTRL for\n      Spectre v2 + SSBD) on Linux.\n- [x] `process.block_privilege_elevation` + `process.block_setid_syscalls`\n      (sudo/su/doas/pkexec exec deny across macOS + Linux + Homebrew +\n      Linuxbrew + Snap; seccomp setid-family deny).\n- [x] `sandkasten learn --yes` non-interactive capture for scripts / CI.\n- [x] Weekly Dependabot-grouped dependency updates (cargo + swift +\n      github-actions).\n- [ ] Transparent mock interposition via `LD_PRELOAD` /\n      `DYLD_INSERT_LIBRARIES`.\n- [ ] Live policy reload (SIGHUP → re-apply; sandbox_init only narrows).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatanoisetv%2Fsandkasten","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdatanoisetv%2Fsandkasten","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatanoisetv%2Fsandkasten/lists"}