> ## Documentation Index
> Fetch the complete documentation index at: https://docs.boxlite.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Inject secrets and harden a box

> Deliver credentials to a sandbox without letting the sandbox see them, and tighten the isolation boundary so untrusted code runs safely.

Two independent capabilities, usually used together. **`Secret`** keeps the credential on the host: sandbox code writes a placeholder such as `<BOXLITE_SECRET:openai>`, and the host proxy substitutes the real value on the way out. **`SecurityOptions`** hardens the OS-level sandbox around the VM — jailer, seccomp, resource limits.

## Quick Example

### Inject a credential (Secret)

The following calls an external API from inside the sandbox: the in-sandbox script only knows the placeholder `<BOXLITE_SECRET:demo>`, while the real credential `<YOUR_API_KEY>` stays on the host and is injected by the proxy on egress.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio

# SimpleBox is the recommended high-level entry point (async context manager).
# Secret is exported at the top level; secrets= is passed through to the underlying BoxOptions via **kwargs.
from boxlite import SimpleBox, Secret

async def main() -> None:
    secret = Secret(
        name="demo",                       # the placeholder defaults to <BOXLITE_SECRET:demo>
        value="<YOUR_API_KEY>",            # TODO: replace with a real credential; it exists only on the host side
        hosts=["api.example.com"],         # injected only when requesting these hosts (supports *.example.com wildcards)
    )

    try:
        async with SimpleBox(image="python:slim", secrets=[secret]) as box:
            # the in-guest script uses the placeholder, not the real key.
            # on egress to api.example.com, the host proxy replaces the placeholder with the real value.
            script = (
                "import urllib.request\n"
                "req = urllib.request.Request(\n"
                "    'https://api.example.com/v1/ping',\n"
                "    headers={'Authorization': 'Bearer <BOXLITE_SECRET:demo>'},\n"
                ")\n"
                "print(urllib.request.urlopen(req, timeout=10).status)\n"
            )
            result = await box.exec("python", "-c", script)
            # a non-zero exit code does not raise; check exit_code yourself.
            if result.exit_code != 0:
                print("command failed:", result.stderr)
            else:
                print(result.stdout)
    except RuntimeError as e:
        # image pull failure / startup failure with no virtualization both raise a standard RuntimeError.
        print("box failed to start:", e)

if __name__ == "__main__":
    asyncio.run(main())
```

> Note: `api.example.com` above is a **placeholder host** with no `/v1/ping` endpoint, so running this as-is yields a non-zero `exit_code`. To verify that injection works, replace both `hosts` and the request target with a real, reachable HTTPS service that echoes request headers, and enable network egress. The following is a minimal verification (using `httpbin.org/headers` to echo the `Authorization` header):

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio
import uuid

import boxlite
from boxlite import Secret

REAL_TOKEN = "<YOUR_API_TOKEN>"  # <-- replace; stays on the host, never enters the box

async def main() -> None:
    secret = Secret(name="testkey", value=REAL_TOKEN, hosts=["httpbin.org"])
    runtime = boxlite.Boxlite.default()
    box = None
    try:
        box = await runtime.create(
            boxlite.BoxOptions(
                image="alpine:latest",
                auto_remove=False,
                # injection requires egress through the proxy: enable the network and allow the target host
                network=boxlite.NetworkSpec(mode="enabled", allow_net=["httpbin.org"]),
                secrets=[secret],
            ),
            name=f"secret-demo-{uuid.uuid4().hex[:8]}",
        )
        # in the guest, BOXLITE_SECRET_TESTKEY is the placeholder; the real value is not in env
        ex = await box.exec(
            "wget",
            [
                "-q", "-O-",
                "--header", "Authorization: Bearer <BOXLITE_SECRET:testkey>",
                "https://httpbin.org/headers",
            ],
        )
        body = "".join([line async for line in ex.stdout()])
        result = await ex.wait()
        # the Authorization header in the body is the real value (the placeholder was replaced by the proxy
        # on egress; the real value never entered the guest).
        print("exit:", result.exit_code, "real value reached upstream:", REAL_TOKEN in body)
    finally:
        if box is not None:
            try:
                await box.stop()
            except Exception:
                pass
            try:
                await runtime.remove(box.id, force=True)
            except Exception:
                pass
        runtime.close()

if __name__ == "__main__":
    asyncio.run(main())
```

