Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 20 additions & 52 deletions .github/workflows/real-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ jobs:
- name: Clean up previous E2E resources
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-redis || true
# Remove root-owned files from previous sandbox runs by mounting parent dir
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true
Expand Down Expand Up @@ -118,60 +120,10 @@ jobs:
if: always()
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-redis || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true

python-credential-vault-e2e:
name: Python Credential Vault E2E (docker bridge)
runs-on: self-hosted
env:
UV_BIN: /home/admin/.local/bin
steps:
- name: Checkout code
uses: actions/checkout@v6

- name: Clean Docker runner cache
run: bash scripts/ci-docker-cleanup.sh

- name: Set up uv PATH and verify
run: |
echo "${UV_BIN}" >> "$GITHUB_PATH"
export PATH="${UV_BIN}:${PATH}"
uv --version
uv run python --version

- name: Clean up previous Credential Vault E2E resources
run: |
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf \
/host_tmp/opensandbox-e2e \
/host_tmp/opensandbox-credential-vault-e2e || true
docker image prune -f || true

- name: Run Credential Vault E2E
run: ./scripts/python-credential-vault-e2e.sh

- name: Eval server logs
if: ${{ always() }}
run: cat /tmp/opensandbox-credential-vault-e2e/server.log || true

- name: Upload Credential Vault E2E logs
if: always()
uses: actions/upload-artifact@v7
with:
name: credential-vault-e2e-logs
path: /tmp/opensandbox-credential-vault-e2e/
retention-days: 5

- name: Clean up after Credential Vault E2E
if: always()
run: |
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf \
/host_tmp/opensandbox-e2e \
/host_tmp/opensandbox-credential-vault-e2e || true
docker rm -f opensandbox-e2e-redis || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true

java-e2e:
name: Java E2E (docker bridge)
Expand Down Expand Up @@ -208,6 +160,8 @@ jobs:
- name: Clean up previous E2E resources
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-redis || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true
docker image prune -f || true
Expand Down Expand Up @@ -284,6 +238,8 @@ jobs:
if: always()
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-redis || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true

Expand Down Expand Up @@ -327,6 +283,8 @@ jobs:
- name: Clean up previous E2E resources
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true
docker image prune -f || true

Expand Down Expand Up @@ -382,6 +340,8 @@ jobs:
if: always()
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true

csharp-e2e:
Expand Down Expand Up @@ -414,6 +374,8 @@ jobs:
- name: Clean up previous E2E resources
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true
docker image prune -f || true

Expand Down Expand Up @@ -468,6 +430,8 @@ jobs:
if: always()
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true

go-e2e:
Expand Down Expand Up @@ -502,6 +466,8 @@ jobs:
- name: Clean up previous E2E resources
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true
docker image prune -f || true

