Network Egress Control
Control which hosts are reachable and which get secret injection
Two-Layer Security Model
Oshu Vault provides two independent controls on every session:
| Field | When set | When not set |
|---|---|---|
allowed_egress | Only listed hosts can be reached — everything else gets 403 Forbidden | All hosts are reachable |
secret_hosts | Only listed hosts get sealed token replacement — others pass through unchanged | All hosts get token replacement |
This separation lets you lock down network egress independently from secret injection scope.
Why two layers?
Consider a sandbox running an AI agent. You want to:
- Block all outbound traffic except the APIs the agent needs (egress control)
- Only inject your API key for the specific provider, not every request (secret scoping)
const session = await client.createSession({
secrets: {
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!,
},
// Layer 1: Network firewall — only these hosts are reachable
allowed_egress: [
"api.anthropic.com",
"*.github.com", // wildcard for all GitHub subdomains
"registry.npmjs.org",
],
// Layer 2: Secret injection — only Anthropic gets the real key
secret_hosts: ["api.anthropic.com"],
});With this configuration:
api.anthropic.com— reachable, sealed tokens replaced with real secretsapi.github.com— reachable (wildcard match), but sealed tokens pass through unchangedregistry.npmjs.org— reachable, no token replacementevil.com— blocked with 403
Wildcard Matching
allowed_egress supports wildcard patterns with *.:
| Pattern | Matches | Does not match |
|---|---|---|
api.openai.com | api.openai.com | beta.openai.com |
*.openai.com | api.openai.com, beta.openai.com, openai.com | openai.org |
*.github.com | api.github.com, raw.githubusercontent.github.com, github.com | github.io |
secret_hosts uses exact matching only (case-insensitive, port stripped).
Combining with Sandbox Network Controls
Both E2B and Daytona have their own network restriction features. These work at the infrastructure level (IP/CIDR-based), while Oshu Vault's egress control works at the application level (domain-based). Using both together provides defense in depth.
E2B's network.allowOut restricts outbound connections by IP, CIDR, or domain. Combined with Oshu Vault, you get:
- E2B network layer — forces all traffic through the proxy (block everything except the proxy IP)
- Oshu Vault egress layer — the proxy blocks domains not in
allowed_egress - Oshu Vault secret layer — only
secret_hostsget token replacement
import { Sandbox, ALL_TRAFFIC } from "@e2b/code-interpreter";
import { SecretsProxyClient } from "@oshu/vault-sdk";
const PROXY_HOST = "pv.oshu.dev";
const client = new SecretsProxyClient({
baseUrl: `https://${PROXY_HOST}`,
apiKey: process.env.SECRETS_PROXY_API_KEY!,
});
// Create session with egress + secret host controls
const session = await client.createSession({
secrets: {
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!,
},
allowed_egress: ["api.anthropic.com", "*.github.com"],
secret_hosts: ["api.anthropic.com"],
});
const proxyUrl = `https://${session.session_id}:${session.token}@${PROXY_HOST}`;
// E2B: only allow traffic to the proxy itself
const sandbox = await Sandbox.create("oshu-vault-claude", {
envs: {
ANTHROPIC_API_KEY: session.sealed_secrets["ANTHROPIC_API_KEY"],
HTTP_PROXY: proxyUrl,
HTTPS_PROXY: proxyUrl,
},
network: {
denyOut: [ALL_TRAFFIC],
allowOut: [PROXY_HOST],
},
});
try {
const result = await sandbox.commands.run(
`claude -p "Write a hello world program"`,
{ timeoutMs: 120_000 },
);
console.log(result.stdout);
} finally {
await client.deleteSession(session.session_id);
await sandbox.kill();
}E2B's domain filtering works via SNI inspection for HTTPS (port 443) and Host header for HTTP (port 80). Since all traffic goes through the proxy anyway, Oshu Vault's domain-based allowed_egress provides more granular control on top.
Daytona's networkAllowList restricts outbound connections by IP/CIDR. Combined with Oshu Vault, you get:
- Daytona network layer — forces all traffic through the proxy (only allow the proxy's IP range)
- Oshu Vault egress layer — the proxy blocks domains not in
allowed_egress - Oshu Vault secret layer — only
secret_hostsget token replacement
import { Daytona, Image } from "@daytonaio/sdk";
import { SecretsProxyClient } from "@oshu/vault-sdk";
import dns from "node:dns/promises";
const PROXY_HOST = "pv.oshu.dev";
const PROXY_BASE_URL = `https://${PROXY_HOST}`;
const client = new SecretsProxyClient({
baseUrl: PROXY_BASE_URL,
apiKey: process.env.SECRETS_PROXY_API_KEY!,
});
// Create session with egress + secret host controls
const session = await client.createSession({
secrets: {
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!,
},
allowed_egress: ["api.anthropic.com", "*.github.com"],
secret_hosts: ["api.anthropic.com"],
});
const proxyUrl = `https://${session.session_id}:${session.token}@${PROXY_HOST}`;
// Resolve the proxy's IP for the Daytona allowlist
const proxyIps = await dns.resolve4(PROXY_HOST);
// Daytona also needs DNS — Cloudflare (1.1.1.0/31) + internal resolver
const networkAllowList = [
...proxyIps.map((ip) => `${ip}/32`),
"1.1.1.0/31",
"100.65.160.1/32",
].join(",");
const image = Image.base("node:22-slim").runCommands(
"apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*",
`curl -fsSL ${PROXY_BASE_URL}/v1/ca.pem -o /usr/local/share/ca-certificates/proxy-ca.crt && update-ca-certificates`,
"npm install -g @anthropic-ai/claude-code",
);
const daytona = new Daytona({ apiKey: process.env.DAYTONA_KEY });
const sandbox = await daytona.create(
{
image,
envVars: {
ANTHROPIC_API_KEY: session.sealed_secrets["ANTHROPIC_API_KEY"],
HTTP_PROXY: proxyUrl,
HTTPS_PROXY: proxyUrl,
NODE_EXTRA_CA_CERTS: "/etc/ssl/certs/ca-certificates.crt",
},
// Daytona: only allow traffic to the proxy + DNS
networkAllowList,
},
{ timeout: 0, onSnapshotCreateLogs: console.log },
);
try {
const result = await sandbox.process.executeCommand(
`claude -p "Write a hello world program"`,
);
console.log(result.result);
} finally {
await client.deleteSession(session.session_id);
await daytona.delete(sandbox);
}Daytona's networkAllowList is IP-based (CIDR ranges), so you need to resolve the proxy's hostname to IPs. The sandbox also needs DNS access to resolve hostnames — include your DNS server IPs in the allowlist (Cloudflare 1.1.1.0/31 and Daytona's internal resolver 100.65.160.1/32).
Defense in Depth
The diagram below shows how the layers interact:
Sandbox Infrastructure Oshu Vault Upstream
| (E2B/Daytona) | |
|-- curl evil.com ------->| | |
| BLOCKED (IP) <----| (not in allowlist) | |
| | | |
|-- curl api.anthropic -->|-- forward to proxy --------->| |
| | (proxy IP allowed) | |
| | |-- api.anthropic.com->|
| | | SEALED_x -> sk-... |
| | | (in secret_hosts) |
| | | |
|-- curl api.github.com->|-- forward to proxy --------->| |
| | |-- api.github.com --->|
| | | SEALED_x intact |
| | | (not in |
| | | secret_hosts) |
| | | |
|-- curl example.com ---->|-- forward to proxy --------->| |
| 403 Forbidden <----|<--- 403 ---------------<-----| |
| | | (not in |
| | | allowed_egress) |Layer 1 (Infrastructure): The sandbox provider blocks all direct outbound traffic except to the proxy. This is enforced at the network level — even root inside the sandbox cannot bypass it.
Layer 2 (Proxy Egress): The proxy checks allowed_egress and returns 403 for hosts not on the list. This provides domain-based control (vs. IP-based at the infrastructure level).
Layer 3 (Secret Scoping): For allowed hosts, secret_hosts controls whether sealed tokens get replaced. Non-matching hosts get the request forwarded with sealed tokens intact.
API Reference
Create Session
curl -X POST https://pv.oshu.dev/v1/sessions \
-H "Authorization: Bearer $SECRETS_PROXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"secrets": {
"ANTHROPIC_API_KEY": "sk-ant-..."
},
"secret_hosts": ["api.anthropic.com"],
"allowed_egress": ["api.anthropic.com", "*.github.com"],
"sliding_ttl": 3600
}'Get Session
The response includes both fields:
{
"session_id": "sess_abc123...",
"secret_hosts": ["api.anthropic.com"],
"allowed_egress": ["api.anthropic.com", "*.github.com"],
"secrets": {
"SEALED_aaa111...": "[REDACTED]"
},
"expires_in": 3600,
"created_at": "2025-01-15T10:00:00Z"
}