Output excerpt (httpbin echo; you can see the placeholder has been replaced by the real value):

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{ "headers": { "Authorization": "Bearer <the real value you passed to Secret(value=...)>", "Host": "httpbin.org", ... } }
```

> Note: the guest also gets an environment variable `BOXLITE_SECRET_TESTKEY=<BOXLITE_SECRET:testkey>` injected automatically (the placeholder, **not** the real value), which you can verify with `printenv BOXLITE_SECRET_TESTKEY`. The real value is not found in `env`, confirming that the real credential never enters the guest. The variable key is derived from the secret name as `BOXLITE_SECRET_{NAME}` (uppercased, with non-alphanumeric characters replaced by `_`).

### Tighten isolation (SecurityOptions)

For untrusted workloads, use the `maximum()` preset to enable all isolation. Note: `security` must be passed through `advanced=AdvancedBoxOptions(...)`; **there is no top-level `security=` keyword**, and `AdvancedBoxOptions` is exported only under the `boxlite.boxlite` submodule.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio

from boxlite import SimpleBox, SecurityOptions
# Note: AdvancedBoxOptions is not exported at the top level; it must be imported from boxlite.boxlite.
from boxlite.boxlite import AdvancedBoxOptions

async def main() -> None:
    advanced = AdvancedBoxOptions(security=SecurityOptions.maximum())

    try:
        # advanced= is passed through to the underlying BoxOptions via SimpleBox's **kwargs.
        async with SimpleBox(image="alpine:latest", advanced=advanced) as box:
            result = await box.exec("id")
            if result.exit_code != 0:
                print("command failed:", result.stderr)
            else:
                print(result.stdout)
    except RuntimeError as e:
        print("box failed to start:", e)

if __name__ == "__main__":
    asyncio.run(main())
```

### Combining the two (credentials plus isolation)

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
import asyncio

from boxlite import SimpleBox, Secret, SecurityOptions
from boxlite.boxlite import AdvancedBoxOptions

async def main() -> None:
    secret = Secret(
        name="demo",
        value="<YOUR_API_KEY>",  # TODO: replace with a real credential
        hosts=["api.example.com"],
    )
    advanced = AdvancedBoxOptions(security=SecurityOptions.maximum())

    try:
        async with SimpleBox(
            image="python:slim",
            secrets=[secret],
            advanced=advanced,
        ) as box:
            result = await box.exec("echo", "secrets + hardening ready")
            print(result.stdout if result.exit_code == 0 else result.stderr)
    except RuntimeError as e:
        print("box failed to start:", e)

if __name__ == "__main__":
    asyncio.run(main())
```

### Node equivalent

In the Node SDK, `secrets` and `security` are both direct fields of `SimpleBoxOptions` (there is no `advanced` wrapper layer), and `SecurityOptions` is expressed as a plain object (there is no `maximum()` static preset).

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
// the package name is @boxlite-ai/boxlite (not 'boxlite'). Pure ESM.
import { SimpleBox } from "@boxlite-ai/boxlite";

async function main(): Promise<void> {
  try {
    // SimpleBox implements AsyncDisposable; use `await using` for automatic release.
    await using box = new SimpleBox({
      image: "python:slim",
      secrets: [
        {
          name: "demo",
          value: "<YOUR_API_KEY>", // TODO: replace with a real credential
          hosts: ["api.example.com"],
          // when placeholder is omitted, it defaults to <BOXLITE_SECRET:demo>
        },
      ],
      // Node has no maximum() preset; pass the object directly (seccomp takes effect on Linux only).
      security: {
        jailerEnabled: true,
        seccompEnabled: true,
        maxOpenFiles: 1024,
        maxProcesses: 100,
        closeFds: true,
      },
    });

    const result = await box.exec("echo", "secrets + hardening ready");
    console.log(result.exitCode === 0 ? result.stdout : result.stderr);
  } catch (err) {
    // a missing command / pull failure throws a bare Error, not necessarily BoxliteError.
    console.error("box failed:", err);
  }
}

main();
```

