# Networking and security | Modal Docs

- **URL:** https://modal.com/docs/guide/sandbox-networking
- **Summary:** 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.

Copy page

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:

PythonJavaScriptGo

    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.

PythonJavaScriptGo

    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: &modal.Allowlist{Entries: []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:

PythonJavaScriptGo

    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: &modal.Allowlist{Entries: []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.

PythonJavaScriptGo

    # 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"],
    )

    // Start with all outbound traffic allowed.
    const sb = await modal.sandboxes.create(app, image, {
      command: ["sleep", "infinity"],
      outboundDomainAllowlist: ["*"],
      outboundCidrAllowlist: ["0.0.0.0/0"],
    });
    
    // ... later, narrow the policy to only the domains we need.
    await sb.updateNetworkPolicy({
      outboundDomainAllowlist: ["api.openai.com", "*.github.com"],
      outboundCidrAllowlist: [],
    });
    
    // Or block all outbound traffic by passing empty allowlists.
    await sb.updateNetworkPolicy({
      outboundDomainAllowlist: [],
      outboundCidrAllowlist: [],
    });
    
    // Widen back to allow-all when needed.
    await sb.updateNetworkPolicy({
      outboundDomainAllowlist: ["*"],
      outboundCidrAllowlist: ["0.0.0.0/0"],
    });

    // Start with all outbound traffic allowed.
    sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
    	Command:                []string{"sleep", "infinity"},
    	OutboundDomainAllowlist: &modal.Allowlist{Entries: []string{"*"}},
    	OutboundCIDRAllowlist:   &modal.Allowlist{Entries: []string{"0.0.0.0/0"}},
    })
    
    // ... later, narrow the policy to only the domains we need.
    err = sb.UpdateNetworkPolicy(ctx, &modal.SandboxUpdateNetworkPolicyParams{
    	OutboundDomainAllowlist: &modal.Allowlist{Entries: []string{"api.openai.com", "*.github.com"}},
    	OutboundCIDRAllowlist:   &modal.Allowlist{Entries: []string{}},
    })
    
    // Or block all outbound traffic by passing empty allowlists.
    err = sb.UpdateNetworkPolicy(ctx, &modal.SandboxUpdateNetworkPolicyParams{
    	OutboundDomainAllowlist: &modal.Allowlist{Entries: []string{}},
    	OutboundCIDRAllowlist:   &modal.Allowlist{Entries: []string{}},
    })
    
    // Widen back to allow-all when needed.
    err = sb.UpdateNetworkPolicy(ctx, &modal.SandboxUpdateNetworkPolicyParams{
    	OutboundDomainAllowlist: &modal.Allowlist{Entries: []string{"*"}},
    	OutboundCIDRAllowlist:   &modal.Allowlist{Entries: []string{"0.0.0.0/0"}},
    })

The new policy takes effect immediately. Established connections that the new policy no longer permits are terminated.

#### Dynamic policy limitations 

*   Each allowlist type must be set at creation time to be usable later. To update `outbound_domain_allowlist` at runtime, the Sandbox must be created with `outbound_domain_allowlist` (e.g. `["*"]`). The same applies to `outbound_cidr_allowlist` — create with `["0.0.0.0/0"]` if you want to restrict by CIDR later.
*   `block_network=True` is not compatible with this API. Use empty allowlists (`[]`) to block all traffic instead.

Inbound access control 

Use `inbound_cidr_allowlist` to restrict which IP addresses can connect **inbound** to the Sandbox through tunnels and Sandbox Connect Tokens:

PythonJavaScriptGo

    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:

PythonJavaScriptGo

    # 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:

1.  By default, requests are routed to port 8080 in the container. Pass `port` to `create_connect_token()` to route to a different port.
2.  The token may be sent in an `Authorization` header, in a `_modal_connect_token` query param, or in a `_modal_connect_token` cookie.
3.  If `_modal_connect_token` is set as a query param, the resulting response will include a `Set-Cookie` header that sets it as a cookie.
4.  The `user_metadata` must be JSON-serializable and must be less than 512 characters after serialization.

### Forwarding ports 

While it is recommended to use [Sandbox Connect Tokens](https://modal.com/docs/guide/sandbox-networking#connecting-to-sandboxes-with-http-and-websockets)
 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`](https://modal.com/docs/sdk/py/latest/modal.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](https://modal.com/docs/guide/tunnels)
.

### 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.com

Modal 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](https://gvisor.dev/)
, 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](https://modal.com/docs/guide/restricted-access)
. As a result, the blast radius of any malicious code will be limited to the Sandbox container itself.

[Networking and security](https://modal.com/docs/guide/sandbox-networking#networking-and-security)
[Outbound access control](https://modal.com/docs/guide/sandbox-networking#outbound-access-control)
[Blocking all network access](https://modal.com/docs/guide/sandbox-networking#blocking-all-network-access)
[Restricting by IP range (CIDR allowlist)](https://modal.com/docs/guide/sandbox-networking#restricting-by-ip-range-cidr-allowlist)
[Restricting by domain name (domain allowlist)](https://modal.com/docs/guide/sandbox-networking#restricting-by-domain-name-domain-allowlist)
[Updating the network policy at runtime](https://modal.com/docs/guide/sandbox-networking#updating-the-network-policy-at-runtime)
[Dynamic policy limitations](https://modal.com/docs/guide/sandbox-networking#dynamic-policy-limitations)
[Inbound access control](https://modal.com/docs/guide/sandbox-networking#inbound-access-control)
[Connecting to Sandboxes with HTTP and WebSockets](https://modal.com/docs/guide/sandbox-networking#connecting-to-sandboxes-with-http-and-websockets)
[Forwarding ports](https://modal.com/docs/guide/sandbox-networking#forwarding-ports)
[Custom domains](https://modal.com/docs/guide/sandbox-networking#custom-domains)
[Security model](https://modal.com/docs/guide/sandbox-networking#security-model)

See it in action

[Running a Jupyter notebook](https://modal.com/docs/examples/jupyter_sandbox)

[Safe code execution](https://modal.com/docs/examples/safe_code_execution)
