sboxd 0.1.4

Policy-driven command runner for sandboxed dependency installation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# sbox

`sbox` is a policy-driven command runner that executes development commands inside a rootless Podman or Docker sandbox.

The threat it addresses: **malicious postinstall scripts**. Code that runs automatically during `npm install`, `pip install`, or `cargo build` can read `~/.ssh/id_rsa`, dump `AWS_SECRET_ACCESS_KEY` from the environment, or exfiltrate your source to an attacker's server. sbox runs those commands in an isolated container with no access to host credentials, no network unless you explicitly allow it, and a read-only workspace.

The March 31 2026 Axios npm supply chain attack (Sapphire Sleet RAT delivered via postinstall hook) is exactly the scenario sbox is built to contain. An `npm install` inside a sbox sandbox with `network: off` and credential masking would have blocked that payload entirely.

## Platform support

| Platform | Status |
|----------|--------|
| Linux + Podman | **First-class** — rootless, no root required |
| Linux + Docker | **Supported** — same feature set, requires Docker socket access |
| macOS + Docker Desktop | **Partial** — containers run in a Linux VM; most features work, `keep-id` user mapping may differ |
| macOS + Podman Machine | **Partial** — similar to Docker Desktop |
| Windows | **Not supported** |

## Status

v1 and v2 are both complete. Current implemented scope:

- `init`, `run`, `exec`, `shell`, `plan`, `doctor`, `clean`, and `shim`
- Podman and Docker backends for sandbox execution
- Reusable Podman/Docker sessions when enabled
- Security validation: dangerous mounts, sensitive env pass-through, lockfile checks
- `--strict-security` and `runtime.strict_security: true`
- Per-profile and global `require_pinned_image` enforcement
- Image digest pinning and real signature verification via `skopeo` + containers policy
- Package-manager-agnostic policy: `role`, `pre_run`, `lockfile_files` on profiles
- Outbound network domain allow-listing with glob/regex pattern support
- Backend auto-detection when `runtime.backend` is not set
- Transparent shim interception for `npm`, `pnpm`, `yarn`, `bun`, `uv`, `pip`, `poetry`, `cargo`, and more

## Installation

**From crates.io:**

```bash
cargo install sboxd
```

**Pre-built binaries** (Linux x86_64 and aarch64) are attached to each [GitHub Release](../../releases):

```bash
curl -fsSL https://github.com/Aquilesorei/sboxd/releases/latest/download/sbox-linux-x86_64 -o ~/.local/bin/sbox
chmod +x ~/.local/bin/sbox
```

**From source:**

```bash
git clone https://github.com/Aquilesorei/sboxd
cd sbox
cargo install --path .
```

## Documentation

Start with **[How it works](docs/how-it-works.md)** — it explains bind mounts, what the sandbox actually isolates, and why the network situation is complicated. Everything else makes more sense after that.

- [How it works]docs/how-it-works.md — the mental model
- [Getting started]docs/getting-started.md — install and first run
- [Progressive adoption]docs/adoption.md — add sbox to an existing project one step at a time
- [Ecosystem guides]docs/ecosystems.md — Node.js, Python, Rust, Go
- [Network security]docs/network.md`network: off`, `network_allow`, two-phase installs
- [Security model]docs/security.md — what is blocked, what is not, adversarial test results
- [Shims]docs/shims.md — transparent interception for npm, pip, cargo, and others
- [Recipes]docs/recipes.md — CI pipelines, private registries, reusable sessions
- [Troubleshooting]docs/troubleshooting.md — common errors and fixes
- [Config reference]docs/config.md — full `sbox.yaml` field reference
- [Architecture]docs/architecture.md — internals and contribution guide

## Quick Start

Generate a config from a preset (recommended):

```bash
sbox init --preset node      # npm  — node:22-bookworm-slim
sbox init --preset python    # uv   — python:3.13-slim
sbox init --preset rust      # cargo — rust:1-bookworm
sbox init --preset go        # go   — golang:1.23-bookworm
sbox init --preset generic   # blank — ubuntu:24.04
```

Or use the interactive wizard (arrow-key menus):

```bash
sbox init --interactive
# → "simple" path: picks PM + image, writes package_manager: config
# → "advanced" path: full manual profiles and dispatch rules
```

Inspect the resolved policy before running anything:

```bash
sbox plan -- uv sync
```

Run a command in the resolved environment:

```bash
sbox run -- uv sync
```

Run a command against a specific profile:

```bash
sbox exec deps -- uv sync
```

Open an interactive shell:

```bash
sbox shell
```

Check backend and policy health:

```bash
sbox doctor
```

Remove current-workspace reusable sessions:

```bash
sbox clean
```

### Transparent interception with shims

`sbox shim` generates thin wrapper scripts for common package managers. When one of these wrappers is called from a directory that has an `sbox.yaml`, it transparently delegates to `sbox run`. Otherwise it calls the real binary unchanged.