## Parameters and Returns

### `Secret(name, value, hosts=[], placeholder=None)`

Constructor signature: see `sdks/python/src/options.rs`.

| Parameter     | Type          | Required                    | Description                                                                                                                                                                                                             |
| ------------- | ------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`        | `str`         | Required                    | Human-readable name for the credential (such as `"openai"`). When `placeholder` is not set explicitly, the placeholder is derived as `<BOXLITE_SECRET:{name}>`.                                                         |
| `value`       | `str`         | Required                    | The real credential value. **It exists only on the host side and never enters the guest VM.**                                                                                                                           |
| `hosts`       | `list[str]`   | **Set this** (default `[]`) | Restrict injection to these hosts. Exact matches (`api.openai.com`) and wildcards (`*.openai.com`). **Leaving it empty places no host restriction on injection** — with untrusted code in the sandbox, always scope it. |
| `placeholder` | `str \| None` | Optional (default `None`)   | The placeholder string that appears in guest HTTP headers. Defaults to `<BOXLITE_SECRET:{name}>`.                                                                                                                       |

Injection entry point: `BoxOptions(secrets=[Secret(...)])`, or passed through via `SimpleBox(..., secrets=[...])`. Node uses `{ name, value, hosts?, placeholder? }`.

### `SecurityOptions` (Python)

Preset static methods (`sdks/python/src/advanced_options.rs`):

| Preset                          | When to use                                          | jailer             | seccomp      | Resource limits | close\_fds |
| ------------------------------- | ---------------------------------------------------- | ------------------ | ------------ | --------------- | ---------- |
| `SecurityOptions.development()` | Debugging (when isolation interferes with diagnosis) | off                | off          | none            | off        |
| `SecurityOptions.standard()`    | Most cases (recommended)                             | on for Linux/macOS | on for Linux | none            | on         |
| `SecurityOptions.maximum()`     | Untrusted workloads (AI sandboxes, multi-tenant)     | on                 | on for Linux | see below       | on         |

> Note: there is no `SecurityOptions.minimum()`; the weak-isolation preset is named `development()`.

Resource limits set by `maximum()`: `max_open_files=1024`, `max_file_size=1 GiB`, `max_processes=100` (`max_memory`/`max_cpu_time` are left to the VM configuration).

Custom constructor parameters (defaults per the source signature):

| Parameter         | Type          | Default | Description                                                                 |
| ----------------- | ------------- | ------- | --------------------------------------------------------------------------- |
| `jailer_enabled`  | `bool`        | `False` | Enable jailer isolation (Linux/macOS).                                      |
| `seccomp_enabled` | `bool`        | `False` | Enable seccomp syscall filtering (**Linux only**).                          |
| `max_open_files`  | `int \| None` | `None`  | Maximum number of open file descriptors.                                    |
| `max_file_size`   | `int \| None` | `None`  | Maximum file size (bytes).                                                  |
| `max_processes`   | `int \| None` | `None`  | Maximum number of processes.                                                |
| `max_memory`      | `int \| None` | `None`  | Maximum virtual memory (bytes).                                             |
| `max_cpu_time`    | `int \| None` | `None`  | Maximum CPU time (seconds).                                                 |
| `network_enabled` | `bool`        | `True`  | Whether network access is allowed in the sandbox (**macOS-relevant only**). |
| `close_fds`       | `bool`        | `True`  | Close file descriptors inherited from the host.                             |

### Attaching `SecurityOptions` to a box

| Entry point                     | Form                                                                                                                        |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Python (recommended high-level) | `SimpleBox(image=..., advanced=AdvancedBoxOptions(security=SecurityOptions.maximum()))`                                     |
| Python (lower-level BoxOptions) | `BoxOptions(image=..., advanced=AdvancedBoxOptions(security=...))`                                                          |
| Node                            | `new SimpleBox({ image, security: { jailerEnabled, seccompEnabled, ... } })` (no `advanced` wrapper, no `maximum()` preset) |

`AdvancedBoxOptions(security=None, health_check=None)`: exported only under the `boxlite.boxlite` submodule; `import boxlite; boxlite.AdvancedBoxOptions` at the top level is not available.

### `box.exec(...)` return value (`ExecResult`)

| Field           | Type          | Description                                                                |
| --------------- | ------------- | -------------------------------------------------------------------------- |
| `exit_code`     | `int`         | Process exit code. A non-zero value **does not raise**; check it yourself. |
| `stdout`        | `str`         | Standard output.                                                           |
| `stderr`        | `str`         | Standard error.                                                            |
| `error_message` | `str \| None` | Non-`None` only when the process died abnormally.                          |

## Troubleshooting

### `AttributeError: module 'boxlite' has no attribute 'AdvancedBoxOptions'`

`AdvancedBoxOptions` lives in the `boxlite.boxlite` submodule, not the top level.

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
# Wrong
# from boxlite import AdvancedBoxOptions

# Correct
from boxlite.boxlite import AdvancedBoxOptions
```

