---
title: Secrets
description: Let sandboxed code use credentials without receiving their values
keywords: ["sandbox secrets", "credential injection", "environment variables"]
icon: "key"
---

Secrets give sandboxed code a placeholder instead of a credential. When it sends that placeholder to an allowed API, microsandbox verifies the destination and substitutes the real value outside the guest.

## Add a secret

Set `GITHUB_TOKEN` in your host environment, then bind it to `api.github.com`:

<CodeGroup>
```typescript TypeScript
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("python:3.12-bookworm")
    .secret(s =>
        s.env("GITHUB_TOKEN")
            .value(process.env.GITHUB_TOKEN!)
            .allow("api.github.com")
    )
    .create();
```

```rust Rust
use microsandbox::{Sandbox};

let token = std::env::var("GITHUB_TOKEN")?;
let sb = Sandbox::builder("worker")
    .image("python:3.12-bookworm")
    .secret(|s| s
        .env("GITHUB_TOKEN")
        .value(token)
        .allow("api.github.com")
    )
    .create()
    .await?;
```

```python Python
import os
from microsandbox import Sandbox, Secret

sb = await Sandbox.create(
    "worker",
    image="python:3.12-bookworm",
    secrets=[
        Secret.env(
            "GITHUB_TOKEN",
            value=os.environ["GITHUB_TOKEN"],
            allow=["api.github.com"],
        ),
    ],
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python:3.12-bookworm"),
    m.WithSecrets(
        m.Secret.Env("GITHUB_TOKEN", os.Getenv("GITHUB_TOKEN"),
            m.SecretEnvOptions{
                Allow: []string{"api.github.com"},
            },
        ),
    ),
)
if err != nil { return err }
```

```bash CLI
msb create python:3.12-bookworm --name worker \
  --secret 'GITHUB_TOKEN@api.github.com'
```
</CodeGroup>

The guest receives `$MSB_GITHUB_TOKEN`. Default placeholders preserve the environment variable’s spelling; custom placeholders are optional.

<Note>
Passing a raw value through an SDK persists it in the host-side sandbox configuration. Stopping the sandbox does not remove that stored value. The CLI example stores an environment reference and reads it at sandbox startup; inline credential values are rejected.
</Note>

## Use a secret

Run the request inside the sandbox:

<CodeGroup>
```typescript TypeScript
const output = await sb.exec("sh", ["-c",
    'curl -sS https://api.github.com/user -H "Authorization: Bearer $GITHUB_TOKEN"',
]);
console.log(output.stdout());
```

```rust Rust
let output = sb.exec("sh", ["-c",
    r#"curl -sS https://api.github.com/user -H "Authorization: Bearer $GITHUB_TOKEN""#,
]).await?;
println!("{}", output.stdout()?);
```

```python Python
output = await sb.exec("sh", ["-c",
    'curl -sS https://api.github.com/user -H "Authorization: Bearer $GITHUB_TOKEN"',
])
print(output.stdout_text)
```

```go Go
output, err := sb.Exec(ctx, "sh", []string{"-c",
    `curl -sS https://api.github.com/user -H "Authorization: Bearer $GITHUB_TOKEN"`,
})
if err != nil { return err }
fmt.Print(output.Stdout())
```

```bash CLI
msb exec worker -- sh -c \
  'curl -sS https://api.github.com/user -H "Authorization: Bearer $GITHUB_TOKEN"'
```
</CodeGroup>

Inside the sandbox, `GITHUB_TOKEN` holds a placeholder. microsandbox substitutes the real token when the request reaches the allowed host.

## Request locations

| Location | Default |
| --- | --- |
| Headers, including Basic authentication | Enabled |
| URL query parameters | Disabled |
| Request body | Disabled |

Enable only the locations your API needs. Disabling headers also disables Basic authentication substitution. Once the destination passes the secret's host and TLS identity checks, placeholders in disabled locations are forwarded unchanged. For example, a request can authenticate with a substituted header while keeping the placeholder in its body. Other destinations need explicit passthrough permission or follow the violation policy.

<Accordion title="Enable query substitution">

Enable query substitution only when the API requires a credential in the URL:

<CodeGroup>
```typescript TypeScript
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("python")
    .secret(s =>
        s.env("GITHUB_TOKEN")
            .value(process.env.GITHUB_TOKEN!)
            .allow("api.github.com")
            .substituteInQuery(true)
    )
    .create();