```bash
# Install shims to ~/.local/bin (must appear before real binaries in PATH)
sbox shim

# Or specify a directory
sbox shim --dir ~/bin

# Preview without writing anything
sbox shim --dry-run
```

Then add to your shell profile:

```bash
export PATH="$HOME/.local/bin:$PATH"
```

After this, `npm install`, `uv sync`, `bun install`, etc. are automatically sandboxed in any project that has an `sbox.yaml`.

## Security Model

The default direction is:

- prefer rootless Podman
- network off in sandbox profiles by default
- only pass through host env vars explicitly configured
- reject dangerous bind mounts: container sockets, `.ssh`, `.aws`, `.kube`, `.npmrc`, and similar
- do not mount the home directory silently
- keep dependency state outside the host workspace unless the user explicitly opts in

### Strict mode

```bash
sbox --strict-security run -- node --version
```

or:

```yaml
runtime:
  strict_security: true
```

Strict mode refuses sandbox execution if:
- sensitive host variables are being passed through
- install-style commands run without a lockfile present
- the image is not pinned to a digest

### Image trust

Pin an image globally:

```yaml
image:
  ref: node:22-bookworm-slim
  digest: sha256:3efebb4f5f2952af4c86fe443a4e219129cc36f90e93d1ea2c4aa6cf65bdecf2
```

Require a pinned image for all sandbox profiles globally:

```yaml
runtime:
  require_pinned_image: true
```

Require it only for a specific profile:

```yaml
profiles:
  install:
    mode: sandbox
    require_pinned_image: true
```

When `require_pinned_image` is set, sbox enforces this at config-load time and refuses execution if the image has no digest. It also integrates with `skopeo` for real signature verification:

```yaml
image:
  ref: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
  digest: sha256:...
  verify_signature: true
```

- `digest` pins the image reference and is enforced at resolve time
- `verify_signature: true` is a real runtime check via `skopeo` — not metadata
- verification requires a containers policy that actually enforces signatures; a policy using `insecureAcceptAnything` does not count

`sbox doctor` reports whether signature verification is usable on the current machine.

### Network allow-listing

Profiles with `network: on` can restrict outbound DNS to a specific set of domains:

```yaml
profiles:
  install:
    mode: sandbox
    network: on
    network_allow:
      - registry.npmjs.org
      - "*.npmjs.org"
      - ".*\\.yarnpkg\\.com"
```

Three entry forms are supported:

| Form | Example | Behavior |
|------|---------|----------|
| Exact hostname | `registry.npmjs.org` | DNS-resolved to IPs, injected as `--add-host` |
| Glob prefix | `*.npmjs.org` | Base domain `npmjs.org` resolved, pattern stored for display |
| Regex prefix | `.*\.npmjs\.org` | Same as glob — base domain unescaped and resolved |

Enforcement works by pointing the container's DNS at a non-routable address (`192.0.2.1`) so arbitrary lookups time out, while injecting resolved IPs directly into `/etc/hosts` via `--add-host`. Raw IP connections bypass this; package managers use domain names.

For glob/regex patterns, sbox expands the base domain to the full set of known subdomains before resolving. `*.npmjs.org` resolves `registry.npmjs.org`, `npmjs.org`, and `www.npmjs.org` — not just `npmjs.org` alone. Built-in expansion tables cover:

- npm/yarn: `npmjs.org`, `yarnpkg.com`
- Python: `pypi.org`, `pythonhosted.org`
- Rust: `crates.io`
- Go: `golang.org`, `go.dev`
- Ruby: `rubygems.org`
- Maven/Gradle: `maven.org`, `gradle.org`
- GitHub: `github.com`, `githubusercontent.com`
- OCI registries: `docker.io`, `ghcr.io`, `gcr.io`

For unknown base domains only the base itself is resolved.

`sbox plan` shows the resolved state:

```
  network_allow: [resolved] registry.npmjs.org, npmjs.org, www.npmjs.org; [patterns] *.npmjs.org
```

### Install-style policy

Profiles declare their role explicitly rather than relying on command-pattern detection:

```yaml
profiles:
  install:
    mode: sandbox
    role: install
    lockfile_files:
      - package-lock.json
      - npm-shrinkwrap.json
    pre_run:
      - npm audit --audit-level=high
    require_lockfile: true
    require_pinned_image: true
```

- `role: install` — marks this profile as install-style; enables lockfile auditing
- `lockfile_files` — which files to check for presence before running
- `pre_run` — shell commands run on the **host** before the sandboxed command; if any fails, execution is refused
- `require_lockfile: true` — refuses install-style commands in strict mode unless a lockfile is present

### Credential masking

Credentials that exist in the workspace can be masked from the container using `/dev/null` bind mounts:

```yaml
workspace:
  exclude_paths:
    - .env
    - .env.local
    - "*.pem"
    - "*.key"
    - .npmrc
    - .netrc
```

Each matched file is replaced with a read-only `/dev/null` mount inside the container. This prevents postinstall scripts from reading secrets that happen to live in the project directory.

## Backend selection

sbox uses Podman by default. To use Docker:

```yaml
runtime:
  backend: docker
```

When `runtime.backend` is omitted, sbox probes PATH at execution time: Podman is preferred if available, Docker otherwise.

## `sbox.yaml` reference

The shortest working config uses `package_manager:` to generate all profiles automatically:

```yaml
version: 1

workspace:
  mount: /workspace
  writable: false
  exclude_paths:
    - .env
    - .npmrc
    - ".ssh/*"
    - ".aws/*"

image:
  ref: node:22-bookworm-slim

package_manager:
  name: npm
```

sbox infers the rest: install profile (network on, registry only, lockfile writable), build profile (network off, dist writable), and a locked-down default for everything else. No profiles or dispatch rules to write.

For full control, use explicit profiles:

```yaml
profiles:
  install:
    mode: sandbox
    network: on
    network_allow:
      - registry.npmjs.org
    writable: false
    writable_paths:
      - node_modules
      - package-lock.json
    role: install
    lockfile_files:
      - package-lock.json

  default:
    mode: sandbox
    network: off
    writable: false
    writable_paths: []

dispatch:
  npm-install:
    match:
      - npm install
      - npm ci
    profile: install
```

Top-level keys:

| Key | Description |
|-----|-------------|
| `version` | Must be `1` |
| `runtime` | Backend, rootless mode, reuse settings, image trust policy |
| `workspace` | Host root, container mount path, writable policy, credential exclusions |
| `image` | Image reference, digest, build recipe, signature policy |
| `identity` | User/group mapping |
| `environment` | Env var pass-through, explicit set, deny list |
| `mounts` | Extra bind or tmpfs mounts |
| `caches` | Named persistent volumes |
| `secrets` | Host files mounted read-only into the container |
| `profiles` | Execution policies indexed by name |
| `dispatch` | Command pattern → profile routing rules |
| `package_manager` | Zero-config shortcut — generates profiles and dispatch automatically from a preset |

## Examples

Repository examples:

- [sbox.yaml]sbox.yaml: `uv`-based Python example with isolated cache and environment
- [examples/python-smoke/reuse-sbox.yaml]examples/python-smoke/reuse-sbox.yaml: reusable Python sandbox session
- [examples/npm-smoke/sbox.yaml]examples/npm-smoke/sbox.yaml: npm with isolated cache, install prefix, artifact storage, and network allow-list
- [examples/bun-smoke/sbox.yaml]examples/bun-smoke/sbox.yaml: bun with lockfile-aware install policy and preflight audit
- [examples/poetry-smoke/sbox.yaml]examples/poetry-smoke/sbox.yaml: poetry with isolated cache and virtualenv paths

## Post-install artifact risk

`sbox` isolates the install step. Once the sandbox exits, installed artifacts (`node_modules`, `.venv`, built binaries) live on the host. Any subsequent invocation outside of `sbox` — running `node`, `npx`, `python`, a script from `node_modules/.bin`, etc. — executes that code with full host privileges.

**Option 1 — Route all execution through sbox**

Add a `default` profile with `network: off` and route every project command through it:

```bash
sbox run -- npm start
sbox run -- node server.js
```

**Option 2 — Keep dependencies out of the workspace entirely**

Redirect package manager output to cache volumes so nothing lands in the workspace:

```yaml
environment:
  set:
    npm_config_prefix: /var/tmp/sbox/npm-prefix

caches:
  - name: npm-prefix
    target: /var/tmp/sbox/npm-prefix
```

Equivalent patterns exist for uv (`UV_PROJECT_ENVIRONMENT`), poetry (`POETRY_VIRTUALENVS_PATH`), and bun (`BUN_INSTALL_CACHE_DIR`).

## Fedora / Podman Signature Setup

On Fedora, Podman reads signature policy from `~/.config/containers/policy.json` or `/etc/containers/policy.json`. The workstation default is too permissive for `sbox` signature enforcement.

Example files are in [examples/fedora-podman-signature-policy/](examples/fedora-podman-signature-policy/).

```bash
mkdir -p ~/.config/containers/registries.d
cp examples/fedora-podman-signature-policy/policy.json ~/.config/containers/policy.json
cp examples/fedora-podman-signature-policy/registries.d/example.yaml ~/.config/containers/registries.d/example.yaml
```

Replace the placeholder registry scope, GPG key path, and lookaside URL with real values, then run `sbox doctor` to verify.

## Contact

Achille Zongo — achillezongo07@gmail.com