---
title: Egress and secrets
description: The env's egress gateway, per-sandbox network policy, and credentials a sandbox can use without being able to read them.
---

# Egress and secrets

Two decisions, and conflating them is the usual mistake:

```
SandboxEnv    does this environment HAVE a gateway        one switch
create call   what THIS sandbox may reach, and what       per sandbox
              may be injected into its outbound requests
```

The env carries a switch; the rules belong to an individual sandbox and arrive
with the create call, in the E2B SDK's own vocabulary. There is no separate
Agent Sandbox dialect for them.

## Why the switch is on the env and the rules are not

Enabling the gateway injects a transparent proxy sidecar. That changes the Pod
spec, and a changed Pod spec rolls the env's pools — a real, environment-level
decision, taken once. Rules are per sandbox because an env is shared: an
env-wide allowlist would be a default every caller overrides anyway.

**It fails closed.** A create that carries filtering rules against an env with
no gateway is refused with `400`, not accepted and ignored. A Pod with no
sidecar has no redirection, so accepting the rules would mean they silently did
nothing — which for an evaluation is the worst outcome, because the run
finishes and the numbers are wrong.

```bash
abx update envs YOUR_ENV --help --cluster YOUR_CLUSTER   # the env's one gateway field
```

## Cutting a sandbox off, and letting one thing through

The common case is an agent under test that must not fetch the answer and must
not install its way around a missing dependency. The shape is always the same:
**deny everything, then allow what the task genuinely needs.** A denylist is a
list of the routes somebody thought of.

```python
# nothing in, nothing out
sbx = Sandbox.create("YOUR_ENV", timeout=3000, secure=False, allow_internet_access=False)

# or: everything out except these
sbx = Sandbox.create(
    "YOUR_ENV",
    timeout=3000,
    secure=False,
    network={"allow_out": ["api.openai.com", "pypi.org", "*.pythonhosted.org"]},
)
```

Naming an allow list is what makes everything else a deny — the entries are the
whole of what is reachable. A CIDR or a bare IP works in the same list.

**An empty allow list is not a deny**

> `network={"allow_out": []}` declares no filtering at all, and egress stays
> unrestricted. To cut a sandbox off, say so: `allow_internet_access=False`, or
> `deny_out=["0.0.0.0/0"]`.

Three things worth checking before calling a run isolated:

1. **The env has a gateway.** Without it, a create carrying rules is refused —
   so make sure you saw a sandbox, not a `400`.
2. **The package index is not on the allowlist** unless the task needs it. It is
   the most common accidental hole: the agent cannot search, but it can
   `pip install` something that can.
3. **Watch it fail from inside.**

```python
sbx.commands.run("curl -sS -m 5 https://example.com")   # expected: it fails
```

An isolation you have not seen fail is an isolation you are assuming.

## Credentials a sandbox can use but cannot read

```
vault (write-only)  →  operator memory  →  sidecar tmpfs  →  outbound header
```

The value never enters the sandbox. The sandbox holds a **decoy** — a
placeholder that looks like a token — and the egress sidecar substitutes the
real one as the request leaves, for the hosts and headers the rule names. Code
inside runs unmodified: it reads `OPENAI_API_KEY`, sends it, and the sidecar
replaces it on the way out. Library code that has never heard of Agent Sandbox
works.

Secrets live in a **vault** that is write-only: you can list names, overwrite
them and delete them, never read them back. In the console it is the **Vault**
page; through the SDK it is the E2B `/secrets` surface, which the official
package speaks as `e2b.Secret` (that module arrived in **e2b 2.43**).

### Store it once

```bash
abx envs YOUR_ENV docs --cluster YOUR_CLUSTER   # API URL and data-plane domain
```

```python
import os

os.environ["E2B_API_KEY"] = "agbx_..."                            # your platform key
os.environ["E2B_API_URL"] = "https://YOUR_GATEWAY/agent-sandbox/api/e2b"
os.environ["E2B_DOMAIN"] = "YOUR_GATEWAY/agent-sandbox/api/data"  # no scheme

from agent_sandbox_e2b import patch_e2b   # before the e2b import
patch_e2b()

from e2b import Secret

Secret.create("openai-api-key", os.environ["OPENAI_API_KEY"])   # write-only
# rotate it later with Secret.update("openai-api-key", new_value)
```

### Use it at create time

Reference it **by name**; the value never leaves the vault. `Secret.fill()` is a
local formatting helper — it returns the string the wire carries, and makes no
call of its own:

```python
from e2b import Sandbox, Secret

sbx = Sandbox.create(
    "YOUR_ENV",
    timeout=3000,
    secure=False,
    # What the code inside reads. It is a decoy: the real value is put in on the
    # way out, so the sandbox cannot leak what it never had.
    envs={"OPENAI_API_KEY": "decoy-not-a-real-key"},
    network={
        # A rule is a transform, not a permission — the host has to be allowed
        # too, or the request never leaves.
        "allow_out": ["api.openai.com"],
        "rules": {
            "api.openai.com": [
                {
                    "transform": {
                        "headers": {
                            "Authorization": f"Bearer {Secret.fill('openai-api-key')}",
                        }
                    }
                }
            ]
        },
    },
)
```

Ordinary code inside, no Agent Sandbox dialect:

```python
sbx.commands.run(
    'curl -s https://api.openai.com/v1/models -H "Authorization: Bearer $OPENAI_API_KEY"'
)   # succeeds
sbx.commands.run("echo $OPENAI_API_KEY")       # prints the decoy, not the key
sbx.commands.run("curl -sS -m 5 https://example.com")   # fails: not allowed
sbx.kill()
```

A plaintext credential in the rules is refused with `400` — deliberately,
because accepting it would put the value in the request body, the access log
and the caller's source, which is the exposure the feature exists to remove.
Wildcard hosts are refused for the same reason: whoever controls a matching
subdomain would receive the injected credential.

### The env has to have the gateway

All of the above is refused on an Env whose gateway is off, with a `400` that
says so:

```
a network policy was requested but this environment has no egress gateway;
enable it on the SandboxEnv (overrides.gateway.enabled) and let its pools roll
```

That is the switch from the top of this page, and turning it on is a change to
the Pod spec, so it rolls the Env's pools.

## When it silently does not work

The sandbox runs, the request goes out, and nothing is substituted. Check in
this order:

| Check | Why |
|---|---|
| the env has the gateway on | without it, a create with rules is refused — so if a sandbox exists, this one is satisfied |
| the host matches the rule exactly | a redirect to another host is not covered |
| the port is 80 or 443 | only those are parsed at layer 7; rules for other ports never fire and nothing says so |
| the secret name exists in **your** vault | a name that resolves to nothing leaves the decoy in place, and the upstream answers `401`, which reads like a bad key |

`abx whoami` tells you which identity the vault is read as; a secret stored by
one user is not visible to another.

## Agents, vaults and approval

Writing a vault secret **is** allowed to an agent credential, under the normal
approval gate: the credential lands in the acting person's own vault and widens
nobody's authority. Minting an Agent Sandbox API key is the act that is
refused. See [API key permissions](/docs/tutorials/cli#6-api-key-permissions).

## See also

- [Envs](/docs/concepts/envs) — the gateway switch and the rest of the settings
- [Pools](/docs/concepts/pools) — why enabling the gateway rolls them
- [In-place update](/docs/concepts/inplace-update) — what a claim does to a Pod