```

```rust Rust
use microsandbox::{Sandbox};

let token = std::env::var("GITHUB_TOKEN")?;
let sb = Sandbox::builder("worker")
    .image("python")
    .secret(|s| s
        .env("GITHUB_TOKEN")
        .value(token)
        .allow("api.github.com")
        .substitute_in_query(true)
    )
    .create()
    .await?;
```

```python Python
import os
from microsandbox import Sandbox, Secret, SecretSubstitution

sb = await Sandbox.create(
    "worker",
    image="python",
    secrets=[
        Secret.env(
            "GITHUB_TOKEN",
            value=os.environ["GITHUB_TOKEN"],
            allow=["api.github.com"],
            substitution=SecretSubstitution(query=True),
        ),
    ],
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithSecrets(
        m.Secret.Env("GITHUB_TOKEN", os.Getenv("GITHUB_TOKEN"),
            m.SecretEnvOptions{
                Allow: []string{"api.github.com"},
                Substitution: m.SecretSubstitution{Query: true},
            },
        ),
    ),
)
if err != nil { return err }
```

```bash CLI
msb create python --name worker \
  --secret 'GITHUB_TOKEN:query@api.github.com'
```
</CodeGroup>

</Accordion>

<Accordion title="Enable body substitution">

Enable body substitution only when the API requires a credential in the body:

<CodeGroup>
```typescript TypeScript
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("python")
    .secret(s =>
        s.env("GITHUB_TOKEN")
            .value(process.env.GITHUB_TOKEN!)
            .allow("api.github.com")
            .substituteInBody(true)
    )
    .create();
```

```rust Rust
use microsandbox::{Sandbox};

let token = std::env::var("GITHUB_TOKEN")?;
let sb = Sandbox::builder("worker")
    .image("python")
    .secret(|s| s
        .env("GITHUB_TOKEN")
        .value(token)
        .allow("api.github.com")
        .substitute_in_body(true)
    )
    .create()
    .await?;
```

```python Python
import os
from microsandbox import Sandbox, Secret, SecretSubstitution

sb = await Sandbox.create(
    "worker",
    image="python",
    secrets=[
        Secret.env(
            "GITHUB_TOKEN",
            value=os.environ["GITHUB_TOKEN"],
            allow=["api.github.com"],
            substitution=SecretSubstitution(body=True),
        ),
    ],
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithSecrets(
        m.Secret.Env("GITHUB_TOKEN", os.Getenv("GITHUB_TOKEN"),
            m.SecretEnvOptions{
                Allow: []string{"api.github.com"},
                Substitution: m.SecretSubstitution{Body: true},
            },
        ),
    ),
)
if err != nil { return err }
```

```bash CLI
msb create python --name worker \
  --secret 'GITHUB_TOKEN:body@api.github.com'
```
</CodeGroup>

When enabled, body substitution supports fixed-length HTTP/1 bodies up to 16 MiB and chunked HTTP/1 bodies. Larger fixed-length bodies are blocked. Encoded bodies pass through unchanged; HTTP/2 body substitution is unsupported and matching body placeholders are blocked. See [body limits](/sdk/typescript/secrets#secret-substituteinbody).

</Accordion>

## Violation policy

When a destination is not permitted to receive the credential or the unchanged placeholder, these policies control what happens to the request:

| Policy | Result |
| --- | --- |
| Block and log (default) | Block the request and log a warning |
| Block | Block without a violation log |
| Block and terminate | Block, log an error, and stop the sandbox |
| Passthrough | Let selected hosts receive the unchanged placeholder |

The first three are violation actions. Passthrough is a separate per-secret host rule; unmatched requests still follow the violation action.

### Block and log

The default. No additional configuration is needed; to set it explicitly:

<CodeGroup>
```typescript TypeScript
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("python")
    .secret(s =>
        s.env("GITHUB_TOKEN")
            .value(process.env.GITHUB_TOKEN!)
            .allow("api.github.com")
            .violationAction("block-and-log")
    )
    .create();
