Limit where a Space connects (egress policies)
Allow and deny lists of addresses for a Space's outbound connections, enforced by the host outside the guest, from the first instruction.
An egress policy says which destinations a Space may open connections to. The machine the Space runs on enforces it outside the guest, so nothing in the guest, root included, can change it: there are no rules inside the guest to flush.
import cua
spaces = cua.embedded().spaces()
created = await spaces.create(cua.SpaceCreateOptions(
on="local",
egress=cua.EgressPolicy(allow=["1.1.1.1"], deny=["all"]),
))
space = created.space
assert space.egress_enforced == cua.EgressEnforcement.HOST
# Replace it while the Space runs (an empty policy removes it).
await spaces.set_egress(space.id, cua.EgressPolicy(deny=["10.0.0.0/8"]))The policy#
- Every
denyentry blocks that destination; everyallowentry is an exception that wins overdeny.deny=["all"]withallow=["1.1.1.1"]leaves only 1.1.1.1 reachable. - An
allowlist alone blocks nothing. Both lists empty: no policy. - Entries are IPv4 or IPv6 addresses and CIDRs;
allis0.0.0.0/0and::/0. A hostname is refused (InvalidArgument): rules are by address. - The destination is the address the guest sends to. DNS works while the resolver the guest uses is allowed: on a VM that is its virtual network's own resolver (
10.0.2.3under QEMU,172.16.0.1in a Firecracker microVM); a container uses its engine's resolvers. - Never filtered: loopback inside the guest, replies to connections made to the guest (cua-spacesd, published ports), IPv6 neighbor discovery.
Where it is enforced#
| Space | Enforced by |
|---|---|
| Container, runc or gVisor (any Docker engine, also the built-in runtime on a Mac) | nftables in the engine's own network namespace, on the Space's bridge port |
| Firecracker microVM (Linux) | iptables in the network namespace pasta creates around the VM |
| QEMU VM (Linux) | iptables in the network namespace pasta creates around QEMU |
A Space one of your machines provides (on="host:...", on="auto") | that machine, as above |
| macOS VM, Apple Containerization microVM, QEMU on macOS or Windows | refused (Unsupported): filtering them needs root (pf, WFP) |
| Cua Cloud, your own cloud account (AWS, Google Cloud, Modal), a Space added by address | refused (Unsupported): no host-side point cua controls |
SpaceInfo.egress_enforced is host for a Space with a policy. A machine whose cua predates egress policies never gets a Space with one: the create fails and the Space it made is deleted.
Lifetime#
- The policy is in place before the guest's first instruction: a container's rules exist before it starts (keyed by the MAC address it is created with), a VM's namespace holds them before the VM boots.
- It survives stop and start, suspend and resume, a restart of the daemon, forks and checkpoints (a fork inherits its source's policy).
set_egressapplies at once. A running QEMU VM started without a policy first moves into its own namespace: its memory is carried over to a new QEMU process in a moment, and connections through the old one end.
What changes for a Space with a policy#
- A container created with a policy has no raw sockets (
CAP_NET_RAW);pingstill works through ICMP datagram sockets. Other containers on the same engine reach it only through the host (published ports), not directly over the bridge. - A QEMU VM with a policy reaches nothing on the host's loopback through
10.0.2.2.
Requirements#
- Containers:
nft(nftables) where the container engine runs. - VMs on Linux:
pasta(the passt package) and unprivileged user namespaces. Ubuntu 24.04 restricts those to programs with an AppArmor profile; allow pasta with one that grantsuserns, or setkernel.apparmor_restrict_unprivileged_userns=0.
To check a policy from inside a Space as root: nft flush ruleset; iptables -F changes nothing, and a raw packet to a denied address gets no answer.