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 |
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.
creds = sb.create_connect_token(user_metadata={"user_id": "foo"})
# 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()The server running on port 8080 in the container will receive an authenticated
request with an unspoofable X-Verified-User-Data header whose value is the
JSON-serialized Python dict that was passed as user_metadata to the create_connect_token() function. This can be used by the application to
determine access control, for example.
There are a few things to remember with Sandbox Connect Tokens:
- The server inside the container must be listening on port 8080.
- 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.