```

```rust Rust
use microsandbox::{Sandbox, SecretViolationAction};

let token = std::env::var("GITHUB_TOKEN")?;
let sb = Sandbox::builder("worker")
    .image("python")
    .secret(|s| s
        .env("GITHUB_TOKEN")
        .value(token)
        .allow("api.github.com")
        .violation_action(SecretViolationAction::BlockAndLog)
    )
    .create()
    .await?;
```

```python Python
import os
from microsandbox import Sandbox, Secret, ViolationAction

sb = await Sandbox.create(
    "worker",
    image="python",
    secrets=[
        Secret.env(
            "GITHUB_TOKEN",
            value=os.environ["GITHUB_TOKEN"],
            allow=["api.github.com"],
            violation_action=ViolationAction.BLOCK_AND_LOG,
        ),
    ],
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithSecrets(
        m.Secret.Env("GITHUB_TOKEN", os.Getenv("GITHUB_TOKEN"),
            m.SecretEnvOptions{
                Allow: []string{"api.github.com"},
                ViolationAction: m.ViolationActionBlockAndLog,
            },
        ),
    ),
)
if err != nil { return err }
```

```bash CLI
msb create python --name worker \
  --secret 'GITHUB_TOKEN@api.github.com' \
  --secret-violation-action block-and-log
```
</CodeGroup>

### Block

Reject the request without a violation log:

<CodeGroup>
```typescript TypeScript
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("python")
    .secret(s =>
        s.env("GITHUB_TOKEN")
            .value(process.env.GITHUB_TOKEN!)
            .allow("api.github.com")
            .violationAction("block")
    )
    .create();
```

```rust Rust
use microsandbox::{Sandbox, SecretViolationAction};

let token = std::env::var("GITHUB_TOKEN")?;
let sb = Sandbox::builder("worker")
    .image("python")
    .secret(|s| s
        .env("GITHUB_TOKEN")
        .value(token)
        .allow("api.github.com")
        .violation_action(SecretViolationAction::Block)
    )
    .create()
    .await?;
```

```python Python
import os
from microsandbox import Sandbox, Secret, ViolationAction

sb = await Sandbox.create(
    "worker",
    image="python",
    secrets=[
        Secret.env(
            "GITHUB_TOKEN",
            value=os.environ["GITHUB_TOKEN"],
            allow=["api.github.com"],
            violation_action=ViolationAction.BLOCK,
        ),
    ],
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithSecrets(
        m.Secret.Env("GITHUB_TOKEN", os.Getenv("GITHUB_TOKEN"),
            m.SecretEnvOptions{
                Allow: []string{"api.github.com"},
                ViolationAction: m.ViolationActionBlock,
            },
        ),
    ),
)
if err != nil { return err }
```

```bash CLI
msb create python --name worker \
  --secret 'GITHUB_TOKEN@api.github.com' \
  --secret-violation-action block
```
</CodeGroup>

### Block and terminate

Stop the sandbox when a violation occurs:

<CodeGroup>
```typescript TypeScript
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("python")
    .secret(s =>
        s.env("GITHUB_TOKEN")
            .value(process.env.GITHUB_TOKEN!)
            .allow("api.github.com")
            .violationAction("block-and-terminate")
    )
    .create();
```

```rust Rust
use microsandbox::{Sandbox, SecretViolationAction};

let token = std::env::var("GITHUB_TOKEN")?;
let sb = Sandbox::builder("worker")
    .image("python")
    .secret(|s| s
        .env("GITHUB_TOKEN")
        .value(token)
        .allow("api.github.com")
        .violation_action(SecretViolationAction::BlockAndTerminate)
    )
    .create()
    .await?;
