Sandboxes: network allowlists¶
A VM can be limited to the domains its work needs, and locked so that nothing inside it can lift the limit. That makes a VM a sandbox you can hand to an agent: it can build, test and push to GitHub, but it can't reach anything you didn't allow.
# GitHub and PyPI only, and no way to undo that from inside
limabean up agent --allow github --allow python --locked
limabean network ls agent # what it can reach
limabean network allow agent registry.npmjs.org # one more domain
limabean network rm agent python # one fewer
Claude can make the same kind of VM for itself through
MCP: create_vm takes allow and locked.
What's allowed¶
Everything not on the allowlist is blocked, for the VM and for its Docker
containers. A domain allows its subdomains, so github.com covers
api.github.com. Names that aren't allowed don't resolve at all, so data
can't leave through DNS lookups either.
Presets name common sets:
| Preset | Domains |
|---|---|
azure |
azure.com, microsoft.com, microsoftonline.com, windows.net, azureedge.net |
claude |
anthropic.com, claude.ai, claude.com |
docker |
docker.io, docker.com |
dotnet |
nuget.org, dot.net, microsoft.com |
elixir |
hex.pm, hexdocs.pm |
gcp |
googleapis.com, google.com, gstatic.com |
github |
github.com, githubusercontent.com, githubassets.com, ghcr.io |
go |
go.dev, golang.org, proxy.golang.org, sum.golang.org |
java |
maven.org, apache.org, gradle.org, gradle-dn.com |
npm |
npmjs.org, npmjs.com, yarnpkg.com |
python |
pypi.org, pythonhosted.org |
ruby |
rubygems.org, ruby-lang.org |
rust |
crates.io, rust-lang.org |
ubuntu |
ubuntu.com |
ntp.ubuntu.com is always allowed alongside an allowlist, for the clock.
Installers often fetch from a second domain, such as a CDN. When something
fails with Could not resolve host, limabean network log shows what was
blocked, and suggests the command that allows it:
Learn what a VM needs¶
Guessing an allowlist is the hard part. Learning mode records it instead: the VM's network stays open while it learns, and every name it or its containers look up is recorded. Do the work the sandbox is for, then turn what was used into the allowlist:
limabean up agent --learn # or, for a VM you have: limabean network learn agent
limabean ssh agent 'cd ~/code/github.com/me/app && make deps test'
limabean network log agent # what it looked up, and how often
limabean network learn agent --done # allow just that from now on
limabean network lock agent
--done allows each name's registered domain (objects.githubusercontent.com
is allowed as githubusercontent.com), on top of anything already on the
allowlist. Check the list it prints: a lookup made once by something you
didn't mean to run is allowed too. Lookups are kept in the VM until it
restarts.
Learning sees DNS lookups. A program that connects to a raw IP address, or brings its own DNS-over-HTTPS, isn't recorded, and is then blocked by the allowlist.
Lock it, or it's only a guardrail¶
A Lima VM's user has passwordless sudo, and Docker's socket is
root-equivalent. So in an unlocked VM, anything running as you, an agent
included, could turn the allowlist off. An unlocked allowlist still stops
accidents, but it isn't a boundary.
Locking takes those away from the VM's user: passwordless sudo, the
sudo and docker groups, and the Docker socket. The lock is re-applied on
every boot.
limabean up agent --allow github --locked # locked from the start
limabean network lock dev # or later
limabean network unlock dev # sudo now, Docker on restart
Lock a VM before giving it to an agent. A root process started before the lock keeps running until the VM restarts.
A locked VM has no docker for its user, and nothing that needs sudo
works. It suits agents that build and test code; for container work, leave
the VM unlocked, or use a separate one.
Commands¶
| Command | Does |
|---|---|
limabean up <name> --allow <x>… [--locked] [--learn] |
Creates a VM with an allowlist, locked or not, or learning |
limabean network ls [name] |
Shows the policy |
limabean network allow <name> <x>… |
Adds domains or presets; an open VM gets an allowlist of just these |
limabean network rm <name> <x>… |
Removes some; it won't remove the last one |
limabean network open <name> |
Removes the allowlist; a locked VM stays locked |
limabean network lock <name> / unlock |
Takes sudo and Docker away, or gives them back |
limabean network learn <name> [--done] |
Records what the VM looks up, then allows it |
limabean network log <name> [--blocked] |
Shows the names the VM looked up, and which were blocked |
limabean ls shows each VM's network in its NETWORK column.
How it works¶
The policy is a small file on your Mac,
~/.config/limabean/network/<vm>/network.conf, mounted read-only into
the VM at /mnt/host/limabean. Nothing inside the VM can edit it.
limabean network changes the file and asks the VM to apply it, using the one
sudo command a locked VM's user is still allowed.
Inside the VM, limabean-network applies it:
- dnsmasq answers only allowed domains; everything else is NXDOMAIN. It
adds the addresses it returns to nftables sets, and logs each lookup to
/run/limabean-network/dns.log, whichnetwork logreads. While a VM learns, dnsmasq answers every name and the firewall is off. - nftables drops every other packet leaving the VM or its containers. Only dnsmasq may reach the upstream DNS server.
- Containers resolve through dnsmasq too, on the
docker0address.
The first boot installs the toolchain with the network open; the policy
applies when limabean up or rebuild finishes. Later boots apply it as the
VM starts. If the policy can't be read, nothing is changed: a missing file is
never taken to mean "open".
VMs created before limabean had this need limabean rebuild first. The
design, and what it would take to enforce this on the Mac instead, is in
design note D15.