Add an address-layer egress allowlist for hosted sandboxes - #73534
Merged
Merged
Conversation
kaxil
force-pushed
the
sandbox-egress-cidr
branch
from
September 22, 2026 11:01
161ec5f to
7490606
Compare
The Modal backend offered two egress modes: deny everything, which is exact, or a hostname allowlist matched on the TLS handshake name, which is weak in ways the author cannot see and so needs an explicit opt-in. Modal has a third mode the backend did not expose: an address allowlist enforced on the destination for any port and protocol, deny-by-default outside the list. SandboxSpec gains allow_egress_to_cidrs. On Modal it maps onto outbound_cidr_allowlist and needs no opt-in, because there is no caveat to accept; entries are validated and normalised to canonical CIDR form, since Modal passes the list through unchecked and a hostname or a range with host bits set would silently mean something other than what was written. It can be combined with the hostname list, which Modal applies additively. sbx refuses the field, having no per-sandbox address rule. Measured live: a listed address connects on 443 and 53, an unlisted one is dropped so the client times out, and hostnames still resolve through Modal's own resolver. The tool description tells the model both facts, so a successful lookup is not read as a reachable host. The system test gains a task that checks all three on a real sandbox.
kaxil
force-pushed
the
sandbox-egress-cidr
branch
from
September 22, 2026 11:09
7490606 to
83c68ee
Compare
vatsrahul1001
approved these changes
Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Modal sandbox backend offered two egress modes.
block_network=Trueis exact. A hostname allowlist is matched on the TLS handshake name, and it is refused unless the author opts in withegress_enforcement="sni", because it is weak in ways the spec does not show: TLS on 443 only, destination address ignored, name resolution open for every host. That left "deny everything or accept the caveats" as the only choice, when Modal has a third mode the backend never exposed.outbound_cidr_allowlistis enforced on the destination address for any port and protocol and denies by default outside the list.This adds
SandboxSpec.allow_egress_to_cidrs. On Modal it maps onto that allowlist and needs no opt-in, since there is no caveat for the author to accept. It is the mode for the case the hostname list serves worst and a platform team most often means by restricted egress: this sandbox may reach one internal service at a fixed address and nothing else. It cannot serve a package registry behind a CDN, whose addresses rotate faster than a sandbox lives, so the hostname list stays for that job.sbxrefuses the field; its per-sandbox rule takes hostnames and there is nothing to map a range onto.Measured on a live sandbox before writing the docs, because the open question on this design was whether name resolution survives an address-only list. With
["1.1.1.1/32"]: the listed address connected on 443 and on 53, an unlisted address timed out on both, andpypi.orgstill resolved through Modal's own resolver inside the sandbox. So hostnames resolve and then their addresses are unreachable. The tool description says exactly that, so the model does not read a successful lookup as a reachable host. It also means DNS is still a channel out under this mode, as under the hostname list; onlyblock_network=Truewith no allowlist closes it, and the docs say so.Entries are validated and normalised, since Modal passes the list through unchecked. A hostname, a URL or a
host:portwould be accepted by Modal and match nothing while reading as a restriction, so they are refused. A bare address is written as/32. A range with host bits set, such as203.0.113.1/24, is refused rather than widened to203.0.113.0/24, because that is not what the author wrote.0.0.0.0/0is refused as well: an allowlist of every address is an open network wearing a restriction's clothes, andblock_network=Falseis the honest way to ask for one. IPv6 entries are refused too, with the reason: Modal's allowlist rejects them at create (Network access allowlist does not support IPv6 CIDRs) and the sandbox has no IPv6 route, so accepting one could only fail the task later and less legibly.The two lists combine, and combining weakens the address one. Modal applies them additively, and I measured what that means: adding
pypi.orgto the hostname list beside["1.1.1.1/32"]made a TCP connection to8.8.8.8:443succeed, because port 443 is then routed by handshake name for every address. A combined spec therefore has the address list's guarantee on every port except 443 and the hostname list's caveats there. The hostname half keeps its opt-in when combined, so adding an address list cannot launder the hostname list past it. The backend also logs a warning at create when both lists are set, naming which hostnames reopen port 443 and which ranges keep their guarantee elsewhere, so the weakening is symmetric and visible from both sides.The destination has to be a public address. The natural use case reads as "our internal service", so I tested that too: connections to
10.20.0.1,172.16.0.1,192.168.1.1and the cloud metadata address timed out from a Modal sandbox both under an open network and with those ranges on the allowlist. Private ranges are unreachable from a hosted sandbox whatever the spec says, so the field serves one service at a fixed public address, and the docs say so rather than promising an internal one.Why a spec field rather than a backend parameter. The tool description reads the spec to tell the model what it can reach, and a network policy split across two objects is one the description would get wrong. A spec field also follows the contract's existing rule: a backend enforces it or refuses it, so a Dag moved to
sbxfails loudly instead of running with an open network the author believed was closed.The Modal system test gains a second task that checks the three measured facts on a real sandbox. No behaviour changes for specs that do not set the new field: the default,
block_network=False, and the hostname list under both enforcement settings map exactly as before, and the existing tests for each still pass unchanged.In the Airflow UI. One Dag, four tasks, the same probe from four sandboxes. Three report what they could and could not reach; the fourth is refused before a sandbox exists because it asks for the hostname allowlist without accepting its caveats.
The default spec denies everything, name resolution included:
The address allowlist
["1.1.1.1/32"]: the listed address answers on 443 and 53,8.8.8.8times out,pypi.organdexample.comresolve and are then unreachable:The hostname allowlist
["pypi.org"]under the opt-in: HTTPS topypi.orgworks,example.comis reset at the handshake, plain HTTP topypi.orgfails, and raw TCP to any address on 443 connects because that port is routed by handshake name:The same hostname allowlist without the opt-in is refused at the first tool call, with the message naming the way out:
Rendered docs. The Modal backend page gains the address allowlist as a third mode, with the measurements and the four caveats:
The configuration page's network paragraph now names both lists and which backend enforces each:
The backend comparison and the limitations list say where the field is refused and what it cannot serve:
{pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.