Sandbox firewall
Network firewall allows users to restrict egress traffic from their sandbox. It is a critical tool to prevent data exfiltration.
- Protect user data: Allow untrusted code to touch user-data without a risk of it getting exfiltrated.
- Avoid malware injection: Constrain package sources, or S3 buckets to access.
- Dynamic policies for multi-step work: Start with Internet access, get required data, lock access and start untrusted process.
- Protect your credentials: Untrusted code running within the sandbox cannot be trusted with credentials, but needs to authenticate to external services (e.g. AI Gateway).
- Proxying requests: Forward requests to a proxy you control, for logging, debugging, or transformation purposes.
Sandboxes can use three distinct modes, which can be updated at runtime, without restarting the process.
Default policy. This gives the sandbox unrestricted access to the public Internet.
Have the ability to install software packages, download dependencies and pull any data from external sources with the enhanced security model of sandboxes.
Most restrictive policy. Denies all outbound network access, including DNS.
This is useful to reduce the chance of data exfiltration when running untrusted code or an agent on private data.
User-defined policies deny traffic by default and let you allow specific destinations. You can define:
- Allowed domains: Allow traffic by domain (for example,
api.example.comor*.example.com), including services that use virtual hosting, such as Amazon S3. A domain allowlist constrains which hostname a connection negotiates, not which virtual host the request ultimately reaches; see HTTP and HTTPS. - Allowed address ranges: Allow traffic by CIDR range (for example,
10.0.0.0/8). Use address ranges for non-encrypted traffic or private network access through Secure Compute. Address range rules do not enforce per-domain rules. - Denied address ranges: Block traffic to specific CIDR ranges (for example,
172.16.0.0/12). Denied ranges take precedence over allowed domains and address ranges, but only remove access that an allow rule already granted.
The firewall makes two separate decisions:
- Access: the three lists above decide whether traffic gets through at all.
- Handling: rules on an allowed domain choose one action for matching requests: credentials brokering, requests proxying, or a direct response. Each rule defines exactly one action.
Matchers alone do not block traffic. A direct-response rule can reject a request to an allowed domain without contacting the destination.
A domain wildcard must replace an entire DNS label. You can place wildcard labels anywhere in a domain pattern. Partial-label wildcards such as api*.example.com are not supported.
Domain patterns match as follows:
| Pattern | Matches | Does not match |
|---|---|---|
example.com | example.com | www.example.com |
*.example.com | www.example.com, www.api.example.com | example.com |
www.*.com | www.example.com | example.com, www.api.example.com |
* | Any domain | None |
The following behaviors determine the effective scope of a user-defined policy:
- Leading wildcards don't match the apex domain:
*.example.commatches subdomains at any depth, but notexample.comitself. Addexample.comseparately if the sandbox needs both. - An empty policy behaves as
deny-all: a policy with no allowed domains or CIDR ranges blocks all outbound traffic, including DNS. This includes{},{ allow: {} },{ subnets: {} }, and policies that contain onlysubnets.deny, which blocks everything rather than creating an allow-all policy with exceptions. Use the explicitdeny-allmode when you intend to block all traffic. - Domain and address range rules apply independently: domain rules do not narrow the IP addresses allowed by
subnets.allow. Code can reach any IP in an allowed range by using a literal IP address or a custom DNS resolver, and this traffic bypasses SNI filtering, credentials brokering, and requests proxying. - Broad ranges override domain scoping:
0.0.0.0/0or::/0grants access to the entire Internet regardless of domain rules. Use domain rules for per-domain enforcement, andsubnets.allowwith exact ranges when you need raw IP access. subnets.allowleaves DNS unrestricted: a policy with allowed address ranges and no allowed domains can resolve any hostname, even when its ranges include only private networks, and code can use those lookups to send data over DNS. Use domain rules to restrict DNS resolution, ordeny-allto block DNS entirely.
Domain-based rules identify traffic by the hostname negotiated during the TLS handshake. The following protocols are supported when filtering by domain.
HTTPS traffic is matched using the SNI (Server Name Indication) extension sent at the start of the TLS handshake. Plain-text HTTP cannot be filtered by domain, and must be allowed by IP range instead.
Domain matching reads the hostname from the SNI extension, which travels unencrypted at the start of the handshake. For an ordinary allowed domain the firewall matches this hostname and forwards the connection without terminating TLS, so it never decrypts the request and never inspects the HTTP Host header inside the encrypted connection. The firewall terminates TLS only for domains with transformation, forwarding, or direct-response rules; see TLS termination.
Because matching happens on the SNI alone, a client inside the sandbox can send an allowlisted hostname as the SNI while sending a different hostname in the HTTP Host header, a technique known as domain fronting. When the destination server or CDN accepts requests whose Host header differs from the SNI, the request can reach a different virtual host behind the same infrastructure than the one you allowed. Many providers reject mismatched requests, but the firewall does not prevent the mismatch by default. Traffic still only flows to endpoints that serve the allowlisted SNI; a domain allowlist constrains which hostname a connection negotiates, not which application behind that endpoint ultimately serves the request.
When this matters for your threat model, prefer allowlisting narrow, single-purpose hostnames whose infrastructure does not host other origins. To enforce policy on the request itself, use credentials brokering through transform or requests proxying through forwardURL. For these rules the firewall terminates TLS, so it can inspect and rewrite the request. Leave match off a rule you rely on as a safeguard: matchers narrow which requests a rule touches, and requests it skips pass through unchanged. You can also route traffic through your own proxy that enforces Host-level rules.
A transform rule can force the HTTP Host header to the allowed domain. Because the firewall terminates TLS for domains with transformation rules, the header is rewritten before the request leaves the sandbox, so every request that reaches the destination carries the hostname you allowed regardless of the Host header the client sent:
import { Sandbox } from '@vercel/sandbox';
// Pin the Host header on all requests to the allowed domain.
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"api.example.com": [{
transform: [{ headers: { "Host": "api.example.com" } }],
}],
},
},
});
Postgres connections to hosted databases are supported when the database host is added to a sandbox's allowed domains. Because the Postgres wire protocol negotiates TLS after the TCP connection is established, the firewall handles this handshake explicitly and applies the domain policy before forwarding the connection.
The following limitations apply when allowing Postgres traffic:
- TLS is required. Clients must connect with
sslmode=requireor stricter. Plain-text Postgres cannot be filtered by domain, and must be allowed by IP range instead. - GSSAPI-encrypted connections are not supported. Clients using
gssencmode=preferwill fall back to TLS automatically.gssencmode=requirewill not connect. sslmode=preferwill not downgrade. If the database does not support TLS, the connection will fail rather than fall back to plain-text.- Credentials brokering and other request transformations are not supported on Postgres connections. Transformation rules on a domain are ignored when the domain is reached over Postgres.
Commands running in the sandbox often require authentication with external services, for instance code repositories or AI services. Providing API keys to those commands would risk abuse or exfiltration. On the other hand, allowing access to a domain can allow data exfiltration if not restricting the permissions or sessions attached to it.
Credentials brokering injects credentials into egressing traffic. The secrets never enter the sandbox, so code running inside it cannot exfiltrate them.
Each rule can define matchers on the path, method, query parameters, and headers. When defined, only matching requests are transformed. A request that matches no rule still reaches the domain unchanged, without the injected credential. Matchers select requests for an action. To reject other requests, add a final direct-response rule.
Brokering relies on the client sending an SNI (Server Name Indication). When a policy combines a catch-all (*) rule with per-domain transforms, connections without a detectable domain pass through unmodified. Examples are TLS without SNI, or non-TLS protocols such as SSH. Use a restrictive allowlist without a catch-all if you need domain-less traffic to be denied.
import { Sandbox } from '@vercel/sandbox';
// Sandbox has access to everything, with credential brokering for two specific domains.
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"ai-gateway.vercel.sh": [{
transform: [{ headers: { "Authorization": `Bearer ${process.env.AI_GATEWAY_TOKEN}` } }],
}],
"*.github.com": [{
transform: [{ headers: { "Authorization": `Bearer ${process.env.GITHUB_TOKEN}` } }],
}],
// Allow traffic to all other domains. If unset only defined ones are reachable.
"*": []
}
}
});
// Apply deny-all to an eligible attached session, then save it for future sessions.
await sandbox.update({ networkPolicy: 'deny-all' });
// Apply and save an AI Gateway-only policy with the same key.
await sandbox.update({
networkPolicy: {
allow: {
"ai-gateway.vercel.sh": [{
transform: [{ headers: { "Authorization": `Bearer ${process.env.AI_GATEWAY_TOKEN}` } }],
}],
},
},
});
Requests proxying allows forwarding traffic toward specific domains to a proxy you control, for logging, debugging, or transformation purposes. This is useful when you want to allow access to a domain while ensuring control over the requests and responses.
The forwardURL field must be a URL pointing to an HTTP/1.1-capable server and must not include a query string or fragment. Each rule can define matchers on the path, method, query parameters, and headers. When defined, only matching requests are forwarded. A request that matches no rule is not forwarded: it goes directly to the destination as ordinary allowed traffic.
To restrict a domain to specific paths, use matched rules followed by a direct-response rejection rule. You can also use a forwarding rule without matchers to send every request through your proxy and reject unwanted requests there. A matched forwarding rule alone lets non-matching requests reach the destination directly.
Like brokering, forwarding is SNI-dependent. Under a catch-all (*) policy, domain-less traffic passes through unforwarded. Use a restrictive allowlist without a catch-all to deny it.
The forwardURL receives the original request as-is, with the addition of the following headers:
-
vercel-forwarded-host: The original request's SNI -
vercel-forwarded-scheme: The original request's scheme -
vercel-forwarded-port: The original request's port -
vercel-forwarded-path: The original request's path -
vercel-sandbox-oidc-token: A Edgenivo-issued OIDC token that the proxy can use to authenticate the request.The OIDC token's audience (
aud) is the configuredforwardURL. The proxy should verify the token signature, issuer, expiry, and thataudexactly matches its expectedforwardURL; this prevents accepting tokens minted for another forwarding endpoint. The token contains the following additional claims about the sandbox that the request originated from:team_id: The ID of the Edgenivo team the sandbox belongs to.project_id: The ID of the Edgenivo project the sandbox belongs to.sandbox_id: The sandbox's ID.sandbox_name: The sandbox's name, when using persistent sandboxes.
We recommend using the defineSandboxProxy helper from @vercel/sandbox/proxy to setup a proxy with automatic OIDC validation and extracted metadata.
import { Sandbox } from '@vercel/sandbox';
// Sandbox has access to everything, with a proxy forwarding for *.github.com.
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"*.github.com": [{
forwardURL: "https://my-proxy.vercel.app/github"
}],
// Allow traffic to all other domains. If unset only defined ones are reachable.
"*": []
}
}
});
Implement the proxy server yourself, or use the TypeScript-only defineSandboxProxy helper inside a Edgenivo Function or a fetch Web Standard compatible server:
import { defineSandboxProxy } from '@vercel/sandbox/proxy';
const proxy = defineSandboxProxy(async (request, { teamId, projectId, sandboxId, sandboxName }) => {
// Perform additional validation, logging, or transformation here.
return await fetch(request);
})
// Per-method Web Handler in Next.js or Edgenivo Function:
export const GET = proxy;
export const POST = proxy;
// fetch Web Handler in Edgenivo Function, handles all methods:
export default {
fetch: proxy
}Direct-response rules return a response from the network proxy without contacting the destination. To restrict paths on an allowed domain, place rules for accepted paths first and a rejection rule without matchers last. The first matching rule handles each request.
This policy injects a credential for POST requests to /v1/ paths and returns 403 for all other requests to api.example.com:
{
"allow": {
"api.example.com": [
{
"match": {
"path": { "startsWith": "/v1/" },
"method": ["POST"]
},
"transform": [
{ "headers": { "x-api-key": "your_api_key_here" } }
]
},
{ "response": { "statusCode": 403 } }
]
}
}A direct response requires an integer status code from 200 through 599. You can include response headers and a string body. Header names must be unique, ignoring case. A body requires an explicit, nonempty content type, even for an empty string or when the headers include Content-Type. Omit the body for status codes 204, 205, and 304.
The proxy manages connection, content-length, keep-alive, proxy-connection, trailer, transfer-encoding, and upgrade. You cannot set these response headers, regardless of capitalization.
See the TypeScript and Python SDK references for complete policy examples and response fields.
The firewall makes two separate decisions:
- Access: the domain list decides whether a request gets through at all.
- Handling: matchers select requests for credentials brokering, forwarding, or a direct response.
With matchers set, the rule applies to the subset of requests that match every dimension you include. Without matchers, the rule applies to every request to the domain. If a rule does not match, evaluation continues with the next rule. A request that matches no rule goes directly to the allowed domain, with no forwarding and no injected headers.
When multiple rules target the same domain, the firewall evaluates them in order and the first match wins. A rule without matchers matches every request and shadows later rules for the same domain.
Matchers alone never block traffic. To restrict an allowed domain to specific paths, put rules for those paths first, followed by a direct-response rejection rule without matchers. The final rule rejects requests that earlier rules did not handle; see direct responses.
The domain is not a match dimension. It is the key the rule is defined under, and it has already matched by the time rules are evaluated.
The following dimensions are supported:
path: Matches on the request path. Comparison is case-sensitive.method: HTTP methods to match. Any single match succeeds (OR semantics).queryString: Query-string entry matchers. Multiple entries are ANDed. Query parameter names and values are both compared case-sensitively (RFC 3986). When a request has multiple values for the same key, any matching value satisfies the matcher.headers: Header matchers. Multiple entries are ANDed. Header names are compared case-insensitively (RFC 9110); header values are compared case-sensitively. When a request has multiple values for the same header, any matching value satisfies the matcher.
Matcher supports exact, prefix, or regex matching:
exact: Match the value exactly. Case-sensitive for paths, header values, and methods; case-insensitive for domains and header keys.startsWith: Match values that start with the given prefix.regex: Match values against an RE2 regular expression. Anchor with ^ or $ as needed.
import { Sandbox } from '@vercel/sandbox';
const sandbox = await Sandbox.create({
networkPolicy: {
allow: {
"ai-gateway.vercel.sh": [{
match: {
path: { exact: "/v1/chat/completions" },
method: ["POST"],
queryString: [{ key: { exact: "model" }, value: { startsWith: "gpt-" } }],
},
transform: [{ headers: { "Authorization": `Bearer ${process.env.AI_GATEWAY_TOKEN}` } }],
}],
"*.github.com": [{
match: {
headers: [{
key: { exact: "Content-Type" },
value: { startsWith: "application/" }
}],
method: ["GET", "POST"]
},
forwardURL: "https://my-proxy.vercel.app/github"
}]
}
}
});
To apply transformation, forwarding, and direct-response rules, the firewall terminates TLS connections. It only terminates connections to domains with these rules.
A unique, per-sandbox CA is added to the system certificates. Standard environment variables are configured automatically to ensure compatibility with most clients. If your application uses a custom CA bundle, configure it to trust the mounted certificate. See Proxy CA certificates.
Policies can be defined on sandboxes on creation, ensuring they will never run without them.
# Sandbox has full Internet and secure-compute access (default).
sandbox create --network-policy allow-all
# Sandbox has no Internet or secure-compute access.
sandbox create --network-policy deny-all
# Sandbox only gets access to listed websites.
sandbox create --allowed-domain "*.google.com" --allowed-domain ai-gateway.vercel.sh
You can replace a sandbox's network policy without restarting its running session. A sandbox-level update applies the policy to an eligible attached session first, then saves it for future sessions. It never resumes a stopped sandbox. If no session is attached, or the cached session status is stopped, stopping, aborted, or failed, the update only saves the policy.
Live and saved updates are not atomic. If applying the live policy fails, the update stops before saving configuration and tags. If saving fails afterward, the running session can retain the new policy.
Each supplied policy replaces the previous policy in full. Use the allow-all mode to remove restrictions. Omitting the policy leaves it unchanged. Keep the original policy separately for updates: policy readback is sanitized and incomplete, so it cannot reconstruct the policy you supplied.
For workloads that must start with a restricted policy, configure the policy at creation. To require a successful live update on a specific session before submitting work, acquire a handle for that session, update its policy, then run the workload through the same handle. Acquiring a session may resume a stopped sandbox. Subsequent operations on a pinned session propagate errors instead of resuming a replacement session.
For a named sandbox, a session-level update also saves the policy for future sessions when the target is still its current session. Updating an older session does not replace the current saved policy. The sandbox handle's cached policy can remain stale after a session-level update.
See the TypeScript and Python SDK references for update methods and session behavior, including deprecated methods that can auto-resume a sandbox. See TypeScript policy readback and Python policy metadata for readback details.
The following examples install dependencies and download data, then request a restrictive policy:
sandbox create --network-policy allow-all
# Install packages
sandbox exec my-sandbox -- npm install
# Download data
sandbox exec my-sandbox -- aws s3 cp s3://my-bucket/dataset .
# Update the network policy
sandbox config network-policy my-sandbox --network-policy deny-all
Was this helpful?