```

```python Python
import os
from microsandbox import Sandbox, Secret, ViolationAction

sb = await Sandbox.create(
    "worker",
    image="python",
    secrets=[
        Secret.env(
            "GITHUB_TOKEN",
            value=os.environ["GITHUB_TOKEN"],
            allow=["api.github.com"],
            violation_action=ViolationAction.BLOCK_AND_TERMINATE,
        ),
    ],
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithSecrets(
        m.Secret.Env("GITHUB_TOKEN", os.Getenv("GITHUB_TOKEN"),
            m.SecretEnvOptions{
                Allow: []string{"api.github.com"},
                ViolationAction: m.ViolationActionBlockAndTerminate,
            },
        ),
    ),
)
if err != nil { return err }
```

```bash CLI
msb create python --name worker \
  --secret 'GITHUB_TOKEN@api.github.com' \
  --secret-violation-action block-and-terminate
```
</CodeGroup>

The SDK examples set a per-secret action; the CLI flag sets the sandbox-wide default. An explicit per-secret action takes precedence.

<span id="passthrough" />

### Allow placeholders

Allow an additional host to receive the **unchanged placeholder** where substitution does not apply. Credential-allowed hosts already receive unchanged placeholders in disabled locations after passing the secret's identity checks. This permission does not grant access to the credential; existing substitution permissions still apply. For example, allow the placeholder to appear in an AI request:

<CodeGroup>
```typescript TypeScript
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("python")
    .secret(s =>
        s.env("GITHUB_TOKEN")
            .value(process.env.GITHUB_TOKEN!)
            .allow("api.github.com")
            .allowPlaceholderFor("api.anthropic.com")
    )
    .create();
```

```rust Rust
use microsandbox::{Sandbox};

let token = std::env::var("GITHUB_TOKEN")?;
let sb = Sandbox::builder("worker")
    .image("python")
    .secret(|s| s
        .env("GITHUB_TOKEN")
        .value(token)
        .allow("api.github.com")
        .allow_placeholder_for("api.anthropic.com")
    )
    .create()
    .await?;
```

```python Python
import os
from microsandbox import Sandbox, Secret

sb = await Sandbox.create(
    "worker",
    image="python",
    secrets=[
        Secret.env(
            "GITHUB_TOKEN",
            value=os.environ["GITHUB_TOKEN"],
            allow=["api.github.com"],
            allow_placeholder_for=["api.anthropic.com"],
        ),
    ],
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithSecrets(
        m.Secret.Env("GITHUB_TOKEN", os.Getenv("GITHUB_TOKEN"),
            m.SecretEnvOptions{
                Allow: []string{"api.github.com"},
                AllowPlaceholderFor: []string{"api.anthropic.com"},
            },
        ),
    ),
)
if err != nil { return err }
```

```bash CLI
msb create python --name worker \
  --secret 'GITHUB_TOKEN:passthrough=api.anthropic.com@api.github.com'
```
</CodeGroup>

Passthrough neither grants network access nor adds the host to the credential allow list. Other destinations still follow the violation action.

<span id="change-while-running" />

## Update secrets

<Tooltip tip="modify is not yet available on microsandbox cloud; recreate the sandbox to change its secrets."><span className="msb-badge-local">Local-only <Icon icon="circle-info" size={11} /></span></Tooltip>

Existing values and removals can apply live when supported. Adding a secret or changing its placeholder requires a restart. These examples use a host environment reference and allow a restart if needed:

New secrets added through `modify` require TLS identity by default and turn interception on when it is off, the same way `secret` does at create time. Interception cannot start on a running sandbox, so that change is restart-backed and appears in the plan as `tls`. Existing secrets that explicitly allow plain-HTTP substitution with `require_tls_identity(false)` continue to rotate live without enabling interception. Removing every TLS-dependent secret leaves interception on, since it may have been enabled for reasons of its own.

<CodeGroup>
```typescript TypeScript
await sb.modify({
  secrets: {
    GITHUB_TOKEN: { env: "GITHUB_TOKEN", allowedHosts: ["api.github.com"] },
  },
  policy: "restart",
});