### `AttributeError: type object 'SecurityOptions' has no attribute 'minimum'`

There is no `minimum()` preset. Use `SecurityOptions.development()` for weak isolation, and `standard()` / `maximum()` otherwise.

### Passing `security=` at the top level errors out

`BoxOptions` / `SimpleBox` **do not** have a `security=` keyword. Security options must be wrapped in `advanced=AdvancedBoxOptions(security=...)`. Passing `security=` directly reports a `TypeError` due to the unknown keyword argument.

### The secret was not injected (the request still contains the raw placeholder / authentication fails)

* The proxy substitutes placeholders in **request headers** (typically `Authorization`), the **URL query string**, and the **request body**. It does not touch the **URL path** — a placeholder there is forwarded literally.
* `hosts` must match the actual outbound target host. For example, requesting `api.openai.com` with `hosts=["openai.com"]` does not match: use the exact host or the `*.openai.com` wildcard.
* Egress must be **HTTPS** and pass through the BoxLite proxy; if the sandbox network is fully disabled, injection cannot occur.

### seccomp / jailer behave differently on macOS

`seccomp_enabled`, `new_pid_ns`, `new_net_ns`, and chroot are **Linux-only**. Setting them on macOS does not error — the runtime logs a `warn` and ignores them. `seccomp_enabled` **takes effect only on Linux** (`standard()`/`maximum()` already guard it internally with `cfg!(target_os = "linux")`). On macOS, even setting it to `True` does not enable seccomp; this is a platform difference. The jailer is available on both Linux and macOS.

### Isolation is blocking your program and you cannot tell which layer

Downgrade temporarily to confirm it, then put it back — do not ship with isolation off:

```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
from boxlite import SecurityOptions

debug_only = SecurityOptions.development()  # jailer and seccomp both off — diagnosis only
```

Once the interference is confirmed, return to `standard()` or `maximum()` and narrow the specific
field instead. Which field touches which layer is on
[Security and isolation](/architecture/security-and-isolation#what-each-switch-turns-off).

### The box fails to start (`RuntimeError`) — virtualization unavailable

BoxLite depends on hardware virtualization:

* **Linux**: requires KVM (`/dev/kvm` accessible); WSL2 requires KVM enabled and the user in the `kvm` group.
* **macOS**: uses Apple's Hypervisor.framework and **does not need `/dev/kvm`**.
* Without virtualization support, the box fails to start and raises a standard `RuntimeError` (the process stays alive and can be caught with try/except).

A network hiccup during image pull also raises a `RuntimeError`, which can be caught and retried.

## See also

* [Box types](/manage-sandbox/sandbox-types) — Box types and lifecycle
* [compute-resources.md](/manage-sandbox/compute-resources) — CPU / memory / disk resource configuration