Expand Down Expand Up @@ -556,4 +522,6 @@ jobs:
if: always()
run: |
docker ps -aq --filter "label=opensandbox" | xargs -r docker rm -f || true
docker rm -f opensandbox-e2e-credential-vault-target || true
docker ps -aq --filter "label=opensandbox.e2e=credential-vault" | xargs -r docker rm -f || true
docker run --rm -v /tmp:/host_tmp alpine rm -rf /host_tmp/opensandbox-e2e || true
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ OpenSandbox is now listed in the [CNCF Landscape](https://landscape.cncf.io/?ite
- **Sandbox Runtime**: Built-in lifecycle management supporting Docker and [high-performance Kubernetes runtime](./kubernetes), enabling both local runs and large-scale distributed scheduling.
- **Sandbox Environments**: Built-in Command, Filesystem, and Code Interpreter implementations. Examples cover Coding Agents (e.g., Claude Code), browser automation (Chrome, Playwright), and desktop environments (VNC, VS Code).
- **Network Policy**: Unified [Ingress Gateway](components/ingress) with multiple routing strategies plus per-sandbox [egress controls](components/egress).
- **Credential Vault**: [Secure credential injection](docs/credential-vault.md) for sandbox outbound requests without exposing real secrets to workloads.
- **Strong Isolation**: Supports secure container runtimes like gVisor, Kata Containers, and Firecracker microVM for enhanced isolation between sandbox workloads and the host. See [Secure Container Runtime Guide](docs/secure-container.md) for details.

## SDKs
Expand Down
80 changes: 76 additions & 4 deletions docs/credential-vault.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ Credential Vault is OpenSandbox's outbound credential broker for sandboxed agent
## How It Works

Credential Vault is implemented by the egress sidecar. A sandbox must be created
with both a `network_policy` and `credential_proxy.enabled = true` in the Python
SDK. The lifecycle API field name is `credentialProxy.enabled`.
with both an outbound `network_policy` / `networkPolicy` and Credential Proxy
enabled. The lifecycle API field name is `credentialProxy.enabled`; SDKs expose
that field using their language-specific naming conventions.

At a high level:

Expand Down Expand Up @@ -82,11 +83,27 @@ X-Client-Secret: <client-secret>
## Requirements

- Server config sets `[egress].image`.
- Sandbox create request includes `network_policy`.
- Sandbox create request sets `credential_proxy=CredentialProxyConfig(enabled=True)`.
- Sandbox create request includes an outbound network policy.
- Sandbox create request enables Credential Proxy.
- The sandbox image has the tools you want to run. For Claude Code, use an image
with Node.js and npm, such as the OpenSandbox code-interpreter image.

## SDK Quick Reference

All sandbox SDKs use the same wire contract. The main differences are naming and
language style:

| SDK | Enable proxy on sandbox create | Vault entry point | Create / patch methods |
| --- | --- | --- | --- |
| Python | `credential_proxy=CredentialProxyConfig(enabled=True)` | `sandbox.credential_vault` | `create(...)`, `patch(...)` |
| Go | `CredentialProxy: &opensandbox.CredentialProxyConfig{Enabled: true}` | `sandbox.CredentialVault(ctx)` or sandbox helpers | `CreateCredentialVault(ctx, req)`, `PatchCredentialVault(ctx, req)` |
| JavaScript/TypeScript | `credentialProxy: { enabled: true }` | `sandbox.credentialVault` | `create(request)`, `patch(request)` |
| Kotlin/JVM | `.credentialProxyEnabled(true)` or `.credentialProxy { enabled(true) }` | `sandbox.credentialVault()` | `create(request)`, `patch(request)` |
| C#/.NET | `CredentialProxy = new CredentialProxyConfig { Enabled = true }` | `sandbox.CredentialVault` or sandbox helpers | `CreateCredentialVaultAsync(...)`, `PatchCredentialVaultAsync(...)` |

The vault APIs return sanitized metadata. Plaintext credential values are
write-only and are not returned by `get`, `list`, or patch responses.

## Claude Code With Anthropic

This example installs Claude Code in the sandbox and calls the official
Expand Down Expand Up @@ -190,6 +207,61 @@ header from Credential Vault. If your environment uses a private npm mirror,
replace `registry.npmjs.org` in the network policy and the `npm install`
command with that mirror host.

## Git And Curl With Vault-Injected Credentials

Credential Vault can also protect credentials used by command-line tools such as
`git` and `curl`. Keep the command free of real secrets and bind the request
shape to the credential in Vault instead.

For a private Git repository, store a base64-encoded `username:token` value and
bind it with `basic` auth:

```python
Credential(name="git-basic", source={"value": "<base64(username:token)>"})

CredentialBinding(
name="git-basic",
match={
"schemes": ["https"],
"ports": [443],
"hosts": ["git.example.com"],
"paths": ["/org/private-repo.git*"],
},
auth={"type": "basic", "credential": "git-basic"},
)
```

Then run the normal URL without embedding credentials:

```bash
GIT_TERMINAL_PROMPT=0 git clone https://git.example.com/org/private-repo.git
```

For an API request that expects a token header, bind the path and method to an
`apiKey` auth rule:

```python
Credential(name="api-token", source={"value": "<token>"})

CredentialBinding(
name="api-token",
match={
"schemes": ["https"],
"ports": [443],
"hosts": ["api.example.com"],
"methods": ["GET"],
"paths": ["/v1/projects/123/variables"],
},
auth={"type": "apiKey", "name": "PRIVATE-TOKEN", "credential": "api-token"},
)
```

The sandbox command stays secret-free:

```bash
curl -fsS https://api.example.com/v1/projects/123/variables
```

## Binding Guidance

- Use `defaultAction="deny"` and only allow the service hosts required by the
Expand Down
71 changes: 68 additions & 3 deletions docs/credential-vault_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Credential Vault 是 OpenSandbox 为沙箱内 Agent 和开发工具提供的出

## 原理

Credential Vault 由 egress sidecar 提供。通过 Python SDK 创建沙箱时需要同时设置 `network_policy` 和 `credential_proxy.enabled = true`;对应的 lifecycle API 字段名是 `credentialProxy.enabled`。
Credential Vault 由 egress sidecar 提供。创建沙箱时需要同时设置出站 `network_policy` / `networkPolicy`,并启用 Credential Proxy。对应的 lifecycle API 字段名是 `credentialProxy.enabled`;各语言 SDK 会按本语言命名习惯暴露这个字段

整体流程如下:

Expand Down Expand Up @@ -72,10 +72,24 @@ X-Client-Secret: <client-secret>
## 前置条件

- Server 配置中设置了 `[egress].image`。
- 创建沙箱时传入 `network_policy`
- 创建沙箱时设置 `credential_proxy=CredentialProxyConfig(enabled=True)`
- 创建沙箱时传入出站网络策略
- 创建沙箱时启用 Credential Proxy
- 沙箱镜像包含要运行的工具。运行 Claude Code 时,可以使用包含 Node.js 和 npm 的 OpenSandbox code-interpreter 镜像。

## SDK 快速对照

所有 sandbox SDK 使用同一套 wire contract,主要差异是命名和语言风格:

| SDK | 创建沙箱时启用 proxy | Vault 入口 | create / patch 方法 |
| --- | --- | --- | --- |
| Python | `credential_proxy=CredentialProxyConfig(enabled=True)` | `sandbox.credential_vault` | `create(...)`, `patch(...)` |
| Go | `CredentialProxy: &opensandbox.CredentialProxyConfig{Enabled: true}` | `sandbox.CredentialVault(ctx)` 或 sandbox helper | `CreateCredentialVault(ctx, req)`, `PatchCredentialVault(ctx, req)` |
| JavaScript/TypeScript | `credentialProxy: { enabled: true }` | `sandbox.credentialVault` | `create(request)`, `patch(request)` |
| Kotlin/JVM | `.credentialProxyEnabled(true)` 或 `.credentialProxy { enabled(true) }` | `sandbox.credentialVault()` | `create(request)`, `patch(request)` |
| C#/.NET | `CredentialProxy = new CredentialProxyConfig { Enabled = true }` | `sandbox.CredentialVault` 或 sandbox helper | `CreateCredentialVaultAsync(...)`, `PatchCredentialVaultAsync(...)` |

Vault API 返回的是脱敏后的 metadata。明文 credential value 是 write-only 的,不会通过 `get`、`list` 或 patch response 返回。

## Claude Code 调用 Anthropic 官方 API

下面的示例会在沙箱中安装 Claude Code,并访问 Anthropic 官方 API 地址。真实 API key 从宿主机环境变量读取并写入 Credential Vault;沙箱中只放一个假的 `ANTHROPIC_API_KEY`。
Expand Down Expand Up @@ -173,6 +187,57 @@ finally:

Claude Code 进程读取到的是假的 `ANTHROPIC_API_KEY`,但访问 `api.anthropic.com/v1/*` 时,Credential Vault 会在出站 HTTPS 请求上注入真实的 `x-api-key` header。如果你的环境使用 npm 私有镜像,需要把 network policy 和 `npm install` 命令中的 `registry.npmjs.org` 替换成对应镜像域名。

## Git 和 curl 使用 Vault 注入凭证

Credential Vault 也可以保护 `git`、`curl` 这类命令行工具使用的凭证。命令中不要携带真实密钥,而是把请求形状绑定到 Vault 中的 credential。

对于私有 Git 仓库,可以把 base64 编码后的 `username:token` 存入 Vault,并用 `basic` auth 绑定:

```python
Credential(name="git-basic", source={"value": "<base64(username:token)>"})

CredentialBinding(
name="git-basic",
match={
"schemes": ["https"],
"ports": [443],
"hosts": ["git.example.com"],
"paths": ["/org/private-repo.git*"],
},
auth={"type": "basic", "credential": "git-basic"},
)
```

然后在沙箱中执行不带凭证的普通 URL:

```bash
GIT_TERMINAL_PROMPT=0 git clone https://git.example.com/org/private-repo.git
```

对于需要 token header 的 API 请求,可以把 method 和 path 绑定到 `apiKey` auth:

```python
Credential(name="api-token", source={"value": "<token>"})

CredentialBinding(
name="api-token",
match={
"schemes": ["https"],
"ports": [443],
"hosts": ["api.example.com"],
"methods": ["GET"],
"paths": ["/v1/projects/123/variables"],
},
auth={"type": "apiKey", "name": "PRIVATE-TOKEN", "credential": "api-token"},
)
```

沙箱命令本身仍然不包含密钥:

```bash
curl -fsS https://api.example.com/v1/projects/123/variables
```

## 使用建议

- 使用 `defaultAction="deny"`,只 allow 工具实际需要访问的服务域名。
Expand Down
Loading
Loading