await sb.modify({ secretsRemove: ["GITHUB_TOKEN"] });
```

```rust Rust
use microsandbox::sandbox::SecretSource;

let plan = sb.modify()
    .secret(|s| s
        .env("GITHUB_TOKEN")
        .source(SecretSource::Env { var: "GITHUB_TOKEN".into() })
        .allow("api.github.com"))
    .restart()
    .apply()
    .await?;

sb.modify().remove_secret("GITHUB_TOKEN").apply().await?;
```

```python Python
from microsandbox import ModificationPolicy

await sb.modify(
    secrets={
        "GITHUB_TOKEN": {
            "env": "GITHUB_TOKEN",
            "allowed_hosts": ["api.github.com"],
        },
    },
    policy=ModificationPolicy.RESTART,
)

await sb.modify(secrets_rm=["GITHUB_TOKEN"])
```

```go Go
_, err := sb.Modify(ctx, m.ModifyOptions{
    Secrets: map[string]m.SecretModifySpec{
        "GITHUB_TOKEN": {
            Env:          "GITHUB_TOKEN",
            AllowedHosts: []string{"api.github.com"},
        },
    },
    Policy: m.ModificationPolicyRestart,
})

_, err = sb.Modify(ctx, m.ModifyOptions{
    SecretsRemove: []string{"GITHUB_TOKEN"},
})
```

```bash CLI
msb modify worker --secret GITHUB_TOKEN@api.github.com --restart  # add or rotate
msb modify worker --secret-rm GITHUB_TOKEN                       # remove
```
</CodeGroup>

Updating a sandbox secret does not rotate or revoke it at the issuing service. Do that separately. See [Live Modify](/sandboxes/tuning) for restart policies.

## YAML configuration

You can also declare secrets in sandbox YAML. An exact environment reference avoids persisting the resolved value:

```yaml
secrets:
  GITHUB_TOKEN:
    value: ${GITHUB_TOKEN}
    allow: [api.github.com]
    substitution:
      headers: true
      query: false
      body: false
    passthrough: [api.anthropic.com]
    violation_action: block-and-terminate
```

In YAML, `substitution` selects where credentials are inserted; `passthrough` permits unchanged placeholders on additional hosts. A top-level `secret_violation_action` sets the sandbox-wide default. See [CLI configuration](/cli/configuration#secrets) for loading configuration and CLI equivalents.

## Security boundary

- **Destination checks:** Injection checks the allowed host against observed DNS and TLS identity, including the HTTP request authority. A forged hostname or hard-coded IP is not enough.
- **TLS:** Injection requires intercepted TLS by default. Bypassed TLS cannot be inspected for placeholders or receive injected credentials. See [TLS inspection](/networking/tls).
- **Trusted hosts:** Allowed endpoints receive the real value and could return it to the guest. Keep allow lists narrow and include redirect destinations only when trusted. Wildcards include the root domain and its subdomains.
- **Network access:** Secret rules decide where credentials can be injected; network rules still control connectivity.
- **Guest-side signing:** A placeholder cannot replace raw credentials used to sign requests inside the guest. Supplying the real value as a plain environment variable exposes it to guest code.

See [Secret handling](/security/secrets) for the protection boundary and storage details.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| Request blocked | Check host logs, destination rules, and enabled request locations. |
| Sandbox stops after a request | Check for a terminate-on-violation policy. |
| API receives a placeholder | Check TLS interception, substitution settings, and passthrough rules. |
| Guest prints the placeholder | Expected: substitution happens outside the guest. |

## Reference

[TypeScript](/sdk/typescript/secrets) · [Rust](/sdk/rust/secrets) · [Python](/sdk/python/secrets) · [Go](/sdk/go/secrets)
