Networking and security
Sandboxes are built to be secure-by-default, meaning that a default Sandbox has no ability to accept incoming network connections or access your Modal resources.
Outbound access control
By default, Sandboxes can make outbound connections to any public IP address. Modal provides three levels of outbound network restriction:
| Level | Parameter | What it controls |
|---|---|---|
| Full block | block_network=True | Drops all outbound traffic. |
| IP-range allowlist | outbound_cidr_allowlist | Only allows traffic to the listed CIDR ranges (any protocol). |
| Domain allowlist (Beta) | outbound_domain_allowlist | Only allows TLS traffic (port 443) to the listed domain names. |
outbound_cidr_allowlist and outbound_domain_allowlist can be combined additively - traffic that meets either criteria will be let through.
Blocking all network access
Set block_network=True to prevent the Sandbox from making any outbound
connections:
sb = modal.Sandbox.create(
"python", "my_script.py",
block_network=True,
app=app,
)const sb = await modal.sandboxes.create(app, image, {
command: ["python", "my_script.py"],
blockNetwork: true,
});sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"python", "my_script.py"},
BlockNetwork: true,
})When block_network is enabled, outbound_cidr_allowlist, outbound_domain_allowlist, and inbound_cidr_allowlist cannot be used.
Restricting by IP range (CIDR allowlist)
Use outbound_cidr_allowlist to restrict outbound traffic to a set of IP
ranges. All traffic to IPs outside these ranges (except traffic allowed by outbound_domain_allowlist) is blocked.
sb = modal.Sandbox.create(
"sleep", "infinity",
outbound_cidr_allowlist=["52.0.0.0/8", "10.0.1.0/24"],
app=app,
)const sb = await modal.sandboxes.create(app, image, {
command: ["sleep", "infinity"],
outboundCidrAllowlist: ["52.0.0.0/8", "10.0.1.0/24"],
});sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"sleep", "infinity"},
OutboundCIDRAllowlist: []string{"52.0.0.0/8", "10.0.1.0/24"},
})Restricting by domain name (domain allowlist)
Use outbound_domain_allowlist to restrict outbound TLS traffic to a set of
domain names:
sb = modal.Sandbox.create(
"sleep", "infinity",
outbound_domain_allowlist=["api.openai.com", "*.github.com"],
app=app,
)const sb = await modal.sandboxes.create(app, image, {
command: ["sleep", "infinity"],
outboundDomainAllowlist: ["api.openai.com", "*.github.com"],
});sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"sleep", "infinity"},
OutboundDomainAllowlist: []string{"api.openai.com", "*.github.com"},
})When a domain allowlist is set:
- TLS (port 443) connections are allowed only to the listed domains. Connections to non-allowlisted domains are securely blocked and logged to the Sandbox’s system output stream.
- Non-TLS traffic (HTTP, raw TCP, UDP) to IPs that are not on a CIDR allowlist is blocked.
Entries prefixed with *. match the parent domain and any subdomain:
| Allowlist entry | Matches | Does not match |
|---|---|---|
example.com | example.com | sub.example.com |
*.example.com | example.com, a.example.com, a.b.example.com | evilexample.com |
Updating the network policy at runtime
You can replace the outbound network policy of a running Sandbox without restarting it. This is useful when an agent’s trust level changes mid-session — for example, starting with broad access while installing dependencies and then locking down to only the domains a tool needs.
# Start with all outbound traffic allowed.
sb = modal.Sandbox.create(
"sleep", "infinity",
outbound_domain_allowlist=["*"],
outbound_cidr_allowlist=["0.0.0.0/0"],
app=app,
)
# ... later, narrow the policy to only the domains we need.
sb._experimental_set_outbound_network_policy(
outbound_domain_allowlist=["api.openai.com", "*.github.com"],
)
# Or block all outbound traffic by passing empty allowlists.
sb._experimental_set_outbound_network_policy(
outbound_domain_allowlist=[],
outbound_cidr_allowlist=[],
)
# Widen back to allow-all when needed.
sb._experimental_set_outbound_network_policy(
outbound_domain_allowlist=["*"],
outbound_cidr_allowlist=["0.0.0.0/0"],
)The new policy takes effect immediately. Established connections that the new policy no longer permits are terminated.
Both outbound_domain_allowlist and outbound_cidr_allowlist can be passed to _experimental_set_outbound_network_policy and work the same way as on Sandbox.create.
Dynamic policy limitations
- Each allowlist type must be set at creation time to be usable later. To
update
outbound_domain_allowlistat runtime, the Sandbox must be created withoutbound_domain_allowlist(e.g.["*"]). The same applies tooutbound_cidr_allowlist— create with["0.0.0.0/0"]if you want to restrict by CIDR later. block_network=Trueis not compatible with this API. Use empty allowlists ([]) to block all traffic instead.- Only available in the Python SDK. Go and JavaScript support is not yet available.
Inbound access control
Use inbound_cidr_allowlist to restrict which IP addresses can connect inbound to the Sandbox through tunnels and Sandbox Connect Tokens:
sb = modal.Sandbox.create(
"python", "-m", "http.server", "8080",
encrypted_ports=[8080],
inbound_cidr_allowlist=["203.0.113.0/24"],
app=app,
)const sb = await modal.sandboxes.create(app, image, {
command: ["python", "-m", "http.server", "8080"],
encryptedPorts: [8080],
inboundCidrAllowlist: ["203.0.113.0/24"],
});sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"python", "-m", "http.server", "8080"},
EncryptedPorts: []int{8080},
InboundCIDRAllowlist: []string{"203.0.113.0/24"},
})Connecting to Sandboxes with HTTP and WebSockets
You can make authenticated HTTP and WebSocket requests to a Sandbox by generating Sandbox Connect Tokens. They work like this:
# Start a Sandbox with a server running on port 8080.
sb = modal.Sandbox.create(
"bash", "-c", "python3 -m http.server 8080",
app=my_app,
)
# Create a connect token, optionally including arbitrary user metadata.
# Port 8080 is the default and could be omitted here.
creds = sb.create_connect_token(user_metadata={"user_id": "foo"}, port=8080)
# Make an HTTP request, passing the token in the Authorization header.
requests.get(creds.url, headers={"Authorization": f"Bearer {creds.token}"})
# You can also put the token in a `_modal_connect_token` query param.
url = f"{creds.url}/?_modal_connect_token={creds.token}"
ws_url = url.replace("https://", "wss://")
with websockets.connect(ws_url) as socket:
socket.send("Hello world!")
sb.detach()// Start a Sandbox with a server running on port 8080.
const sb = await modal.sandboxes.create(app, image, {
command: ["bash", "-c", "python3 -m http.server 8080"],
});
// Create a connect token, optionally including arbitrary user metadata.
// Port 8080 is the default and could be omitted here.
const creds = await sb.createConnectToken({
userMetadata: '{"user_id": "foo"}',
port: 8080,
});
// Make an HTTP request, passing the token in the Authorization header.
const response = await fetch(creds.url, {
headers: { Authorization: `Bearer ${creds.token}` },
});
sb.detach();// Start a Sandbox with a server running on port 8080.
sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"bash", "-c", "python3 -m http.server 8080"},
})
// Create a connect token, optionally including arbitrary user metadata.
// Port 8080 is the default and could be omitted here.
creds, err := sb.CreateConnectToken(ctx, &modal.SandboxCreateConnectTokenParams{
UserMetadata: `{"user_id": "foo"}`,
Port: 8080,
})
// Make an HTTP request, passing the token in the Authorization header.
req, _ := http.NewRequestWithContext(ctx, "GET", creds.URL, nil)
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", creds.Token))
resp, _ := http.DefaultClient.Do(req)
sb.Detach()The server running on the specified port in the container will receive an authenticated
request with an unspoofable X-Verified-User-Data header whose value is the
JSON-serialized metadata that was passed as user_metadata to create_connect_token(). This can be used by the application to
determine access control, for example.
There are a few things to remember with Sandbox Connect Tokens:
- By default, requests are routed to port 8080 in the container. Pass
porttocreate_connect_token()to route to a different port. - The token may be sent in an
Authorizationheader, in a_modal_connect_tokenquery param, or in a_modal_connect_tokencookie. - If
_modal_connect_tokenis set as a query param, the resulting response will include aSet-Cookieheader that sets it as a cookie. - The
user_metadatamust be JSON-serializable and must be less than 512 characters after serialization.
Forwarding ports
While it is recommended to use Sandbox Connect Tokens for HTTP requests and WebSocket connections to the container, you can also expose raw TCP ports to the internet. This is useful if, for example, you want to run a server inside the Sandbox that expects a raw TCP connection and handles authentication itself.
Use the encrypted_ports and unencrypted_ports parameters of Sandbox.create to specify which ports to forward. You can then access the public URL of a tunnel
using the Sandbox.tunnels method:
import requests
import time
sb = modal.Sandbox.create(
"python",
"-m",
"http.server",
"12345",
encrypted_ports=[12345],
app=my_app,
)
tunnel = sb.tunnels()[12345]
time.sleep(1) # Wait for server to start.
print(f"Connecting to {tunnel.url}...")
print(requests.get(tunnel.url, timeout=5).text)
sb.detach()It is also possible to create an encrypted port that uses HTTP/2 rather than HTTP/1.1 with the h2_ports option. This will return
a URL that you can make H2 (HTTP/2 + TLS) requests to. If you want to run an HTTP/2 server inside a sandbox, this feature may be useful.
Here is an example:
import time
port = 4359
sb = modal.Sandbox.create(
app=my_app,
image=my_image,
h2_ports=[port],
)
p = sb.exec("python", "my_http2_server.py")
tunnel = sb.tunnels()[port]
time.sleep(1)
print(f"Tunnel URL: {tunnel.url}")
sb.detach()For more details on how tunnels work, see the tunnels guide.
Custom domains
By default, Sandbox tunnels are served from subdomains of w.modal.host.
In some cases, it’s necessary to have a tunnel served through a custom domain
for security reasons. This is possible with manual setup.
Note that tunnel custom domains are distinct from other custom domains in Modal.
Other custom domains use CNAME forwarding. For tunnels, we need to use an NS record to delegate the domain to Modal’s nameservers.
1. Delegate a (sub)domain to Modal’s nameservers.
Add NS records to your DNS zone pointing to Modal’s nameservers. For example,
to use sandbox.example.com, add the following records in your DNS provider’s
control panel:
| Name | Type | Value |
|---|---|---|
sandbox.example.com | NS | w-ns-a.modal.host. |
sandbox.example.com | NS | w-ns-b.modal.host. |
sandbox.example.com | NS | w-ns-c.modal.host. |
sandbox.example.com | NS | w-ns-d.modal.host. |
You can delegate any subdomain depth you like (e.g. tunnels.a.b.c.example.com).
2. Ask Modal to set up the domain.
Reach out to us on Slack and provide the domain name. We’ll enable it for your workspace.
3. Pass custom_domain to Sandbox.create.
import modal
app = modal.App.lookup("my-app", create_if_missing=True)
sb = modal.Sandbox.create(
"python", "-m", "http.server", "8080",
encrypted_ports=[8080],
custom_domain="sandbox.example.com",
app=app,
)
tunnel = sb.tunnels()[8080]
print(tunnel.url) # https://[...].sandbox.example.comModal will provision a TLS certificate automatically. Sandbox Connect Tokens generated for this sandbox will also use the custom domain.
Security model
Sandboxes are built on top of gVisor, a container runtime by Google that provides strong isolation properties. gVisor has custom logic to prevent Sandboxes from making malicious system calls, giving you stronger isolation than most other container runtimes.
Additionally, Sandboxes are not authorized to access other resources in your Modal workspace the way that Modal Functions are by default. As a result, the blast radius of any malicious code will be limited to the Sandbox container itself.